aCelery — Guía de usuario
Crea y ejecuta tus propias apps JavaScript en el móvil.
aCelery es un pequeño entorno de desarrollo que vive en tu dispositivo. Escribes una app en el editor integrado, pulsas Run y se abre: sin ordenador, sin paso de compilación, sin cuenta y sin tienda de apps. Una app es una carpeta de archivos normales: un manifiesto, un módulo de entrada y el CSS y las imágenes que quieras. Se comunica con SQLite, con el sistema de archivos del dispositivo y con la red a través de un pequeño conjunto de capacidades que aCelery le ofrece.
Esta guía tiene dos mitades.
- Parte 1 — Usar aCelery describe la propia app: las cinco pantallas, cómo ejecutar apps, cómo compartirlas con otros dispositivos de tu red y cómo conectar un asistente de IA.
- Parte 2 — Escribir apps de aCelery es la referencia de todo lo que una app puede importar y llamar.
La interfaz de aCelery está en inglés, así que esta guía muestra los nombres de botones, menús y pantallas tal como aparecen en la app, en negrita, a veces con su traducción entre paréntesis.
Licencia: GNU General Public License v3.0.
Parte 1 — Usar aCelery
1. Qué es aCelery
aCelery ejecuta dentro de sí un servidor web diminuto, en el puerto 8123, y te
muestra páginas servidas por él. Las pantallas del sistema que usas (Home, Apps,
Code, Data, Settings) están construidas igual que tus propias apps, con los
mismos componentes y sin acceso privilegiado a nada.
Esa única decisión de diseño es la razón por la que aCelery puede hacer algo poco habitual: como todo viaja ya por HTTP, puedes abrir aCelery, y tus propias apps, desde un navegador en otro ordenador conectado a la misma Wi-Fi. Responde el mismo servidor, no una segunda implementación. Consulta Compartir en tu red.
De ello se derivan dos cosas que conviene saber desde el principio:
- Tus apps se ejecutan en un WebView, el motor de navegador del propio dispositivo. Todo lo que puede hacer el navegador lo puede hacer tu app.
- Nada se empaqueta ni se minifica. El archivo que escribes es el archivo que se ejecuta, así que un mensaje de error señala tu línea.
2. Cómo moverse
aCelery tiene cinco destinos. En un móvil están en la parte inferior; en una tablet o en un navegador de escritorio, en una barra lateral a la izquierda.
| Destino | Para qué sirve |
|---|---|
| Home | Retomar lo que estabas haciendo o empezar algo nuevo |
| Apps | Todo lo que puedes ejecutar |
| Code | Proyectos, archivos y el editor |
| Data | Bases de datos, tablas y SQL |
| Settings | Tema, acceso a la red, copia de seguridad y restauración, información |
Cada pantalla tiene una dirección. #/code/Example/main.js es el editor abierto
en ese archivo; #/data/notes.db/table/note es esa tabla. El botón Atrás del
dispositivo retrocede por ellas como lo hace un navegador, y al recargar vuelves
a donde estabas, también después de ejecutar una app y volver.
En una pantalla ancha (992 px o más), Code muestra la lista de archivos y el editor uno junto al otro; en un móvil son pantallas separadas.
3. Home
Home se abre con lo último que estabas haciendo.
- Continue muestra el proyecto y la base de datos que tocaste más recientemente, con Open y Run en el proyecto. Si has borrado alguno desde entonces, la tarjeta desaparece sin más en lugar de llevar a ninguna parte.
- Create tiene New app y New database, que te llevan a Code o a Data con el diálogo ya abierto.
- On this device cuenta tus apps y bases de datos, y cada recuento es un enlace.
La primera vez que abres aCelery no hay nada que continuar, así que Home te ofrece ejecutar la app Example o crear la primera tuya. La app Example es la implementación de referencia: todas las funciones de la Parte 2 de esta guía se demuestran en ella, y su código es un único archivo legible que puedes abrir en Code.
4. Apps
Todo lo ejecutable, en forma de tarjetas. Al tocar una tarjeta se ejecuta la app.
El menú de cada tarjeta ofrece:
- Edit in Code: abre el proyecto en el editor.
- Add to home screen: pone un icono de esta app en la pantalla de inicio del dispositivo, de modo que se abre directamente en tu app. Solo en Android; iOS no ofrece a las apps ninguna forma de hacerlo.
- Export: comprime el proyecto en un zip y lo pasa al menú de compartir, para que puedas enviártelo por correo, guardarlo en Archivos o mandarlo a otro móvil.
- Delete: elimina la carpeta de la app y todo lo que contiene. No borra las bases de datos que usaba la app; esas viven en Data.
Cuando tienes más de seis apps aparece un cuadro de búsqueda, que busca por nombre y descripción.
5. Code
Code se abre con tus proyectos. New project pide un nombre (letras, números y
guion bajo, 16 caracteres como máximo) y una descripción opcional de una línea;
después crea la carpeta con un manifiesto y un main.js que ya funciona, y te
deja en el editor.
Import project (el icono de la barra superior) vuelve a cargar un zip de proyecto, ya sea uno exportado desde aquí o desde otro dispositivo. Solo dentro de la app aCelery: un navegador en la red no tiene acceso al selector de archivos del dispositivo.
La pantalla del proyecto
Un proyecto es su lista de archivos. Al tocar uno se abre en el editor.
- New file crea un
.jso un.cssen el proyecto. - Run lanza la app. Desde Code es una ejecución de depuración, que añade el Error log (registro de errores) al menú de la app en ejecución.
- El menú del proyecto también tiene Export project, Delete (este archivo) y Delete project.
Las imágenes se previsualizan en lugar de abrirse. Los archivos que no son de texto (zips, fuentes, audio, bases de datos) lo indican en lugar de abrirse como basura en el editor.
El editor
CodeMirror 6, elegido porque es el único editor importante con un soporte táctil práctico. Te ofrece números de línea, resaltado de sintaxis, emparejamiento y cierre de corchetes, plegado de código, historial de deshacer, búsqueda, autocompletado y resaltado de la línea activa.
El resaltado depende de la extensión: .js, .mjs, .json, .css, .html,
.htm, .xml y .svg. Cualquier otro archivo se abre con resaltado HTML.
Un punto junto al nombre del archivo indica cambios sin guardar. Save es el icono del disquete, pero rara vez lo necesitarás: ejecutar la app guarda primero, y lo mismo ocurre al salir de aCelery o al salir del editor con el botón Atrás del dispositivo. Si navegas a otra parte dentro de aCelery con trabajo sin guardar, te pregunta si quieres Save (guardar) o Discard (descartar); si cierras la pregunta, te quedas donde estabas.
6. Data
Data se abre con tus bases de datos SQLite. New database crea una; el nombre
admite letras, números, guion y guion bajo, y el archivo se guarda con la
extensión .db.
Al abrir una base de datos tienes dos pestañas.
Tables
Todas las tablas de la base de datos. Al abrir una obtienes el mismo explorador
que tus propias apps obtienen de TableMaint: una lista, una vista de registro,
edición, búsqueda y paginación, construidos a partir de las columnas reales de
la tabla. Puedes elegir qué columnas muestra la lista, ir a Structure para
ver las definiciones de columna que guarda SQLite, o usar Drop table para
eliminarla.
Es un explorador y editor de datos. Está pensado para ver lo que ha guardado tu app, corregir un valor a mano y borrar filas de prueba.
SQL
Un bloc de pruebas. Escribe una sentencia y pulsa Run, o Ctrl/Cmd + Intro.
SELECT,PRAGMA,WITHyEXPLAINmuestran sus filas, con el número de filas y el tiempo que tardaron. Se muestran las primeras 200 filas, con Show all para el resto.INSERTinforma del id de la fila nueva.- Cualquier otra sentencia informa del número de filas modificadas.
History guarda las diez últimas sentencias de la sesión, para que puedas recuperar una en lugar de volver a escribirla.
El menú de la base de datos tiene Delete database, que elimina el archivo.
Crea tus tablas desde tu app, con
create table if not existsal arrancar. Usa la pestaña SQL para consultar y reparar datos, no para crear el esquema del que depende tu app: si no, la app no funcionará en un dispositivo en el que no hayas ejecutado esas sentencias a mano.
7. Settings
Los cambios se aplican en cuanto los haces; no hay nada que guardar.
Appearance
- Mode: System, Light o Dark (sistema, claro u oscuro). Se aplica a los dos temas que pueden mostrarse de ambas formas: aCelery y Default. Un tema de Bootswatch es claro u oscuro, y si eliges uno de ellos el control Mode te lo indica.
- Theme: hay 18: aCelery, Default, Cerulean, Cosmo, Cyborg, Darkly, Flatly, Journal, Lumen, Paper, Readable, Sandstone, Simplex, Slate, Spacelab, Superhero, United y Yeti. Cyborg, Darkly, Slate y Superhero son oscuros. El tema se aplica tanto a tus propias apps como a las pantallas de aCelery.
- Editor colours: el esquema de colores propio del editor de código. Si lo dejas en Light or dark, to match the app (claro u oscuro, según la app), sigue el Mode; si eliges una paleta, se mantiene esa paleta haga lo que haga el Mode.
This device
(No se muestra en un navegador en la red: son los ajustes del propio dispositivo.)
- Network access: abre el panel para compartir. Consulta la sección siguiente.
- Back up data: guarda tus bases de datos, los archivos que han escrito tus
apps y las propias apps en un único zip,
aCelery_backup_<fecha>.zip, y abre el menú de compartir para que puedas guardarlo en Archivos, en Drive, en un correo o donde quieras. Consulta Copia de seguridad y restauración. - Restore data: vuelve a cargar uno de esos zips. Primero te lo confirma y después te deja elegir el archivo.
- Keep screen on: impide que el dispositivo entre en reposo mientras aCelery está abierto. Útil mientras lees código en pantalla, e imprescindible en iPhone y iPad si quieres que el servidor siga accesible.
About
La versión y la licencia, la dirección en la que sirve aCelery y un enlace a www.acelery.com.
8. Ejecutar una app
Al ejecutar una app, se abre a pantalla completa por encima de aCelery, con su propia barra de título. El menú de esa barra tiene:
- Reload: reinicia la app con los archivos tal como están ahora. Después de una edición, es el ciclo más rápido.
- Error log: todo lo que la app ha registrado y todos los errores que no ha capturado. Disponible en una ejecución de depuración, que es lo que hace Run desde Code.
- Add to home screen (Android).
- Close: vuelve a aCelery. El botón Atrás del dispositivo hace lo mismo.
Si la app no arranca (un error de sintaxis, un módulo de entrada que falta, una
excepción dentro de main), aCelery muestra el fallo y su traza en lugar de la
app, indicando el archivo y la línea. Nada se empaqueta, así que esa línea es tu
línea.
9. Compartir en tu red
Settings → Network access, o el mismo panel desde cualquier otro sitio que lo ofrezca.
Por defecto, aCelery solo se escucha a sí mismo. Al activar Share on this
network (compartir en esta red) se vuelve accesible desde otros dispositivos de
la misma Wi-Fi, en la dirección que muestra entonces el panel, algo como
http://192.168.1.24:8123. Ábrela en un navegador de tu ordenador y tendrás
aCelery: las mismas pantallas, el mismo editor, tus apps.
Emparejamiento
Un dispositivo que nunca se ha conectado no puede entrar. Ve una página que le pide solicitar acceso y le muestra un código corto; en el móvil aparece un aviso con la dirección de ese dispositivo y el mismo código, y tú lo apruebas o lo rechazas. Los códigos sirven para distinguir dos solicitudes simultáneas: aprueba la que muestre el mismo código que tu navegador.
Al aprobarlo, ese dispositivo recibe una clave, para que no tenga que volver a pedir acceso. La dirección por sí sola nunca es la credencial: las direcciones se reasignan, y cualquier cosa en la red puede hacerse pasar por una.
Approved devices enumera todo lo que tiene una clave en este momento y cuándo hizo su última llamada, y ofrece una × en cada uno para revocarla. Revoke all las elimina todas.
Qué tener en cuenta
- Aprobar un dispositivo le da acceso a tus bases de datos y archivos. Aprueba dispositivos que sean tuyos, en redes de confianza.
- El tráfico es HTTP sin cifrar en tu red local: sin túnel y sin cifrado. Es una función para la red local, no una forma de publicar una app en internet.
- En Android, compartir sigue funcionando con la pantalla apagada: mientras el interruptor está activado, aCelery ejecuta un servicio en primer plano y muestra una notificación con un botón Stop sharing (dejar de compartir).
- En iPhone y iPad, iOS suspende la app pocos segundos después de que salga de la pantalla, y no ofrece nada equivalente. Compartir funciona mientras aCelery está abierto en pantalla; activa Keep screen on para sesiones más largas.
10. Conectar un asistente de IA
aCelery puede ser controlado por un asistente de IA que se ejecute en tu ordenador: Claude Code, Claude Desktop o cualquier otro que hable MCP. El asistente escribe archivos en tus proyectos, los ejecuta en el móvil, lee la consola y la página renderizada, y corrige lo que encuentra.
Settings → Network access → Connect an assistant. Pon a la clave el nombre del lugar donde se va a usar («Claude en mi portátil»), y aCelery te muestra, una sola vez:
- la dirección a la que conectarse,
http://<móvil>:8123/mcp; - la clave;
- una línea de comandos lista para Claude Code, para pegarla en un terminal;
- un bloque de configuración listo para Claude Desktop.
Activa antes Share on this network, o el ordenador no podrá llegar al móvil en absoluto.
El asistente obtiene un conjunto de herramientas a través de esa conexión: listar, leer, crear y borrar apps y sus archivos; listar bases de datos, consultarlas y ejecutar sentencias sobre ellas; y ejecutar una app en el dispositivo, leer su consola, inspeccionar lo que ha renderizado, evaluar JavaScript en ella, hacer una captura de pantalla (Android) y cerrarla. También obtiene una guía escrita de la API de aCelery, con el mismo contenido que la Parte 2 de este documento.
Ten en cuenta
- La clave se muestra una sola vez. Si la pierdes, revócala y crea otra.
- Cualquiera que tenga la clave puede leer y modificar tus apps y bases de datos, y la clave viaja sin cifrar por tu Wi-Fi. Úsala en una red de confianza y revócala en Approved devices cuando termines.
- Estar emparejado como navegador no convierte a un dispositivo en asistente, y ser asistente no depende del emparejamiento del navegador. Son claves distintas.
- En iOS la conexión se corta cada vez que aCelery sale de la pantalla.
11. Dónde se guarda tu trabajo
Todo vive en una única carpeta aCelery dentro del almacenamiento privado de la
app: no hacen falta permisos, y nada más en el dispositivo puede leerla.
aCelery/
www/user/<App>/ tus apps: una carpeta cada una
db/ tus bases de datos SQLite
files/ lo que tus apps escriben con acelery/file.js
log/ registros de errores
cache/ espacio temporal (zips exportados)
Desinstalar aCelery lo borra todo. Si te importa, haz antes una copia de seguridad (ver más abajo).
Una actualización de la app nunca sobrescribe esos cuatro directorios, así que tus proyectos, bases de datos y archivos la sobreviven.
Copia de seguridad y restauración
Settings → Back up data comprime db/, files/ y www/user/ (todo lo que
has creado) en un único aCelery_backup_<fecha>.zip y lo ofrece al menú de
compartir. Los registros y los archivos temporales se quedan fuera. Guarda el
zip en algún sitio que no sea el dispositivo, sobre todo antes de desinstalar
aCelery o de cambiar de móvil.
Settings → Restore data vuelve a cargarlo. Elige el zip y aCelery copia en su sitio sus bases de datos, archivos y apps, y después recarga. Todo lo que esté en la copia sustituye al elemento del dispositivo con el mismo nombre; lo que la copia no menciona se deja tal cual. Un zip que no sea una copia de seguridad de aCelery se rechaza.
Estas dos opciones son para todo a la vez. Para mover una sola app entre dispositivos, usa en su lugar Export project e Import project en Code.
Haz la copia cuando ninguna app esté escribiendo en una base de datos: la copia es una instantánea de los archivos tal como están en ese momento.
Parte 2 — Escribir apps de aCelery
12. Una app, de principio a fin
Aquí tienes una app completa que funciona. Crea un proyecto llamado Notes,
pon esto en main.js y pulsa Run.
import { openDB } from "acelery/sql.js";
import {
html, render, useState, useEffect,
Panel, Form, Input, Button, ListGroup, notEmpty,
} from "acelery/ui.js";
function Notes({ db }) {
const [notes, setNotes] = useState([]);
const load = async () =>
setNotes(await db.select("select rowid, body from note order by rowid desc"));
useEffect(() => { load(); }, []);
async function add(values) {
await db.insert("insert into note (body) values (?)", [values.body]);
await load();
}
return html`
<${Panel} title="Notes">
<${Form} initial=${{ body: "" }} onSubmit=${add}>
<${Input} label="Note" name="body" validate=${[notEmpty()]} />
<${Button} type="submit" variant="primary">Add<//>
<//>
<${ListGroup} variant="flush">
${notes.map((n) => html`<${ListGroup.Item} key=${n.rowid}>${n.body}<//>`)}
<//>
<//>`;
}
export default async function main() {
const db = await openDB("notes.db");
await db.exec("create table if not exists note (body text)");
render(html`<${Notes} db=${db} />`, document.body);
}
Esa es toda la forma de una app de aCelery: dos importaciones por nombre, un
árbol de componentes y un main exportado. Todo lo que sigue son detalles.
13. Anatomía de una app
www/user/<App>/
acelery_app.json el manifiesto
main.js el módulo de entrada (el manifiesto puede indicar otro)
*.css opcional; se cargan todos los .css de la carpeta
... cualquier otra cosa: más módulos, imágenes, datos
El manifiesto
{
"name": "Water",
"description": "Logs how much I drink",
"entry": "main.js",
"icon": "icon.png"
}
| Clave | Significado |
|---|---|
name |
Se muestra en la tarjeta. Letras, dígitos y guion bajo, 16 como máximo |
description |
Una línea, que se muestra bajo el nombre |
entry |
El módulo que se ejecuta. main.js si no se indica |
icon |
Una imagen dentro de la carpeta del proyecto. Si no hay, se dibuja un monograma |
La ruta de icon debe quedarse dentro del proyecto: cualquier ruta que salga de
él, o que apunte a otro origen, se ignora.
El módulo de entrada
aCelery importa el módulo de entrada y llama a su exportación por defecto:
export default function main() { … }
main puede ser async, y aCelery espera a que termine. Como alternativa se
acepta una exportación con nombre main, o un main global, así que exportar
lo que no toca te da tu app en lugar de una página en blanco.
La página es tuya: renderiza en document.body. Los demás módulos de la carpeta
se importan por ruta relativa: import { total } from "./sums.js";.
Hojas de estilo
Todos los archivos .css de la carpeta del proyecto se cargan automáticamente,
en el orden en que los lista la carpeta. No hace falta enlazarlos.
Importaciones
Los módulos se importan con nombres acelery/ sin ruta, resueltos mediante un
import map:
| Importación | Qué te da |
|---|---|
acelery/ui.js |
renderizado, hooks, componentes, formularios, temas |
acelery/sql.js |
SQLite |
acelery/file.js |
archivos dentro de la carpeta de aCelery |
acelery/picker.js |
elegir archivos y fotos; recortar y reducir imágenes |
acelery/http.js |
HTTP saliente a través del dispositivo |
acelery/export.js |
compartir un archivo, salir de la app |
acelery/chart.js |
gráficos; una importación aparte porque Chart.js es grande |
acelery/datatable.js |
tablas ordenables, con búsqueda y paginadas; también aparte |
acelery/editor.js |
el editor de código, si tu app quiere uno |
No hay nada más que importar. Ni npm ni CDN: es muy posible que el dispositivo esté sin conexión, y no hay ningún paso de instalación. Todo lo de la tabla anterior ya está en el dispositivo.
14. Renderizado: Preact y htm
La interfaz es Preact (la API de React en 4 KB) con plantillas htm en lugar de JSX. htm compila sus plantillas en tiempo de ejecución, que es precisamente por lo que una app de aCelery no necesita paso de compilación.
Si conoces JSX, ya conoces esto, con cuatro diferencias:
html`
<${Panel} title="Directory">
<${Button} variant="primary" onClick=${save}>Save<//>
<div class="mt-3" ...${rest}>
${items.map((i) => html`<p key=${i.id}>${i.name}</p>`)}
</div>
<//>`
- los componentes se interpolan:
<${Button}> <//>cierra el componente que esté abierto; no repites su nombre- los atributos admiten expresiones:
onClick=${save},rows=${3},disabled=${busy} - en los elementos normales funcionan tanto
classcomoclassName
El estado se maneja con hooks, importados del mismo módulo: useState,
useEffect, useMemo, useRef, useCallback, useContext, useReducer,
useLayoutEffect. También render, createRef, Fragment y createContext.
Cambia el estado para pasar de una pantalla a otra. No vacíes y reconstruyas el DOM a mano: la gracia de un árbol de componentes es que redibujar una lista no destruye la posición de desplazamiento, el foco ni el texto a medio escribir que hay dentro.
15. acelery/ui.js — la capa de componentes
Una sola importación te da todo lo que sigue.
Maquetación
| Componente | Notas |
|---|---|
Container |
El contenedor de Bootstrap |
Row |
Una fila de la cuadrícula; los hijos pasan a la línea siguiente en lugar de desbordarse |
Col span=${6} |
span es el ancho sobre 12 en tablet o mayor; en un móvil todas las columnas ocupan el ancho completo |
Panel title="…" footer=${…} |
Una tarjeta con cabecera. title y footer son opcionales |
Card |
La tarjeta de Bootstrap, si quieres construir las partes tú |
Formularios
Form guarda los valores y los valida. onSubmit recibe todos los campos en un
único objeto, y solo se dispara cuando todos los campos son válidos.
<${Form} initial=${{ name: "", grp: "work", active: true }} onSubmit=${save}>
<${Input} label="Name" name="name" validate=${[notEmpty()]} />
<${Input} label="Email" name="email" type="email" validate=${[email()]} />
<${Select} label="Group" name="grp" options=${["work", "home"]} />
<${TextArea} label="Notes" name="notes" rows=${3} />
<${CheckBox} label="Active" name="active" />
<${Button} type="submit" variant="primary">Save<//>
<//>
| Componente | Props |
|---|---|
Form |
initial, onSubmit |
Input |
label, name, type, validate, help, placeholder, id, más cualquier atributo de <input> |
TextArea |
como Input, más rows (4 por defecto) |
Select |
como Input, más options y placeholder |
CheckBox |
label, name |
InputGroup |
El input group de Bootstrap, para un control con texto o un botón adosado |
Las opciones de Select admiten cadenas simples u objetos { label, value }.
Cada campo gestiona su propio emparejamiento <label for>/id: nunca escribes
un id a menos que quieras uno. El error de un campo desaparece en cuanto se
edita y se vuelve a comprobar al enviar.
Los validadores son notEmpty(), notZero(), email(), tel() y
maxLength(n), y cada uno admite un mensaje opcional:
validate=${[notEmpty("Ponle un nombre"), maxLength(40)]}
Un validador propio es simplemente una función que devuelve true cuando el
valor es correcto y un mensaje cuando no lo es:
const inThePast = (v) => new Date(v) <= new Date() ? true : "No puede ser futura";
useForm() lee el formulario desde un componente que esté dentro de él:
{ values, errors, setValue, submitted }.
Componentes de Bootstrap
Reexportados desde react-bootstrap para que te baste con una importación. Sus props son las de react-bootstrap.
Alert · Badge · Button · ButtonGroup · Card · Container ·
Dropdown · DropdownButton · Image · InputGroup · ListGroup · Modal ·
Nav · NavDropdown · Navbar · Offcanvas · Pagination · Placeholder ·
ProgressBar · Spinner · Tab · Table · Tabs · Toast ·
ToastContainer
Los propios de aCelery
| Exportación | Qué hace |
|---|---|
TableMaint |
Una pantalla CRUD completa sobre una tabla; consulta la sección siguiente |
useTableOptions(db, table, column) |
Opciones para un campo list, leídas de otra tabla |
FileButton |
Un botón que abre el selector de archivos o de fotos |
ImageCropper |
Un diálogo para arrastrar y pellizcar un marco sobre una imagen |
useDismiss(ref, onDismiss, active) |
Cierra un panel cuando el usuario toca fuera de él o pulsa Escape |
ThemeSelect |
Un selector de tema listo para usar |
applyTheme, currentTheme, currentMode, themeHasModes, isDark, THEMES, MODES |
Temas, a mano |
FileButton es la forma correcta de ofrecer un selector:
<${FileButton} accept="image/*" capture="environment"
onFiles=${([photo]) => setPhoto(photo)}>Hacer una foto<//>
Props: onFiles (se llama con un File[]; no se llama si se cancela),
accept, multiple, capture ("environment" para la cámara trasera,
"user" para la frontal), variant, size, disabled.
ImageCropper está abierto mientras su prop image tenga valor:
<${ImageCropper} image=${picked} shape="round" aspect=${1} maxSide=${800}
onDone=${(blob) => save(blob)} onCancel=${() => setPicked(null)} />
Props: image (un File, un Blob o una URL), shape ("rect" o
"round"), aspect, maxSide, type, quality, title, confirmLabel,
onDone, onCancel. Un marco redondo sigue dándote una imagen cuadrada;
muéstrala redonda con CSS.
16. TableMaint — una pantalla CRUD a partir de una declaración
Esta es la función más característica de aCelery. Declara los campos de una tabla y obtienes una lista, una vista de registro, un editor, búsqueda y paginación que funcionan sobre ella: las mismas pantallas que usa la pestaña Data.
const FIELDS = [
{ type: "string", title: "Name", name: "mname", validate: [notEmpty()] },
{ type: "email", title: "Email", name: "email", validate: [email()] },
{ type: "list", title: "Group", name: "grp", options: ["family", "work"] },
];
<${TableMaint} db=${db} title="Directory" table="person" fields=${FIELDS} />
La tabla ya debe existir. Créala antes de renderizar; consulta
acelery/sql.js.
Las vistas
- Lista: las filas, con New, First, Prev., Next, Last y Search.
- Registro: una fila, con Edit, Delete, Ok y las acciones especiales que hayas añadido.
- Edición / alta: los campos como controles de entrada, con Save y Cancel.
- Búsqueda: marca los campos por los que buscar y rellena un valor o un intervalo.
La paginación recorre los id de fila en lugar de contar desplazamientos, así que
Next y Prev siguen siendo baratos en una tabla de cualquier tamaño. pageSize
es 20 por defecto.
La búsqueda compara el texto por prefijo, los números y las fechas por
igualdad y, en number, money y date, por intervalo cuando rellenas
los dos extremos.
Declaración de campos
| Clave | Significado |
|---|---|
type |
Ver más abajo. "string" por defecto |
title |
El encabezado de la columna y la etiqueta del control |
name |
La columna de la tabla |
validate |
Un array de validadores, como en un Form |
inList |
Mostrar este campo en la lista. true por defecto |
inSearch |
Ofrecer este campo en la búsqueda. true por defecto |
readOnly |
Mostrarlo, pero no permitir editarlo. false por defecto |
options |
Para list: cadenas u objetos { label, value } |
rows |
Para textarea. 4 por defecto |
onValue / offValue |
Para checkbox: qué se guarda. 1 y 0 por defecto |
Tipos: string, email, tel, number, money, date, textarea,
checkbox, list.
date usa el selector de fecha de la propia plataforma: áreas táctiles más
grandes, el formato regional correcto y accesibilidad sin esfuerzo. money se
muestra con separadores de miles y dos decimales, y se alinea a la derecha con
el resto de números.
Tablas hijas
linked renderiza un TableMaint hijo dentro de cada registro, que muestra
solo las filas que le pertenecen y rellena la columna de enlace al guardar:
<${TableMaint} db=${db} title="Directory" table="person" fields=${PERSON_FIELDS}
linked=${[
{ title: "Phone numbers", table: "person_tel", on: "person",
fields: PHONE_FIELDS },
]} />
on es la columna del hijo que guarda el id de fila del padre. Esa columna se
oculta en las vistas del propio hijo: el registro que estás viendo ya indica qué
valor tendría.
Hooks
Cada uno puede ser async, y todos son opcionales.
| Prop | Cuándo se ejecuta |
|---|---|
preNew(values) |
Antes de una inserción; puede modificar values directamente |
postNew(rowid, values) |
Después de una inserción |
preEdit(id, values) |
Antes de una actualización |
postEdit(id, values) |
Después de una actualización |
preDelete(id) |
Antes de un borrado |
postDelete(id) |
Después de un borrado |
validateForm(values, id) |
Devuelve false para impedir el guardado. id es null en un registro nuevo |
specialActions(id) |
Devuelve [{ label, bind }] para añadir elementos al menú Special de la vista de registro |
onError(e) |
Cualquier cosa que haya fallado; el aviso lo muestra de todos modos |
Opciones desde otra tabla
const groups = useTableOptions(db, "grp", "name");
// …
{ type: "list", title: "Group", name: "grp", options: groups }
Lee rowid y la columna indicada, y te da pares { label, value } con el id de
fila como valor.
17. acelery/sql.js — SQLite
import { openDB, deleteDB } from "acelery/sql.js";
const db = await openDB("water.db"); // db/water.db, se crea si no existe
await db.exec("create table if not exists drink (at text, ml integer)");
const id = await db.insert("insert into drink values (?, ?)",
[new Date().toISOString(), 250]);
const rows = await db.select("select * from drink where ml > ?", [100]);
const one = await db.selectOne("select sum(ml) as total from drink");
const n = await db.exec("delete from drink where ml < ?", [50]);
| Llamada | Se resuelve con |
|---|---|
openDB(path, basePath?) |
un Database. El archivo se crea si no existe |
deleteDB(path, basePath?) |
— |
db.select(sql, args?) |
todas las filas, como un array de objetos |
db.selectOne(sql, args?) |
la primera fila, o null |
db.exec(sql, args?) |
el número de filas modificadas |
db.insert(sql, args?) |
el id de la fila nueva |
db.close() |
— |
Todo es una promesa, así que la interfaz no se congela mientras se ejecuta una
sentencia. Las columnas conservan sus tipos de SQLite: un INTEGER llega como
número y NULL llega como null. Una sentencia fallida se rechaza con el propio
mensaje de SQLite.
Reglas que conviene seguir
- Pasa siempre los valores como parámetros
?. No construyas nunca SQL concatenando cadenas: un nombre con un apóstrofo romperá la sentencia, y será peor si el valor vino de fuera de tu app. - Crea tus tablas cuando arranque la app, con
create table if not exists. La app tiene que funcionar en un dispositivo en el que su base de datos todavía no existe. - Abre la base de datos una sola vez y pásala hacia abajo. Abrirla en cada pantalla significa varios manejadores compitiendo por el mismo bloqueo.
18. acelery/file.js — archivos
Las rutas son relativas al directorio files/ de aCelery. Cualquier ruta que se
resuelva fuera de la carpeta de aCelery se rechaza: un .. que intente salir
lanza una excepción en lugar de leer donde no debe.
import * as file from "acelery/file.js";
const f = await file.open("Garden/notes.txt");
await f.write("planted the roses\n", true); // añadir al final
const text = await f.read();
await f.close();
await file.mkdir("Garden/photos");
const entries = await file.listFiles("Garden");
await file.writeBytes("Garden/rose.jpg", blob);
const src = file.url("Garden/rose.jpg"); // para <img src>
| Llamada | Notas |
|---|---|
open(path, basePath?) |
Un FileHandle; el archivo se crea si no existe |
handle.read() |
El archivo completo, como texto |
handle.write(text, append?) |
Vacía el archivo antes, salvo que append sea true |
handle.delete() |
|
handle.close() |
|
listFiles(path, basePath?) |
[{ path, fname, directory, lastmodified, length }] |
mkdir(path, basePath?) |
|
writeBytes(path, data, basePath?) |
Un Blob, File, ArrayBuffer o Uint8Array. Las carpetas se crean según haga falta. Se resuelve con el tamaño escrito |
url(path, basePath?) |
Una dirección para <img src>, un enlace o fetch |
externalStoragePath() |
La raíz que aCelery aceptará como basePath |
Las rutas de texto no pueden transportar datos binarios: usa writeBytes para
imágenes y cualquier otra cosa que no sea texto. Una sola subida está limitada a
25 MB.
Guarda los archivos de cada app en una carpeta con su nombre
(Garden/photos/12.jpg) y guarda esa ruta en la base de datos, no los bytes.
19. acelery/picker.js — archivos y fotos que elige el usuario
Todos los selectores de este módulo son el propio campo de archivo de la página,
así que el archivo viene de donde esté el usuario: la galería, la cámara o los
documentos del móvil, o el disco del otro ordenador cuando la app está abierta
en un navegador en la red. En ambos casos recibes un File.
import { pickFiles, pickImages, shrinkImage, cropImage } from "acelery/picker.js";
| Llamada | Notas |
|---|---|
pickFiles({ accept?, multiple?, capture? }) |
File[], vacío si el usuario cancela |
pickImages({ multiple?, camera? }) |
El selector de fotos o, con camera: true, la cámara |
shrinkImage(blob, { maxSide = 1600, type = "image/jpeg", quality = 0.85 }) |
La imagen completa, reducida lo suficiente para guardarla |
cropImage(blob, area, { maxSide = 1024, type, quality }) |
Recorta area y la escala. area es {x, y, width, height} en los píxeles de la propia imagen, tal como lo devuelve ImageCropper, o null para la imagen completa |
Llama a los selectores desde un toque. Los navegadores solo abren un selector
de archivos en respuesta a un clic, nunca desde main ni desde un efecto.
Prefiere FileButton cuando el selector se abre desde un botón. Estas
funciones hacen clic en un campo desde código, y entonces WebKit en iOS coloca
su menú Fototeca / Hacer foto / Elegir archivo en la esquina superior izquierda
de la página en lugar de junto a tu botón.
Reduce antes de guardar. Una foto de la cámara de un móvil ocupa entre 3 y
12 MB; con el maxSide por defecto se queda en unos cientos de KB.
Elige tú el nombre con el que se guarda. El nombre del archivo elegido viene del dispositivo del usuario y puede ser cualquier cosa.
const [photo] = await pickImages({ camera: true });
await file.writeBytes(`Garden/${Date.now()}.jpg`, await shrinkImage(photo));
20. acelery/http.js — la red
import * as http from "acelery/http.js";
const body = await http.get("https://example.com/rates.json");
const data = await http.getJson("https://example.com/rates.json");
const reply = await http.post("https://example.com/log", "a=1&b=2");
La petición la hace el dispositivo, no la página, así que el origen de la página
no la restringe y no hay CORS que satisfacer. post envía un cuerpo codificado
como formulario.
Una petición fallida se resuelve con una cadena vacía en lugar de rechazarse: es el comportamiento que aCelery ha tenido siempre. Compruébalo:
getJsonsobre un cuerpo vacío lanzará un error de análisis, que rara vez es el mensaje que quieres mostrar.
21. acelery/export.js — compartir archivos y salir de la app
import { saveFile, exportProject, importProject, runApp, closeApp }
from "acelery/export.js";
await saveFile("text/csv", "directory.csv", csv);
closeApp();
| Llamada | Qué hace |
|---|---|
saveFile(mime, filename, text) |
Entrega un archivo al usuario: el menú de compartir en el dispositivo, una descarga en un navegador |
exportProject(name) |
Comprime uno de tus proyectos en un zip y lo ofrece |
importProject() |
Abre el selector de archivos del dispositivo para importar un zip de proyecto |
runApp(title, app, debug?) |
Abre una de tus apps |
closeApp() |
Sale de la app en ejecución y vuelve a aCelery |
Son las únicas llamadas que piden al dispositivo que haga algo en lugar de
devolver datos, así que se lanzan y se olvidan: no hay nada que esperar salvo
saveFile, que primero prepara el archivo.
En un navegador en la red siguen teniendo sentido: saveFile y exportProject
se convierten en descargas, runApp abre la app en una pestaña nueva y
closeApp vuelve atrás. Solo importProject no tiene equivalente remoto
(necesita el selector de archivos del propio dispositivo) y lanza una excepción
si lo llamas ahí.
22. acelery/chart.js — gráficos
Una importación aparte, porque Chart.js ocupa 68 KB comprimido con gzip y la mayoría de las apps nunca dibujan un gráfico. Una app solo lo paga si lo pide.
import { Chart, fromRows } from "acelery/chart.js";
const rows = await db.select(
"select grp, count(*) as people from person group by grp order by people desc");
<${Chart} type="bar" height=${260} data=${fromRows(rows, "grp", "people")} />
fromRows(rows, labelColumn, valueColumns) es el paso entre un conjunto de
resultados y una configuración de Chart.js: agrupa en SQL y representa las
filas. valueColumns puede ser un array para tener varias series.
Props de Chart: type (bar, line, pie o doughnut), data, options
(se combinan sobre los valores por defecto), height (300 por defecto),
title, className.
Al cambiar los datos, el gráfico se actualiza en su sitio, así que una actualización se anima desde donde estaba en lugar de parpadear.
23. acelery/datatable.js — tablas ordenables y con búsqueda
DataTables 3, como componente. Es una importación
aparte, como los gráficos, porque ocupa 55 KB comprimido con gzip y la mayoría de
las apps nunca lo necesitan. La hoja de estilos viene incluida: no hay ningún
<link> que añadir.
import { DataTable, DataTables, sqlSource } from "acelery/datatable.js";
// Una tabla de cualquier tamaño: cada página, orden y búsqueda es una consulta.
<${DataTable} source=${sqlSource(db, "person")}
columns=${[
{ data: "mname", title: "Name" },
{ data: "email", title: "Email" },
{ data: "salary", title: "Salary", className: "text-end",
render: DataTables.render.number(",", ".", 2) },
]}
onRowClick=${(row) => openPerson(row.rowid)} />
// Filas que ya tienes: se ordenan, se buscan y se paginan en la página.
<${DataTable} rows=${rows} columns=${[{ data: "name", title: "Name" }]} />
columns usa el vocabulario propio de DataTables (data, title, render,
className, orderable, searchable, visible), así que su documentación se
aplica tal cual.
sqlSource(db, from) responde a DataTables desde SQLite página a página,
así que una tabla de 50.000 filas nunca cruza el puente completa. from es una
de estas opciones:
- un nombre de tabla:
sqlSource(db, "person"); { table, where, args }:sqlSource(db, { table: "person", where: "grp = ?", args: [g] });{ query, where, args }: cualquierselect, joins incluidos. Sus columnas son las que la tabla ordena y busca.argsrellena los?dequeryy después los dewhere.
Las filas de una tabla llevan su rowid, que suele ser lo que necesita
onRowClick. Para una vista o una tabla WITHOUT ROWID, pasa rowid: false.
La búsqueda funciona como la del propio DataTables: cada palabra tiene que
aparecer en alguna columna de la fila, y "una frase entre comillas" cuenta
como una sola palabra. Con una fuente SQL, solo se pueden ordenar o buscar las
columnas cuyo data sea un nombre de columna simple. Una columna calculada
(data: null con un render) se sigue mostrando, pero su encabezado no ofrece
ordenar.
Props de DataTable: columns, rows o source, onRowClick(row, event)
(Intro también funciona), refresh (cámbialo para volver a consultar la
fuente, por ejemplo después de una inserción), onError, onInit(api) para la
API de DataTables, options (cualquier opción de DataTables; se lee al
construir la tabla), striped, hover, small y className.
Las celdas son texto. Un valor que contiene <b> muestra los caracteres
<b>. Una columna que deba mostrar marcado tiene que aportar su propio
render, y escapar lo que inserte pasa a ser responsabilidad de esa función.
No hay jQuery. La mayoría de los ejemplos de DataTables en internet
escriben $("#table").DataTable({...}). Eso aquí no funciona, porque
DataTables 3 no necesita jQuery y aCelery no lo incluye. Usa el componente y
pasa las mismas opciones a través de options.
Los errores, como un nombre de tabla mal escrito, se muestran como un aviso rojo encima de la tabla en lugar de un diálogo. En pantallas estrechas, las columnas que no caben se pliegan en una vista de detalles por fila en lugar de desplazarse en horizontal.
24. Estilos y temas de tu app
Tu app se muestra dentro del tema que haya elegido el usuario, que puede ser cualquiera de los 18 y puede ser oscuro.
Da estilo con las clases y variables CSS de Bootstrap 5
(text-body-secondary, bg-body-tertiary, border, var(--bs-primary)) y
nunca con colores fijos. Un #333 fijo es invisible en Cyborg, y un texto negro
sobre un panel blanco fijo es justo lo que se verá roto en modo oscuro.
- Todos los archivos
.cssde la carpeta del proyecto se cargan automáticamente. ThemeSelectofrece al usuario un selector de tema dentro de tu app;applyTheme(name)aplica uno desde código.isDark()te dice si el tema actual es oscuro, y el documento lanza un eventoacelery:themechangecuando cambia.
Diseña primero para el móvil: una columna, áreas táctiles grandes y nada que
dependa de pasar el ratón por encima. Col span se ocupa de la pantalla ancha.
Iconos
aCelery incluye un subconjunto de 34 glifos de Font Awesome 6, que se dibujan con
<i class="fa-solid fa-play"></i>. Un icono que no esté en el subconjunto no se
muestra en absoluto, así que compruébalo antes de contar con él. El subconjunto
es:
arrow-left · arrow-up-right-from-square · circle-half-stroke ·
circle-info · clock-rotate-left · code · database ·
ellipsis-vertical · file · file-code · file-export · file-image ·
file-import · file-lines · floppy-disk · folder · gear · house ·
magnifying-glass · mobile-screen · moon · pen-to-square · play ·
plus · seedling · sun · table · table-cells · table-columns ·
terminal · trash · triangle-exclamation · wifi · xmark
Para cualquier otra cosa, usa un emoji, un SVG propio o una imagen en la carpeta de tu proyecto.
25. Depuración
En el dispositivo
Si el módulo no se puede analizar, no exporta ninguna función de entrada o
main lanza una excepción, aCelery sustituye la app por el error, su traza y el
archivo y la línea donde ocurrió. Nada se empaqueta ni se minifica, así que esa
es tu línea.
Mientras la app se ejecuta, Error log en su menú muestra todo lo que ha registrado, todos los errores que nadie capturó y todas las promesas que nadie gestionó. Está en el menú en una ejecución de depuración, que es lo que hace Run desde Code.
console.log funciona como en cualquier sitio. Úsalo en lugar de alert, que
bloquea la página.
Desde un navegador en tu ordenador
Con Share on this network activado, abre http://<móvil>:8123/ en tu
ordenador y aprueba el emparejamiento. Tendrás tu app en un navegador de
escritorio, con DevTools: puntos de interrupción, el inspector de elementos, el
panel de red y una consola de verdad.
Tu app se sirve en /system/launcher.html?app=<App>.
Es la forma más rápida de trabajar en la maquetación y la lógica de una pantalla. Vuelve al dispositivo para todo lo que tenga que ver con la cámara, el menú de compartir o cómo se siente bajo el pulgar.
El ciclo de trabajo
- Edita en Code y pulsa Run.
- Mira la pantalla; si ha fallado, lee la traza.
- Usa Reload en el menú de la app después de cada edición: siempre carga los archivos tal como están en ese momento.
Trabaja en pasos pequeños: un cambio, una ejecución, un vistazo.
26. Límites, reglas y trampas habituales
Nombres
- Nombres de app: letras, dígitos y guion bajo, 16 caracteres como máximo.
- Nombres de base de datos: letras, dígitos, guion y guion bajo.
- Los nombres de tabla y de columna en
TableMaintdeben ser identificadores SQL normales: no se pueden pasar como parámetros, así que en su lugar se comprueban.
Tamaños
- Una subida con
writeBytes: 25 MB. - El bloc de SQL muestra las primeras 200 filas, con Show all para el resto.
Lo que suele pillar desprevenido
| Un selector que no hace nada | Lo has llamado fuera de un manejador de clic. Los navegadores solo abren un selector en respuesta a un toque |
| Un icono que no se muestra | No está en el subconjunto de 34 glifos |
getJson lanza un error de análisis |
La petición falló; http.get se resuelve con "" en lugar de rechazarse |
TableMaint da error al cargar |
La tabla todavía no existe. Créala antes de renderizar |
| Una página en blanco, sin error | El módulo de entrada exportó lo que no tocaba. Debe ser la exportación por defecto |
| Texto invisible en algunos temas | Un color fijo. Usa las clases y variables de Bootstrap |
| Tu app se ve bien, pero el móvil se cuelga | Algo llamó a alert, confirm o prompt dentro de un bucle. Usa Modal en su lugar |
| El asistente pierde la conexión (iOS) | aCelery salió de la pantalla. iOS lo suspende; mantenlo abierto |
Reglas que se aplican, no simples recomendaciones
- Las rutas de archivos y bases de datos no pueden salir de la carpeta de aCelery.
- Un dispositivo en la red no ve nada hasta que lo apruebas.
importProjectnecesita la app; no tiene equivalente en un navegador remoto.
27. Solución de problemas
La app no arranca y el error indica una línea. Abre ese archivo en Code en
esa línea. La causa habitual es un <//> de más, una plantilla sin cerrar o un
${ que falta antes del nombre de un componente.
No se muestra nada y no hay ningún error. Comprueba que el módulo de entrada
tiene export default y que realmente se llama a render(…, document.body). Un
main que devuelve un componente en lugar de renderizarlo no hace nada.
La app funciona, pero los datos están vacíos. Confirma que la tabla existe y
tiene filas: Data → tu base de datos → la tabla, o la pestaña SQL. Si falta la
tabla, tu create table if not exists no se está ejecutando, normalmente porque
está dentro de una rama que no se ejecutó, o porque la app abrió un archivo de
base de datos distinto del que crees.
Los cambios no surten efecto. Guarda el archivo (el punto junto a su nombre indica que hay cambios sin guardar) y después usa Reload en la app en ejecución en lugar de dejarla en pantalla.
Un ordenador no llega al móvil. Los dos deben estar en la misma Wi-Fi: un móvil con datos móviles no es accesible, y las redes de «invitados» suelen aislar a los dispositivos entre sí. Comprueba la dirección en Network access; cambia cuando el móvil vuelve a conectarse a una red.
La conexión se corta al cambiar de app. Es lo esperado en iPhone y iPad. En Android, compartir sigue funcionando con la pantalla apagada.
Has perdido la clave de un asistente. Revócala en Approved devices y crea otra; una clave solo se muestra una vez.
Has borrado algo por error. No se puede deshacer. Exporta los proyectos que te importen: el zip es la copia de seguridad, y se puede importar en cualquier dispositivo.
Colofón
aCelery fue escrita en 2014 por Xavier Llamas Rolland como app para Android, y reconstruida como app Flutter para Android e iOS. Es software libre bajo la GNU General Public License v3.0.
La app Example de tu dispositivo es la referencia práctica de todo lo que aparece en la Parte 2: ábrela en Code y léela de principio a fin.