Criador de Widgets para Scriptable

Constrói widgets prontos para a tela inicial do iPhone para o aplicativo Scriptable a partir de uma única frase. Use quando alguém pedir um widget para iPhone ou um script para Scriptable.

O que é Criador de Widgets para Scriptable?

O Criador de Widgets para Scriptable é uma skill de agente de IA que transforma uma descrição de frase única em um widget JavaScript funcional para a tela inicial do aplicativo Scriptable no iPhone. Ela elimina a necessidade de o usuário codificar, gerenciando todo o processo de desenvolvimento — desde a interpretação do requisito, seleção de uma das quatro fontes de dados gratuitas (como Open-Meteo para clima ou CoinGecko para criptomoedas), até a oferta de quatro estilos visuais predefinidos (Clean, Minimal, Bold, Playful). A skill faz uma única pergunta de acompanhamento sobre idioma e estilo (se necessário) e, em seguida, gera um script completo que inclui caching offline, suporte a modo claro/escuro e designs responsivos para widgets pequenos, médios e grandes. Ela impõe as práticas recomendadas para o ecossistema Scriptable — usando `Request` para dados de rede, `FileManager` para caching e `Color.dynamic` para temas — enquanto evita serviços que exigem chaves de API ou contas. O usuário final simplesmente copia o script para o aplicativo Scriptable, e o widget aparece na tela inicial. Esta skill é ideal para usuários que desejam widgets personalizados para iPhone sem escrever nenhum código.

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

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Construtor de Widgets para Scriptable

Transforme uma frase em um widget JavaScript completo para o aplicativo Scriptable do iPhone. O usuário não programa, então nunca peça a ele para editar o script.

Fluxo de Trabalho

  1. Descubra o que o widget exibe e de onde os dados vêm.
  2. Envie uma única mensagem curta de acompanhamento antes de construir. Pergunte sobre o idioma e estilo do widget (ofereça os estilos abaixo), além de qualquer outra coisa sem a qual o widget não possa ser construído, como o local para um widget de clima. Deixe de fora o que o usuário já declarou, e se ele declarou tudo, construa imediatamente. Nunca envie uma segunda mensagem de acompanhamento. Escolha valores padrão sensatos para todo o resto.
  3. Escreva o script adaptando o modelo abaixo.
  4. Verifique a lista de verificação.
  5. Responda no formato descrito em "Formato de resposta".

Fontes de Dados

Nunca use um serviço que precise de chave de API, conta ou login. Scripts nunca contêm segredos. Se a solicitação depender de tal serviço, use a fonte gratuita mais próxima e diga isso em uma frase. Se não houver nenhuma, diga o que não pode ser feito e ofereça o widget mais próximo.

Prefira dados nativos, depois estas fontes gratuitas:

DadoFonte
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
Qualidade do ar, pólenhttps://air-quality-api.open-meteo.com/v1/air-quality?latitude=..&longitude=..&current=european_aqi,birch_pollen
Coordenadas de um lugarhttps://geocoding-api.open-meteo.com/v1/search?name=..&count=1
Taxas de câmbiohttps://api.frankfurter.dev/v1/latest?base=EUR&symbols=USD
Preços de criptomoedashttps://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur

Coloque as coordenadas de um lugar diretamente em CONFIG. Use Location.current() somente se o usuário pedir a localização atual dele.

Dados nativos não precisam de rede: CalendarEvent, Reminder, Device, datas. Para estes, remova as funções de cache e loadData, e construa os dados diretamente. Calendário, lembretes e localização precisam de permissão, então informe o usuário para executar o script uma vez no aplicativo Scriptable.

Estilos

Ofereça estes quatro pelo nome com sua breve descrição. Se o usuário descrever seu próprio estilo, siga-o. Coloque as cores em COLORS como Color.dynamic(new Color(light), new Color(dark)).

EstiloDescriçãoFundoPrimáriaSecundáriaDestaqueFontes
CleanCalmo e organizado, tons de azul#EAF3FF / #14202E#10253F / #FFFFFF#52657C / #A9B8C9#E88A00 / #FFC24DSistema
MinimalApenas preto e branco#FFFFFF / #000000#000000 / #FFFFFF#6E6E73 / #98989D#000000 / #FFFFFFSistema claro, valores grandes
BoldCor forte que se destaca#2F54EB / #1D39C4#FFFFFF / #FFFFFF#D6E0FF / #D6E0FF#FFD666 / #FFD666Sistema negrito
PlayfulQuente e amigável#FFF1E6 / #2B1B14#4A2511 / #FFF1E6#9A6B4F / #D4B8A6#FF6B4A / #FF9A76Sistema negrito arredondado

Regras do Script

  • Comece a partir do modelo e mantenha sua estrutura. O cabeçalho do Scriptable deve permanecer no topo.
  • CONFIG deve ter um title curto como sua primeira entrada. Ele se torna o nome sugerido do script. Tudo que uma pessoa possa querer alterar pertence ao CONFIG.
  • Mantenha Script.setWidget(widget) e termine o script com Script.complete(); como a última linha.
  • Defina refreshAfterDate. O iOS decide quando os widgets realmente atualizam, então nunca prometa horários exatos. Use pelo menos 15 minutos.
  • Mostre erros legíveis através de buildErrorWidget. Nunca deixe um widget em branco.
  • Para dados de rede, mantenha o cache para que o widget ainda funcione offline.
  • Use apenas o que o Scriptable fornece. Não há fetch, document, window, require, localStorage, import ou export. Use Request para rede e FileManager para arquivos. Pacotes npm não podem ser usados.
  • O código não deve conter crases triplas.
  • Widgets não podem reagir a toques além de abrir uma URL, não podem animar e não podem mostrar Alert. Para uma contagem regressiva ao vivo, use addDate com applyTimerStyle().
  • Apenas tamanhos de tela inicial. Se alguém pedir um widget de tela de bloqueio, construa um widget de tela inicial e diga isso em uma frase.

Regras de Design

  • Mostre uma informação principal, grande e legível de relance.
  • Use todos os três tamanhos: pequeno mostra apenas o essencial, médio adiciona detalhes ao lado, grande adiciona mais abaixo.
  • Aplique o estilo escolhido. Suporte modo claro e escuro com Color.dynamic, e mantenha as cores em COLORS.
  • Use Símbolos SF para ícones, não emojis ou imagens baixadas.
  • Use no máximo três tamanhos de texto e uma cor de destaque.
  • Mantenha lineLimit e minimumScaleFactor no texto para que nunca seja cortado de forma estranha. Teste valores longos mentalmente.
  • Escreva todo o texto visível no idioma escolhido, incluindo as mensagens de erro e offline. Defina locale em CONFIG para corresponder, por exemplo de_DE, para que DateFormatter também use esse idioma.

Modelo

Substitua requestData, os valores de CONFIG, os COLORS e o ícone. Adapte as funções de layout ao conteúdo.

// 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: "Meu Widget",
  locale: "pt_BR", // Idioma para datas e dias da semana
  refreshMinutes: 30,
  previewSize: "medium", // Tamanho mostrado ao executar no aplicativo: "small", "medium" ou "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 };

// --- Dados ---

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;
  }
}

// Substitua esta função. Ela retorna o que o layout mostra.
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 ?? "Dados inesperados.");
  }
  return {
    symbol: "thermometer.medium",
    value: `${Math.round(response.current.temperature_2m)}°`,
    caption: "Colônia",
    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])}°`,
    })),
  };
}

// Retorna dados frescos se possível e os últimos dados salvos caso contrário.
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;
  }
}

// --- Auxiliares de desenho ---

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, "Não foi possível carregar os dados", Font.semiboldSystemFont(14), COLORS.primary);
  widget.addSpacer(4);
  const message = addLabel(widget, error.message, Font.systemFont(12), COLORS.secondary);
  message.lineLimit = 3;
  return widget;
}

// --- Execução ---

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();

Referência da API

Use apenas os membros listados aqui, a menos que tenha certeza de que existem no 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()): mesmos 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: mesmo que WidgetText, mais 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. Use cores simples em gradientes, não Color.dynamic.

SFSymbol: SFSymbol.named(name) (pode retornar null), applyFont(font), image

Size, Point: new Size(width, height), onde 0 significa flexível. new Point(x, y).

DrawContext (para gráficos e anéis): 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.isLowPowerModeEnabled(), Device.name(), Device.model(), Device.systemVersion(), Device.screenSize(), Device.preferredContentSizeCategory(), Device.isUsingDarkAppearance()

Location: Location.current(), Location.setAccuracyToXxx(), Location.setDistanceFilter(xx)

CalendarEvent: CalendarEvent.between(startDate, endDate, calendars?) (retorna array; event.title, event.startDate, event.endDate, event.calendar.title)

Reminder: Reminder.allIncomplete(inCalendars?) (retorna array; reminder.title, reminder.dueDate, reminder.completed, reminder.calendar.title)

Calendar: Calendar.defaultForEvents(), Calendar.defaultForReminders(), Calendar.forRemindersByTitle(title)

Lista de Verificação

  • Cabeçalho do Scriptable no topo imutável.
  • CONFIG tem title, locale, refreshMinutes, previewSize; ajuste locale e cidade nos dados.
  • Nenhuma chave de API ou segredo no script.
  • Estilo aplicado: cores com Color.dynamic, símbolos SF, máx. 3 tamanhos de texto, lineLimit e minimumScaleFactor.
  • Todos os três tamanhos de widget construídos.
  • refreshAfterDate definido.
  • Cache implementado para dados de rede.
  • Apenas membros da referência da API usados.
  • Texto visível está no idioma escolhido e locale corresponde.
  • Mensagem única de acompanhamento (ou construção imediata se tudo estiver claro).
  • Resposta inclui o script completo, uma frase sobre onde os dados vêm, e uma nota sobre execução única para permissões.

Formato de Resposta

Sempre publique sua resposta da seguinte forma:

Widget: Nome do Widget (baseado em CONFIG.title)

Cole o script do widget completo.

Notas:

  • Fonte de dados: [uma frase curta]
  • Permissões: [se necessário, uma frase curta]
  • Tamanhos: pequeno, médio, grande
  • Idioma: [idioma]
  • Estilo: [nome do estilo]

Habilidades Relacionadas