CommunityProgramación y desarrollogithub.com

veritatron/scriptable-widget

Crea widgets listos para usar en la pantalla de inicio del iPhone para la app Scriptable a partir de una sola frase. Úsalo cuando alguien pida un widget de iPhone o un script de Scriptable.

¿Qué es scriptable-widget?

Esta habilidad permite a un agente de IA convertir una frase del usuario en un widget funcional de JavaScript para la aplicación Scriptable en iPhone, sin necesidad de que el usuario escriba código. El agente primero trabaja junto al usuario para determinar el contenido del widget y la fuente de datos, luego envía un único mensaje de seguimiento (si es necesario) para preguntar por el idioma y el estilo visual, ofreciendo cuatro estilos predefinidos: Clean, Minimal, Bold y Playful. Luego, escribe el script basándose en una plantilla sólida que maneja la obtención de datos (usando fuentes gratuitas como Open-Meteo para el clima o CoinGecko para criptomonedas, sin necesidad de claves API), el almacenamiento en caché para funcionar sin conexión, y el diseño responsivo para los tres tamaños de widget (pequeño, mediano y grande). La habilidad está documentada para soportar plataformas como Claude Code, Cursor y Codex, y está diseñada para agentes de IA que asisten a usuarios no técnicos. El script generado sigue reglas estrictas: usa solo APIs nativas de Scriptable, maneja errores de forma legible, soporta modo claro y oscuro, y evita servicios que requieran autenticación. El resultado final se presenta en el formato de respuesta especificado, con el código completo y las instrucciones para su instalación.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/veritatron/words2widget/tree/HEAD/skills/scriptable-widget

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Constructor de Widgets para Scriptable

Convierte una frase en un widget JavaScript listo para la aplicación Scriptable de iPhone. El usuario no programa, así que nunca le pidas que edite el script.

Flujo de trabajo

  1. Determina qué muestra el widget y de dónde provienen los datos.
  2. Envía un único mensaje corto de seguimiento antes de construir. Pregunta por el idioma y el estilo del widget (ofrece los estilos siguientes), además de cualquier otra cosa sin la cual no se pueda construir el widget, como el lugar para un widget del clima. Omite lo que el usuario ya haya indicado; si ya lo ha dicho todo, construye directamente. Nunca envíes un segundo seguimiento. Elige valores predeterminados sensatos para todo lo demás.
  3. Escribe el script adaptando la plantilla a continuación.
  4. Revisa la lista de verificación.
  5. Responde en el formato descrito en "Formato de respuesta".

Fuentes de datos

Nunca uses un servicio que requiera una clave API, cuenta o inicio de sesión. Los scripts nunca contienen secretos. Si la solicitud depende de dicho servicio, utiliza la fuente gratuita más cercana y menciónalo en una oración. Si no hay ninguna, di lo que no se puede hacer y ofrece el widget más similar.

Prefiere datos nativos, luego estas fuentes gratuitas:

DatosFuente
Climahttps://api.open-meteo.com/v1/forecast?latitude=..&longitude=..&current=temperature_2m,weather_code,is_day&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=auto
Calidad del aire, polenhttps://air-quality-api.open-meteo.com/v1/air-quality?latitude=..&longitude=..&current=european_aqi,birch_pollen
Coordenadas de un lugarhttps://geocoding-api.open-meteo.com/v1/search?name=..&count=1
Tipos de cambiohttps://api.frankfurter.dev/v1/latest?base=EUR&symbols=USD
Precios de criptomonedashttps://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur

Coloca las coordenadas de un lugar directamente en CONFIG. Usa Location.current() solo si el usuario pregunta por su ubicación actual.

Los datos nativos no necesitan red: CalendarEvent, Reminder, Device, fechas. Para estos, elimina las funciones de caché y loadData, y construye los datos directamente. Calendario, recordatorios y ubicación necesitan permiso, así que dile al usuario que ejecute el script una vez en la app Scriptable.

Estilos

Ofrece estos cuatro por nombre con su breve descripción. Si el usuario describe su propio estilo, síguelo. Coloca los colores en COLORS como Color.dynamic(new Color(light), new Color(dark)).

EstiloDescripciónFondoPrimarioSecundarioAcentoFuentes
CleanTranquilo y ordenado, tonos azules#EAF3FF / #14202E#10253F / #FFFFFF#52657C / #A9B8C9#E88A00 / #FFC24DSistema
MinimalSolo blanco y negro#FFFFFF / #000000#000000 / #FFFFFF#6E6E73 / #98989D#000000 / #FFFFFFSistema ligero, valores grandes
BoldColor fuerte que destaca#2F54EB / #1D39C4#FFFFFF / #FFFFFF#D6E0FF / #D6E0FF#FFD666 / #FFD666Sistema negrita
PlayfulCálido y amigable#FFF1E6 / #2B1B14#4A2511 / #FFF1E6#9A6B4F / #D4B8A6#FF6B4A / #FF9A76Sistema negrita redondeada

Reglas del script

  • Comienza desde la plantilla y mantén su estructura. El encabezado de Scriptable debe permanecer en la parte superior.
  • CONFIG debe tener un title corto como su primera entrada. Se convierte en el nombre sugerido del script. Todo lo que una persona pueda querer cambiar pertenece a CONFIG.
  • Mantén Script.setWidget(widget) y termina el script con Script.complete(); como última línea.
  • Establece refreshAfterDate. iOS decide cuándo se actualizan realmente los widgets, así que nunca prometas horas exactas. Usa al menos 15 minutos.
  • Muestra errores legibles a través de buildErrorWidget. Nunca dejes un widget en blanco.
  • Para datos de red, mantén la caché para que el widget aún funcione sin conexión.
  • Solo usa lo que proporciona Scriptable. No hay fetch, document, window, require, localStorage, import o export. Usa Request para la red y FileManager para archivos. Los paquetes npm no se pueden usar.
  • El código no debe contener triple comilla invertida.
  • Los widgets no pueden reaccionar a toques más allá de abrir una URL, no pueden animarse y no pueden mostrar Alert. Para una cuenta regresiva en vivo, usa addDate con applyTimerStyle().
  • Solo tamaños de pantalla de inicio. Si alguien pide un widget de pantalla de bloqueo, construye un widget de pantalla de inicio y dilo en una oración.

Reglas de diseño

  • Muestra una pieza principal de información, grande y legible de un vistazo.
  • Usa los tres tamaños: pequeño muestra solo lo esencial, mediano agrega detalles a su lado, grande agrega más abajo.
  • Aplica el estilo elegido. Soporta modo claro y oscuro con Color.dynamic, y mantén los colores en COLORS.
  • Usa símbolos SF para iconos, no emojis ni imágenes descargadas.
  • Usa como máximo tres tamaños de texto y un color de acento.
  • Mantén lineLimit y minimumScaleFactor en el texto para que nunca se corte de manera extraña. Prueba valores largos mentalmente.
  • Escribe todo el texto visible en el idioma elegido, incluidos los mensajes de error y sin conexión. Configura locale en CONFIG para que coincida, por ejemplo de_DE, para que DateFormatter también use ese idioma.

Plantilla

Reemplaza requestData, los valores de CONFIG, los COLORS y el icono. Adapta las funciones de diseño al contenido.

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: blue; icon-glyph: star;

const CONFIG = {
  title: "My Widget",
  locale: "en_US", // Language for dates and weekdays
  refreshMinutes: 30,
  previewSize: "medium", // Size shown when run in the app: "small", "medium" or "large"
};

const COLORS = {
  background: Color.dynamic(new Color("#EAF3FF"), new Color("#14202E")),
  primary: Color.dynamic(new Color("#10253F"), new Color("#FFFFFF")),
  secondary: Color.dynamic(new Color("#52657C"), new Color("#A9B8C9")),
  accent: Color.dynamic(new Color("#E88A00"), new Color("#FFC24D")),
};

const ROW_COUNTS = { medium: 3, large: 6 };

// --- Data ---

function cachePath() {
  const fm = FileManager.local();
  return fm.joinPath(fm.libraryDirectory(), `${Script.name()}-cache.json`);
}

function saveCache(data) {
  FileManager.local().writeString(cachePath(), JSON.stringify({ savedAt: Date.now(), data }));
}

function readCache() {
  const fm = FileManager.local();
  const path = cachePath();
  if (!fm.fileExists(path)) return null;
  try {
    return JSON.parse(fm.readString(path));
  } catch {
    return null;
  }
}

// Replace this function. It returns what the layout shows.
async function requestData() {
  const request = new Request(
    "https://api.open-meteo.com/v1/forecast?latitude=50.94&longitude=6.96&current=temperature_2m&daily=temperature_2m_max&timezone=auto"
  );
  request.timeoutInterval = 10;
  const response = await request.loadJSON();

  if (response?.current?.temperature_2m === undefined) {
    throw new Error(response?.reason ?? "Unexpected data.");
  }
  return {
    symbol: "thermometer.medium",
    value: `${Math.round(response.current.temperature_2m)}°`,
    caption: "Köln",
    rows: response.daily.time.slice(1).map((day, index) => ({
      label: formatDate(new Date(`${day}T12:00:00`), "EEE"),
      value: `${Math.round(response.daily.temperature_2m_max[index + 1])}°`,
    })),
  };
}

// Returns fresh data if possible and the last saved data otherwise.
async function loadData() {
  try {
    const data = await requestData();
    saveCache(data);
    return { data, savedAt: null };
  } catch (error) {
    const cache = readCache();
    if (cache?.data) return { data: cache.data, savedAt: cache.savedAt };
    throw error;
  }
}

// --- Drawing helpers ---

function formatDate(date, pattern) {
  const formatter = new DateFormatter();
  formatter.locale = CONFIG.locale;
  formatter.dateFormat = pattern;
  return formatter.string(date);
}

function addLabel(stack, text, font, color) {
  const label = stack.addText(text);
  label.font = font;
  label.textColor = color;
  label.lineLimit = 1;
  label.minimumScaleFactor = 0.6;
  return label;
}

function addSymbol(stack, name, size, color) {
  const symbol = SFSymbol.named(name) ?? SFSymbol.named("questionmark.circle.fill");
  symbol.applyFont(Font.systemFont(size));
  const image = stack.addImage(symbol.image);
  image.imageSize = new Size(size, size);
  image.tintColor = color;
  return image;
}

// --- Layout ---

function addMainContent(stack, { data, savedAt }) {
  addLabel(stack, CONFIG.title, Font.semiboldSystemFont(14), COLORS.primary);
  stack.addSpacer();
  addSymbol(stack, data.symbol, 30, COLORS.accent);
  stack.addSpacer(4);
  addLabel(stack, data.value, Font.boldSystemFont(38), COLORS.primary);
  const caption = savedAt ? `Offline · ${formatDate(new Date(savedAt), "HH:mm")}` : data.caption;
  addLabel(stack, caption, Font.systemFont(12), COLORS.secondary);
}

function addRow(stack, row) {
  const line = stack.addStack();
  line.layoutHorizontally();
  line.centerAlignContent();
  addLabel(line, row.label, Font.semiboldSystemFont(14), COLORS.primary);
  line.addSpacer();
  addLabel(line, row.value, Font.systemFont(14), COLORS.secondary);
}

function addRows(stack, rows) {
  stack.addSpacer();
  rows.forEach((row) => {
    addRow(stack, row);
    stack.addSpacer();
  });
}

function createWidget() {
  const widget = new ListWidget();
  widget.backgroundColor = COLORS.background;
  widget.setPadding(14, 16, 14, 16);
  widget.refreshAfterDate = new Date(Date.now() + CONFIG.refreshMinutes * 60 * 1000);
  return widget;
}

function buildSmallWidget(result) {
  const widget = createWidget();
  addMainContent(widget, result);
  return widget;
}

function buildMediumWidget(result) {
  const widget = createWidget();
  const columns = widget.addStack();
  columns.layoutHorizontally();

  const main = columns.addStack();
  main.layoutVertically();
  addMainContent(main, result);

  columns.addSpacer(24);

  const details = columns.addStack();
  details.layoutVertically();
  addRows(details, result.data.rows.slice(0, ROW_COUNTS.medium));
  return widget;
}

function buildLargeWidget(result) {
  const widget = createWidget();
  const main = widget.addStack();
  main.layoutVertically();
  main.size = new Size(0, 150);
  addMainContent(main, result);

  addRows(widget, result.data.rows.slice(0, ROW_COUNTS.large));
  return widget;
}

function buildErrorWidget(error) {
  const widget = createWidget();
  addSymbol(widget, "exclamationmark.triangle.fill", 26, COLORS.accent);
  widget.addSpacer(8);
  addLabel(widget, "Couldn't load the data", Font.semiboldSystemFont(14), COLORS.primary);
  widget.addSpacer(4);
  const message = addLabel(widget, error.message, Font.systemFont(12), COLORS.secondary);
  message.lineLimit = 3;
  return widget;
}

// --- Run ---

function currentSize() {
  const family = config.widgetFamily ?? CONFIG.previewSize;
  if (family === "medium") return "medium";
  if (family === "large" || family === "extraLarge") return "large";
  return "small";
}

async function buildWidget(size) {
  try {
    const result = await loadData();
    if (size === "medium") return buildMediumWidget(result);
    if (size === "large") return buildLargeWidget(result);
    return buildSmallWidget(result);
  } catch (error) {
    return buildErrorWidget(error);
  }
}

const size = currentSize();
const widget = await buildWidget(size);

Script.setWidget(widget);

if (!config.runsInWidget) {
  if (size === "medium") await widget.presentMedium();
  else if (size === "large") await widget.presentLarge();
  else await widget.presentSmall();
}

Script.complete();

Referencia de API

Solo usa los miembros listados aquí a menos que estés seguro de que existen en Scriptable.

ListWidget: addText(text), addDate(date), addImage(image), addSpacer(length?), addStack(), backgroundColor, backgroundImage, backgroundGradient, setPadding(top, leading, bottom, trailing), spacing, url, refreshAfterDate, presentSmall(), presentMedium(), presentLarge()

WidgetStack (de addStack()): mismos métodos add…, layoutHorizontally(), layoutVertically(), topAlignContent(), centerAlignContent(), bottomAlignContent(), size, spacing, backgroundColor, cornerRadius, borderWidth, borderColor, setPadding(…), url

WidgetText: font, textColor, textOpacity, lineLimit, minimumScaleFactor, leftAlignText(), centerAlignText(), rightAlignText(), url

WidgetDate: igual que WidgetText, más applyTimeStyle(), applyDateStyle(), applyRelativeStyle(), applyOffsetStyle(), applyTimerStyle()

WidgetImage: imageSize, tintColor, imageOpacity, cornerRadius, resizable, applyFittingContentMode(), applyFillingContentMode()

Font: Font.systemFont(size), lightSystemFont, mediumSystemFont, semiboldSystemFont, boldSystemFont, boldRoundedSystemFont, regularMonospacedSystemFont

Color: new Color(hex, alpha?), Color.dynamic(light, dark), Color.white(), Color.black(), Color.clear()

LinearGradient: new LinearGradient(), colors, locations, startPoint, endPoint. Usa colores planos en degradados, no Color.dynamic.

SFSymbol: SFSymbol.named(name) (puede devolver null), applyFont(font), image

Size, Point: new Size(width, height), donde 0 significa flexible. new Point(x, y).

DrawContext (para gráficos y anillos): new DrawContext(), size, opaque, respectScreenScale, setFillColor(color), setStrokeColor(color), setLineWidth(width), fillRect(rect), fillEllipse(rect), addPath(path), strokePath(), fillPath(), getImage(). Path: move(point), addLine(point). new Rect(x, y, width, height).

Request: new Request(url), method, headers, body, timeoutInterval, loadJSON(), loadString(), loadImage(), response

FileManager: FileManager.local(), libraryDirectory(), joinPath(a, b), fileExists(path), readString(path), writeString(path, text)

DateFormatter: new DateFormatter(), locale, dateFormat, string(date)

Device: Device.batteryLevel() (0 a 1), Device.isCharging(), Device.model(), Device.systemVersion(), Device.screenSize(), Device.name(), Device.isUsingDarkAppearance()

CalendarEvent: CalendarEvent.today(calendars?), CalendarEvent.thisWeek(calendars?), title, startDate, endDate, isAllDay, location, calendar

Reminder: Reminder.allCompleted(lists?), Reminder.allIncomplete(lists?), title, notes, isCompleted, dueDate, completedDate, calendar

Location: Location.current() (requiere permiso, el usuario debe ejecutar el script en la app una vez)

Formato de respuesta

Entrega:

  1. Una frase que explique lo que hace el widget.
  2. El script completo en un bloque de código (sin triple comilla invertida).
  3. Instrucciones breves: copia el código en Scriptable, asígnale un nombre, agrégalo como widget.

Nunca le pidas al usuario que edite el script ni que toque configuraciones avanzadas.

Skills relacionados