veritatron/scriptable-widget

Crée des widgets prêts à l'emploi pour l'écran d'accueil de l'iPhone avec l'application Scriptable à partir d'une seule phrase.

Qu'est-ce que scriptable-widget ?

Cette compétence permet à un assistant IA de générer un widget JavaScript complet pour l'application Scriptable sur iPhone, en une seule phrase de description. L'utilisateur n'a pas besoin de coder ou de modifier le script. L'assistant suit un workflow précis : déterminer le contenu et la source des données, poser une question unique sur la langue et le style, puis écrire le script à partir d'un modèle. Le widget utilise des sources de données gratuites (Open-Meteo pour la météo, CoinGecko pour les crypto-monnaies, etc.) sans nécessiter de clé API, de compte ou de connexion. Il prend en charge quatre styles visuels (Clean, Minimal, Bold, Playful) avec mode clair/sombre, trois tailles de widget (petit, moyen, grand), et utilise uniquement les API natives de Scriptable (ListWidget, SFSymbol, Request, etc.).

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

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Constructeur de Widget Scriptable

Transformez une phrase en un widget JavaScript prêt à l'emploi pour l'application iPhone Scriptable. L'utilisateur ne code pas, donc ne lui demandez jamais de modifier le script.

Flux de travail

  1. Déterminez ce que le widget affiche et d'où proviennent les données.
  2. Envoyez un seul message de suivi court avant la construction. Demandez la langue et le style du widget (proposez les styles ci-dessous), ainsi que tout autre élément indispensable à la construction, comme le lieu pour un widget météo. Omettez ce que l'utilisateur a déjà indiqué, et s'il a tout indiqué, construisez immédiatement. N'envoyez jamais un deuxième message de suivi. Choisissez des valeurs par défaut judicieuses pour tout le reste.
  3. Écrivez le script en adaptant le modèle ci-dessous.
  4. Parcourez la liste de vérification.
  5. Répondez selon le format décrit dans « Format de réponse ».

Sources de données

N'utilisez jamais un service nécessitant une clé API, un compte ou une connexion. Les scripts ne contiennent jamais de secrets. Si la demande dépend d'un tel service, utilisez la source gratuite la plus proche et mentionnez-le en une phrase. S'il n'y en a pas, indiquez ce qui ne peut pas être fait et proposez le widget le plus proche possible.

Privilégiez les données natives, puis ces sources gratuites :

DonnéesSource
Météohttps://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
Qualité de l'air, pollenhttps://air-quality-api.open-meteo.com/v1/air-quality?latitude=..&longitude=..&current=european_aqi,birch_pollen
Coordonnées d'un lieuhttps://geocoding-api.open-meteo.com/v1/search?name=..&count=1
Taux de changehttps://api.frankfurter.dev/v1/latest?base=EUR&symbols=USD
Prix des crypto-monnaieshttps://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur

Placez les coordonnées d'un lieu directement dans CONFIG. Utilisez Location.current() uniquement si l'utilisateur demande sa position actuelle.

Les données natives ne nécessitent pas de réseau : CalendarEvent, Reminder, Device, dates. Pour celles-ci, supprimez les fonctions de cache et loadData, et construisez les données directement. Le calendrier, les rappels et la localisation nécessitent une autorisation, donc dites à l'utilisateur d'exécuter le script une fois dans l'application Scriptable.

Styles

Proposez ces quatre styles par leur nom avec leur courte description. Si l'utilisateur décrit son propre style, suivez-le. Placez les couleurs dans COLORS sous forme de Color.dynamic(new Color(light), new Color(dark)).

StyleDescriptionFondPrimaireSecondaireAccentPolices
CleanCalme et ordonné, teintes bleues#EAF3FF / #14202E#10253F / #FFFFFF#52657C / #A9B8C9#E88A00 / #FFC24DSystem
MinimalNoir et blanc uniquement#FFFFFF / #000000#000000 / #FFFFFF#6E6E73 / #98989D#000000 / #FFFFFFLight system, grandes valeurs
BoldCouleur forte qui se démarque#2F54EB / #1D39C4#FFFFFF / #FFFFFF#D6E0FF / #D6E0FF#FFD666 / #FFD666Bold system
PlayfulChaud et amical#FFF1E6 / #2B1B14#4A2511 / #FFF1E6#9A6B4F / #D4B8A6#FF6B4A / #FF9A76Bold rounded system

Règles du script

  • Partez du modèle et conservez sa structure. L'en-tête Scriptable doit rester tout en haut.
  • CONFIG doit avoir un title court comme première entrée. Il devient le nom suggéré du script. Tout ce qu'une personne pourrait vouloir modifier doit se trouver dans CONFIG.
  • Conservez Script.setWidget(widget) et terminez le script par Script.complete(); comme dernière ligne.
  • Définissez refreshAfterDate. iOS décide du moment réel de rafraîchissement des widgets, donc ne promettez jamais d'heures exactes. Utilisez au moins 15 minutes.
  • Affichez les erreurs lisibles via buildErrorWidget. Ne laissez jamais un widget vide.
  • Pour les données réseau, conservez le cache pour que le widget fonctionne encore hors ligne.
  • Utilisez uniquement ce que Scriptable fournit. Il n'y a pas de fetch, document, window, require, localStorage, import ou export. Utilisez Request pour le réseau et FileManager pour les fichiers. Les paquets npm ne peuvent pas être utilisés.
  • Le code ne doit pas contenir de triples backticks.
  • Les widgets ne peuvent pas réagir aux tapotements au-delà de l'ouverture d'une URL, ne peuvent pas animer et ne peuvent pas afficher d'Alert. Pour un compte à rebours en direct, utilisez addDate avec applyTimerStyle().
  • Tailles d'écran d'accueil uniquement. Si quelqu'un demande un widget d'écran de verrouillage, créez un widget d'écran d'accueil et mentionnez-le en une phrase.

Règles de conception

  • Affichez une information principale, grande et lisible en un coup d'œil.
  • Utilisez les trois tailles : la petite montre uniquement l'essentiel, la moyenne ajoute des détails à côté, la grande en ajoute plus en dessous.
  • Appliquez le style choisi. Prenez en charge le mode clair et sombre avec Color.dynamic et conservez les couleurs dans COLORS.
  • Utilisez les symboles SF pour les icônes, pas d'emoji ni d'images téléchargées.
  • Utilisez au maximum trois tailles de texte et une couleur d'accent.
  • Gardez lineLimit et minimumScaleFactor sur le texte pour qu'il ne soit jamais coupé de manière disgracieuse. Testez mentalement les longues valeurs.
  • Écrivez tout le texte visible dans la langue choisie, y compris les messages d'erreur et hors ligne. Définissez locale dans CONFIG en conséquence, par exemple fr_FR, pour que DateFormatter utilise également cette langue.

Modèle

Remplacez requestData, les valeurs CONFIG, les COLORS et l'icône. Adaptez les fonctions de mise en page au contenu.

// 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: "Mon Widget",
  locale: "fr_FR", // Langue pour les dates et les jours de la semaine
  refreshMinutes: 30,
  previewSize: "medium", // Taille affichée lors de l'exécution dans l'application : "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 };

// --- Données ---

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

// Remplacez cette fonction. Elle retourne ce que la mise en page affiche.
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 ?? "Données inattendues.");
  }
  return {
    symbol: "thermometer.medium",
    value: `${Math.round(response.current.temperature_2m)}°`,
    caption: "Cologne",
    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])}°`,
    })),
  };
}

// Retourne les données fraîches si possible et les dernières données enregistrées sinon.
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;
  }
}

// --- Aides au dessin ---

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

// --- Mise en page ---

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 ? `Hors ligne · ${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, "Impossible de charger les données", Font.semiboldSystemFont(14), COLORS.primary);
  widget.addSpacer(4);
  const message = addLabel(widget, error.message, Font.systemFont(12), COLORS.secondary);
  message.lineLimit = 3;
  return widget;
}

// --- Exécution ---

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

Référence API

Utilisez uniquement les membres listés ici, sauf si vous êtes sûr qu'ils existent dans 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 (depuis addStack()) : mêmes méthodes 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 : identique à 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. Utilisez des couleurs simples dans les dégradés, pas Color.dynamic.

SFSymbol : SFSymbol.named(name) (peut retourner null), applyFont(font), image

Size, Point : new Size(width, height), où 0 signifie flexible. new Point(x, y).

DrawContext (pour les graphiques et les anneaux) : 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 à 1), Device.isCharging(), Device.model(), Device.systemVersion(), Device.screenResolution(), Device.name()

Location : Location.current() (nécessite une autorisation – demande Location.setAccuracyToKilometer(), mais il peut aussi retourner les coordonnées actuelles)

CalendarEvent et Reminder : CalendarEvent.oneDay(), Reminder.allIncomplete() (documentation Scriptable complète pour l'utilisation)

config : config.widgetFamily, config.runsInWidget

Format de réponse

Répondez en utilisant des blocs de code pour les scripts réutilisables et du texte normal pour la description du widget.

Une fois que vous construisez le widget, répondez avec :

  1. Le titre du widget et le style choisi.
  2. Un court résumé de ce qu'il affiche.
  3. Le code complet du widget dans un bloc de code JavaScript. L'utilisateur l'enregistre en tant que nouveau script dans Scriptable sous le titre du widget.
  4. La liste de vérification ci-dessous, avec votre confirmation pour chaque élément.

Liste de vérification

  • Le script commence par l'en-tête Scriptable.
  • CONFIG contient un titre court comme première entrée.
  • refreshAfterDate est défini sur au moins 15 minutes.
  • Script.setWidget(widget) et Script.complete() sont présents.
  • Les symboles SF sont utilisés pour les icônes.
  • Le texte visible est dans la bonne langue.
  • lineLimit et minimumScaleFactor sont appliqués aux étiquettes.
  • Les trois tailles (petite, moyenne, grande) sont construites.
  • Une couleur dynamique est utilisée pour chaque rôle.
  • Les données sont mises en cache pour un fonctionnement hors ligne.

Skills associés