CommunityCodierung & Entwicklunggithub.com

veritatron/scriptable-widget

Erstellt aus einem einzigen Satz fertige iPhone-Startbildschirm-Widgets für die Scriptable-App. Verwende dies, wenn jemand nach einem iPhone-Widget oder einem Scriptable-Skript fragt.

Was ist scriptable-widget?

Der Scriptable Widget Builder ist ein leistungsstarkes KI-Agenten-Skill, das aus einer einfachen Beschreibung ein voll funktionsfähiges JavaScript-Widget für die Scriptable iPhone-App erstellt. Der Benutzer muss dabei keinen Code schreiben oder bearbeiten. Das Skill unterstützt verschiedene Datenquellen wie Open-Meteo für Wetter und Luftqualität, CoinGecko für Kryptopreise und native iOS-Daten wie Kalender, Erinnerungen und Geräteinformationen. Es ist auf Claude Code, Codex, Cursor und ähnliche KI-Entwicklungstools ausgelegt. Das Skill bietet vier vordefinierte Stile (Clean, Minimal, Bold, Playful) mit vollständiger Unterstützung für Hell-/Dunkelmodi, erstellt Widgets in drei Größen (klein, mittel, groß) und enthält ein integriertes Caching-System für Offline-Funktionalität. Es fragt nur bei Bedarf nach und verwendet sensible Standardwerte, um den Workflow zu optimieren.

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

Installed? Explore more Codierung & Entwicklung skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Scriptable Widget Builder

Verwandle einen einzigen Satz in ein fertiges JavaScript-Widget für die Scriptable-iPhone-App. Der Benutzer programmiert nicht, frage ihn also nie, das Skript zu bearbeiten.

Arbeitsablauf

  1. Ermittle, was das Widget anzeigt und woher die Daten kommen.
  2. Sende vor dem Erstellen eine kurze Rückfrage. Frage nach der Sprache und dem Stil des Widgets (biete die untenstehenden Stile an), sowie nach allem anderen, ohne das das Widget nicht erstellt werden kann, z. B. dem Ort für ein Wetter-Widget. Lasse weg, was der Benutzer bereits genannt hat. Wenn alles genannt wurde, erstelle sofort. Sende niemals eine zweite Rückfrage. Verwende für alles andere sinnvolle Standardwerte.
  3. Schreibe das Skript, indem du die untenstehende Vorlage anpasst.
  4. Gehe die Checkliste durch.
  5. Antworte im unter „Antwortformat“ beschriebenen Format.

Datenquellen

Verwende niemals einen Dienst, der einen API-Schlüssel, ein Konto oder einen Login benötigt. Skripte enthalten niemals Geheimnisse. Wenn die Anfrage auf einen solchen Dienst angewiesen ist, verwende die nächstbeste kostenlose Quelle und teile dies in einem Satz mit. Wenn es keine gibt, nenne, was nicht möglich ist, und biete das nächstbeste Widget an.

Bevorzuge native Daten, dann diese kostenlosen Quellen:

DatenQuelle
Wetterhttps://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
Luftqualität, Pollenhttps://air-quality-api.open-meteo.com/v1/air-quality?latitude=..&longitude=..&current=european_aqi,birch_pollen
Koordinaten eines Orteshttps://geocoding-api.open-meteo.com/v1/search?name=..&count=1
Wechselkursehttps://api.frankfurter.dev/v1/latest?base=EUR&symbols=USD
Kryptopreisehttps://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur

Setze die Koordinaten eines Ortes direkt in CONFIG ein. Verwende Location.current() nur, wenn der Benutzer nach seinem aktuellen Standort fragt.

Native Daten benötigen kein Netzwerk: CalendarEvent, Reminder, Device, Datumsangaben. Entferne für diese die Cache-Funktionen und loadData und erstelle die Daten direkt. Kalender, Erinnerungen und Standort benötigen eine Berechtigung. Sage dem Benutzer daher, er solle das Skript einmal in der Scriptable-App ausführen.

Stile

Biete diese vier mit Namen und Kurzbeschreibung an. Wenn der Benutzer seinen eigenen Stil beschreibt, folge ihm. Setze die Farben als Color.dynamic(new Color(light), new Color(dark)) in COLORS.

StilBeschreibungHintergrundPrimärSekundärAkzentSchriftarten
CleanRuhig und ordentlich, Blautöne#EAF3FF / #14202E#10253F / #FFFFFF#52657C / #A9B8C9#E88A00 / #FFC24DSystem
MinimalNur Schwarz und Weiß#FFFFFF / #000000#000000 / #FFFFFF#6E6E73 / #98989D#000000 / #FFFFFFHelles System, große Werte
BoldStarke Farbe, die heraussticht#2F54EB / #1D39C4#FFFFFF / #FFFFFF#D6E0FF / #D6E0FF#FFD666 / #FFD666Fettes System
PlayfulWarm und freundlich#FFF1E6 / #2B1B14#4A2511 / #FFF1E6#9A6B4F / #D4B8A6#FF6B4A / #FF9A76Fette abgerundete System

Skript-Regeln

  • Beginne mit der Vorlage und behalte ihre Struktur bei. Der Scriptable-Header muss ganz oben bleiben.
  • CONFIG muss einen kurzen title als ersten Eintrag haben. Er wird zum vorgeschlagenen Skriptnamen. Alles, was eine Person ändern möchte, gehört in CONFIG.
  • Behalte Script.setWidget(widget) und beende das Skript mit Script.complete(); als letzter Zeile.
  • Setze refreshAfterDate. iOS entscheidet, wann Widgets tatsächlich aktualisiert werden, verspreche also niemals genaue Zeiten. Verwende mindestens 15 Minuten.
  • Zeige lesbare Fehler durch buildErrorWidget an. Lasse ein Widget niemals leer.
  • Behalte für Netzwerkdaten den Cache, damit das Widget auch offline funktioniert.
  • Verwende nur das, was Scriptable bereitstellt. Es gibt kein fetch, document, window, require, localStorage, import oder export. Verwende Request für Netzwerke und FileManager für Dateien. npm-Pakete können nicht verwendet werden.
  • Der Code darf keine drei Backticks enthalten.
  • Widgets können nicht auf Tippen reagieren, außer um eine URL zu öffnen, können nicht animiert werden und können kein Alert anzeigen. Verwende für einen Live-Countdown addDate mit applyTimerStyle().
  • Nur Startbildschirmgrößen. Wenn jemand nach einem Sperrbildschirm-Widget fragt, erstelle ein Startbildschirm-Widget und teile dies in einem Satz mit.

Design-Regeln

  • Zeige eine Hauptinformation, groß und auf einen Blick lesbar.
  • Verwende alle drei Größen: Klein zeigt nur das Wesentliche, mittel fügt daneben Details hinzu, groß fügt darunter mehr hinzu.
  • Wende den gewählten Stil an. Unterstütze den Hell- und Dunkelmodus mit Color.dynamic und behalte die Farben in COLORS.
  • Verwende SF-Symbole für Icons, keine Emojis oder heruntergeladene Bilder.
  • Verwende höchstens drei Textgrößen und eine Akzentfarbe.
  • Behalte lineLimit und minimumScaleFactor bei Text, damit er nie unschön abgeschnitten wird. Teste lange Werte gedanklich.
  • Schreibe den gesamten sichtbaren Text in der gewählten Sprache, einschließlich der Fehler- und Offline-Meldungen. Setze locale in CONFIG entsprechend, z. B. de_DE, damit auch DateFormatter diese Sprache verwendet.

Vorlage

Ersetze requestData, die CONFIG-Werte, die COLORS und das Icon. Passe die Layout-Funktionen an den Inhalt an.

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

API-Referenz

Verwende nur die hier aufgeführten Member, es sei denn, du bist sicher, dass sie in Scriptable existieren.

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 (von addStack()): gleiche add…-Methoden, layoutHorizontally(), layoutVertically(), topAlignContent(), centerAlignContent(), bottomAlignContent(), size, spacing, backgroundColor, cornerRadius, borderWidth, borderColor, setPadding(…), url

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

WidgetDate: wie WidgetText, plus 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. Verwende einfache Farben in Gradienten, nicht Color.dynamic.

SFSymbol: SFSymbol.named(name) (kann null zurückgeben), applyFont(font), image

Size, Point: new Size(width, height), wobei 0 flexibel bedeutet. new Point(x, y).

DrawContext (für Diagramme und Ringe): 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 bis 1), Device.isCharging(), Device.model(), Device.systemVersion(), Device.screenResolution(), Device.name(), Device.isUsingDarkAppearance()

CalendarEvent: CalendarEvent.between(dateStart, dateEnd, calendars?) gibt ein Array. event.title, event.startDate, event.endDate, event.location, event.calendar, event.notes.

Reminder: Reminder.allIncomplete(calendars?). reminder.title, reminder.dueDate, reminder.notes, reminder.isOverdue, reminder.completionDate, reminder.calendar.

Location: Location.current() (benötigt Berechtigung, kann langsam sein), gibt ein Objekt mit latitude und longitude zurück.

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

config: config.widgetFamily ("small", "medium", "large", "extraLarge"), config.runsInWidget (boolesch)

Checkliste vor dem Absenden

  1. Enthält das Skript keine API-Schlüssel oder Geheimnisse?
  2. Sind der CONFIG.title und der locale-Wert gesetzt und die COLORS vorhanden?
  3. Gibt Script.setWidget(widget) das Widget mit Script.complete() zurück?
  4. Haben alle Texte die richtige lineLimit und minimumScaleFactor?
  5. Wird refreshAfterDate auf mindestens 15 Minuten gesetzt?
  6. Gibt es für jeden möglichen Fehler eine lesbare Ausgabe?
  7. Haben wir nur einen Eintrag pro Zeile im Skript-Codeblock verwendet? (Keine drei Backticks im Skript.)
  8. Ist die Fehlermeldung, falls vorhanden, in der gleichen Sprache wie das Widget?
  9. Werden nur hier dokumentierte Scriptable-Member verwendet?

Antwortformat

Wenn das Widget fertig ist, antworte mit diesem Format:

Dein Widget „[Name]“ ist fertig!

⚠️  **Wichtig:** Öffne das Skript einmal in der Scriptable-App, um den vollständigen Funktionsumfang freizuschalten. Tippe auf ▶️, um eine Vorschau anzuzeigen, und weise es dann auf dem Startbildschirm zu (Widget hinzufügen > Scriptable > [Skriptname] auswählen).

Verwandte Skills