diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 9b42491e..712c1d2c 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -15,6 +15,450 @@ "authentication": "ON_INSTALL" }, "category": "Productivity" + }, + { + "name": "antigravity-bundle-essentials", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-essentials" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Essentials & Core" + }, + { + "name": "antigravity-bundle-security-engineer", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-security-engineer" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Security & Compliance" + }, + { + "name": "antigravity-bundle-security-developer", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-security-developer" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Security & Compliance" + }, + { + "name": "antigravity-bundle-web-wizard", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-web-wizard" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Web Development" + }, + { + "name": "antigravity-bundle-web-designer", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-web-designer" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Web Development" + }, + { + "name": "antigravity-bundle-full-stack-developer", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-full-stack-developer" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Web Development" + }, + { + "name": "antigravity-bundle-agent-architect", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-agent-architect" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "AI & Agents" + }, + { + "name": "antigravity-bundle-llm-application-developer", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-llm-application-developer" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "AI & Agents" + }, + { + "name": "antigravity-bundle-indie-game-dev", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-indie-game-dev" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Game Development" + }, + { + "name": "antigravity-bundle-python-pro", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-python-pro" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Backend & Languages" + }, + { + "name": "antigravity-bundle-typescript-javascript", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-typescript-javascript" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Backend & Languages" + }, + { + "name": "antigravity-bundle-systems-programming", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-systems-programming" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Backend & Languages" + }, + { + "name": "antigravity-bundle-startup-founder", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-startup-founder" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Product & Business" + }, + { + "name": "antigravity-bundle-business-analyst", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-business-analyst" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Product & Business" + }, + { + "name": "antigravity-bundle-marketing-growth", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-marketing-growth" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Product & Business" + }, + { + "name": "antigravity-bundle-devops-cloud", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-devops-cloud" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "DevOps & Infrastructure" + }, + { + "name": "antigravity-bundle-observability-monitoring", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-observability-monitoring" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "DevOps & Infrastructure" + }, + { + "name": "antigravity-bundle-data-analytics", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-data-analytics" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Data & Analytics" + }, + { + "name": "antigravity-bundle-data-engineering", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-data-engineering" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Data & Analytics" + }, + { + "name": "antigravity-bundle-creative-director", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-creative-director" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Creative & Content" + }, + { + "name": "antigravity-bundle-qa-testing", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-qa-testing" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Quality Assurance" + }, + { + "name": "antigravity-bundle-mobile-developer", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-mobile-developer" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Specialized Packs" + }, + { + "name": "antigravity-bundle-integration-apis", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-integration-apis" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Specialized Packs" + }, + { + "name": "antigravity-bundle-architecture-design", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-architecture-design" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Specialized Packs" + }, + { + "name": "antigravity-bundle-ddd-evented-architecture", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-ddd-evented-architecture" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Specialized Packs" + }, + { + "name": "antigravity-bundle-automation-builder", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-automation-builder" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Specialized Packs" + }, + { + "name": "antigravity-bundle-revops-crm-automation", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-revops-crm-automation" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Specialized Packs" + }, + { + "name": "antigravity-bundle-commerce-payments", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-commerce-payments" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Specialized Packs" + }, + { + "name": "antigravity-bundle-odoo-erp", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-odoo-erp" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Specialized Packs" + }, + { + "name": "antigravity-bundle-azure-ai-cloud", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-azure-ai-cloud" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Specialized Packs" + }, + { + "name": "antigravity-bundle-expo-react-native", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-expo-react-native" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Specialized Packs" + }, + { + "name": "antigravity-bundle-apple-platform-design", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-apple-platform-design" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Specialized Packs" + }, + { + "name": "antigravity-bundle-makepad-builder", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-makepad-builder" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Specialized Packs" + }, + { + "name": "antigravity-bundle-seo-specialist", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-seo-specialist" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Specialized Packs" + }, + { + "name": "antigravity-bundle-documents-presentations", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-documents-presentations" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Specialized Packs" + }, + { + "name": "antigravity-bundle-oss-maintainer", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-oss-maintainer" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Maintainer & OSS" + }, + { + "name": "antigravity-bundle-skill-author", + "source": { + "source": "local", + "path": "./plugins/antigravity-bundle-skill-author" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Maintainer & OSS" } ] } diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 3b94eac0..282fdd05 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,14 +5,14 @@ "url": "https://github.com/sickn33/antigravity-awesome-skills" }, "metadata": { - "description": "Single-plugin Claude Code marketplace entry for the Antigravity Awesome Skills library.", - "version": "7.7.0" + "description": "Claude Code marketplace entries for the full Antigravity Awesome Skills library and its editorial bundles.", + "version": "8.10.0" }, "plugins": [ { "name": "antigravity-awesome-skills", - "version": "7.7.0", - "description": "Expose the repository's curated `skills/` tree to Claude Code through a single plugin marketplace entry.", + "version": "8.10.0", + "description": "Expose the full repository `skills/` tree to Claude Code through a single marketplace entry.", "author": { "name": "sickn33 and contributors", "url": "https://github.com/sickn33/antigravity-awesome-skills" @@ -28,6 +28,746 @@ "marketplace" ], "source": "./" + }, + { + "name": "antigravity-bundle-essentials", + "version": "8.10.0", + "description": "Install the \"Essentials\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "essentials", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-essentials" + }, + { + "name": "antigravity-bundle-security-engineer", + "version": "8.10.0", + "description": "Install the \"Security Engineer\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "security-engineer", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-security-engineer" + }, + { + "name": "antigravity-bundle-security-developer", + "version": "8.10.0", + "description": "Install the \"Security Developer\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "security-developer", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-security-developer" + }, + { + "name": "antigravity-bundle-web-wizard", + "version": "8.10.0", + "description": "Install the \"Web Wizard\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "web-wizard", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-web-wizard" + }, + { + "name": "antigravity-bundle-web-designer", + "version": "8.10.0", + "description": "Install the \"Web Designer\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "web-designer", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-web-designer" + }, + { + "name": "antigravity-bundle-full-stack-developer", + "version": "8.10.0", + "description": "Install the \"Full-Stack Developer\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "full-stack-developer", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-full-stack-developer" + }, + { + "name": "antigravity-bundle-agent-architect", + "version": "8.10.0", + "description": "Install the \"Agent Architect\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "agent-architect", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-agent-architect" + }, + { + "name": "antigravity-bundle-llm-application-developer", + "version": "8.10.0", + "description": "Install the \"LLM Application Developer\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "llm-application-developer", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-llm-application-developer" + }, + { + "name": "antigravity-bundle-indie-game-dev", + "version": "8.10.0", + "description": "Install the \"Indie Game Dev\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "indie-game-dev", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-indie-game-dev" + }, + { + "name": "antigravity-bundle-python-pro", + "version": "8.10.0", + "description": "Install the \"Python Pro\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "python-pro", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-python-pro" + }, + { + "name": "antigravity-bundle-typescript-javascript", + "version": "8.10.0", + "description": "Install the \"TypeScript & JavaScript\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "typescript-javascript", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-typescript-javascript" + }, + { + "name": "antigravity-bundle-systems-programming", + "version": "8.10.0", + "description": "Install the \"Systems Programming\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "systems-programming", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-systems-programming" + }, + { + "name": "antigravity-bundle-startup-founder", + "version": "8.10.0", + "description": "Install the \"Startup Founder\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "startup-founder", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-startup-founder" + }, + { + "name": "antigravity-bundle-business-analyst", + "version": "8.10.0", + "description": "Install the \"Business Analyst\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "business-analyst", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-business-analyst" + }, + { + "name": "antigravity-bundle-marketing-growth", + "version": "8.10.0", + "description": "Install the \"Marketing & Growth\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "marketing-growth", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-marketing-growth" + }, + { + "name": "antigravity-bundle-devops-cloud", + "version": "8.10.0", + "description": "Install the \"DevOps & Cloud\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "devops-cloud", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-devops-cloud" + }, + { + "name": "antigravity-bundle-observability-monitoring", + "version": "8.10.0", + "description": "Install the \"Observability & Monitoring\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "observability-monitoring", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-observability-monitoring" + }, + { + "name": "antigravity-bundle-data-analytics", + "version": "8.10.0", + "description": "Install the \"Data & Analytics\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "data-analytics", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-data-analytics" + }, + { + "name": "antigravity-bundle-data-engineering", + "version": "8.10.0", + "description": "Install the \"Data Engineering\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "data-engineering", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-data-engineering" + }, + { + "name": "antigravity-bundle-creative-director", + "version": "8.10.0", + "description": "Install the \"Creative Director\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "creative-director", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-creative-director" + }, + { + "name": "antigravity-bundle-qa-testing", + "version": "8.10.0", + "description": "Install the \"QA & Testing\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "qa-testing", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-qa-testing" + }, + { + "name": "antigravity-bundle-mobile-developer", + "version": "8.10.0", + "description": "Install the \"Mobile Developer\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "mobile-developer", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-mobile-developer" + }, + { + "name": "antigravity-bundle-integration-apis", + "version": "8.10.0", + "description": "Install the \"Integration & APIs\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "integration-apis", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-integration-apis" + }, + { + "name": "antigravity-bundle-architecture-design", + "version": "8.10.0", + "description": "Install the \"Architecture & Design\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "architecture-design", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-architecture-design" + }, + { + "name": "antigravity-bundle-ddd-evented-architecture", + "version": "8.10.0", + "description": "Install the \"DDD & Evented Architecture\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "ddd-evented-architecture", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-ddd-evented-architecture" + }, + { + "name": "antigravity-bundle-automation-builder", + "version": "8.10.0", + "description": "Install the \"Automation Builder\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "automation-builder", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-automation-builder" + }, + { + "name": "antigravity-bundle-revops-crm-automation", + "version": "8.10.0", + "description": "Install the \"RevOps & CRM Automation\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "revops-crm-automation", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-revops-crm-automation" + }, + { + "name": "antigravity-bundle-commerce-payments", + "version": "8.10.0", + "description": "Install the \"Commerce & Payments\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "commerce-payments", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-commerce-payments" + }, + { + "name": "antigravity-bundle-odoo-erp", + "version": "8.10.0", + "description": "Install the \"Odoo ERP\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "odoo-erp", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-odoo-erp" + }, + { + "name": "antigravity-bundle-azure-ai-cloud", + "version": "8.10.0", + "description": "Install the \"Azure AI & Cloud\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "azure-ai-cloud", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-azure-ai-cloud" + }, + { + "name": "antigravity-bundle-expo-react-native", + "version": "8.10.0", + "description": "Install the \"Expo & React Native\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "expo-react-native", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-expo-react-native" + }, + { + "name": "antigravity-bundle-apple-platform-design", + "version": "8.10.0", + "description": "Install the \"Apple Platform Design\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "apple-platform-design", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-apple-platform-design" + }, + { + "name": "antigravity-bundle-makepad-builder", + "version": "8.10.0", + "description": "Install the \"Makepad Builder\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "makepad-builder", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-makepad-builder" + }, + { + "name": "antigravity-bundle-seo-specialist", + "version": "8.10.0", + "description": "Install the \"SEO Specialist\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "seo-specialist", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-seo-specialist" + }, + { + "name": "antigravity-bundle-documents-presentations", + "version": "8.10.0", + "description": "Install the \"Documents & Presentations\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "documents-presentations", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-documents-presentations" + }, + { + "name": "antigravity-bundle-oss-maintainer", + "version": "8.10.0", + "description": "Install the \"OSS Maintainer\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "oss-maintainer", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-oss-maintainer" + }, + { + "name": "antigravity-bundle-skill-author", + "version": "8.10.0", + "description": "Install the \"Skill Author\" editorial skill bundle for Claude Code.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "claude-code", + "skills", + "bundle", + "skill-author", + "marketplace" + ], + "source": "./plugins/antigravity-bundle-skill-author" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 515501bb..001bd986 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "antigravity-awesome-skills", - "version": "7.7.0", - "description": "Universal agentic skill library for Claude Code with 1,254+ reusable skills across coding, security, design, product, and operations workflows.", + "version": "8.10.0", + "description": "Universal agentic skill library for Claude Code with 1,328+ reusable skills across coding, security, design, product, and operations workflows.", "author": { "name": "sickn33 and contributors", "url": "https://github.com/sickn33/antigravity-awesome-skills" diff --git a/README.md b/README.md index 08fe123c..989abdb1 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,8 @@ If you use Codex and prefer a marketplace-style plugin source instead of copying The Codex plugin points at the same curated `skills/` tree through a repo-local plugin entry, so the library can be exposed as an installable Codex plugin source without duplicating the catalog. +Bundle users can also install focused Claude Code and Codex bundle plugins from the generated marketplace metadata instead of taking the full library at once. + ## Choose Your Tool | Tool | Install | First Use | diff --git a/data/editorial-bundles.json b/data/editorial-bundles.json new file mode 100644 index 00000000..7e036598 --- /dev/null +++ b/data/editorial-bundles.json @@ -0,0 +1,1319 @@ +{ + "bundles": [ + { + "id": "essentials", + "name": "Essentials", + "group": "Essentials & Core", + "emoji": "๐Ÿš€", + "tagline": "The \"Essentials\" Starter Pack", + "audience": "For everyone. Install these first.", + "description": "For everyone. Install these first.", + "skills": [ + { + "id": "concise-planning", + "summary": "Always start with a plan." + }, + { + "id": "lint-and-validate", + "summary": "Keep your code clean automatically." + }, + { + "id": "git-pushing", + "summary": "Save your work safely." + }, + { + "id": "kaizen", + "summary": "Continuous improvement mindset." + }, + { + "id": "systematic-debugging", + "summary": "Debug like a pro." + } + ] + }, + { + "id": "security-engineer", + "name": "Security Engineer", + "group": "Security & Compliance", + "emoji": "๐Ÿ›ก๏ธ", + "tagline": "The \"Security Engineer\" Pack", + "audience": "For pentesting, auditing, and hardening.", + "description": "For pentesting, auditing, and hardening.", + "skills": [ + { + "id": "ethical-hacking-methodology", + "summary": "The Bible of ethical hacking." + }, + { + "id": "burp-suite-testing", + "summary": "Web vulnerability scanning." + }, + { + "id": "top-web-vulnerabilities", + "summary": "OWASP-aligned vulnerability taxonomy." + }, + { + "id": "linux-privilege-escalation", + "summary": "Advanced Linux security assessment." + }, + { + "id": "cloud-penetration-testing", + "summary": "AWS/Azure/GCP security." + }, + { + "id": "security-auditor", + "summary": "Comprehensive security audits." + }, + { + "id": "vulnerability-scanner", + "summary": "Advanced vulnerability analysis." + } + ] + }, + { + "id": "security-developer", + "name": "Security Developer", + "group": "Security & Compliance", + "emoji": "๐Ÿ”", + "tagline": "The \"Security Developer\" Pack", + "audience": "For building secure applications.", + "description": "For building secure applications.", + "skills": [ + { + "id": "api-security-best-practices", + "summary": "Secure API design patterns." + }, + { + "id": "auth-implementation-patterns", + "summary": "JWT, OAuth2, session management." + }, + { + "id": "backend-security-coder", + "summary": "Secure backend coding practices." + }, + { + "id": "frontend-security-coder", + "summary": "XSS prevention and client-side security." + }, + { + "id": "cc-skill-security-review", + "summary": "Security checklist for features." + }, + { + "id": "pci-compliance", + "summary": "Payment card security standards." + } + ] + }, + { + "id": "web-wizard", + "name": "Web Wizard", + "group": "๐ŸŒ Web Development", + "emoji": "๐ŸŒ", + "tagline": "The \"Web Wizard\" Pack", + "audience": "For building modern, high-performance web apps.", + "description": "For building modern, high-performance web apps.", + "skills": [ + { + "id": "frontend-design", + "summary": "UI guidelines and aesthetics." + }, + { + "id": "react-best-practices", + "summary": "React & Next.js performance optimization." + }, + { + "id": "react-patterns", + "summary": "Modern React patterns and principles." + }, + { + "id": "nextjs-best-practices", + "summary": "Next.js App Router patterns." + }, + { + "id": "tailwind-patterns", + "summary": "Tailwind CSS v4 styling superpowers." + }, + { + "id": "form-cro", + "summary": "Optimize your forms for conversion." + }, + { + "id": "seo-audit", + "summary": "Get found on Google." + } + ] + }, + { + "id": "web-designer", + "name": "Web Designer", + "group": "๐ŸŒ Web Development", + "emoji": "๐Ÿ–Œ๏ธ", + "tagline": "The \"Web Designer\" Pack", + "audience": "For pixel-perfect experiences.", + "description": "For pixel-perfect experiences.", + "skills": [ + { + "id": "ui-ux-pro-max", + "summary": "Premium design systems and tokens." + }, + { + "id": "frontend-design", + "summary": "The base layer of aesthetics." + }, + { + "id": "3d-web-experience", + "summary": "Three.js & React Three Fiber magic." + }, + { + "id": "canvas-design", + "summary": "Static visuals and posters." + }, + { + "id": "mobile-design", + "summary": "Mobile-first design principles." + }, + { + "id": "scroll-experience", + "summary": "Immersive scroll-driven experiences." + } + ] + }, + { + "id": "full-stack-developer", + "name": "Full-Stack Developer", + "group": "๐ŸŒ Web Development", + "emoji": "โšก", + "tagline": "The \"Full-Stack Developer\" Pack", + "audience": "For end-to-end web application development.", + "description": "For end-to-end web application development.", + "skills": [ + { + "id": "senior-fullstack", + "summary": "Complete fullstack development guide." + }, + { + "id": "frontend-developer", + "summary": "React 19+ and Next.js 15+ expertise." + }, + { + "id": "backend-dev-guidelines", + "summary": "Node.js/Express/TypeScript patterns." + }, + { + "id": "api-patterns", + "summary": "REST vs GraphQL vs tRPC selection." + }, + { + "id": "database-design", + "summary": "Schema design and ORM selection." + }, + { + "id": "stripe-integration", + "summary": "Payments and subscriptions." + } + ] + }, + { + "id": "agent-architect", + "name": "Agent Architect", + "group": "๐Ÿค– AI & Agents", + "emoji": "๐Ÿค–", + "tagline": "The \"Agent Architect\" Pack", + "audience": "For building AI systems and autonomous agents.", + "description": "For building AI systems and autonomous agents.", + "skills": [ + { + "id": "agent-evaluation", + "summary": "Test and benchmark your agents." + }, + { + "id": "langgraph", + "summary": "Build stateful agent workflows." + }, + { + "id": "mcp-builder", + "summary": "Create your own MCP tools." + }, + { + "id": "prompt-engineering", + "summary": "Master the art of talking to LLMs." + }, + { + "id": "ai-agents-architect", + "summary": "Design autonomous AI agents." + }, + { + "id": "rag-engineer", + "summary": "Build RAG systems with vector search." + } + ] + }, + { + "id": "llm-application-developer", + "name": "LLM Application Developer", + "group": "๐Ÿค– AI & Agents", + "emoji": "๐Ÿง ", + "tagline": "The \"LLM Application Developer\" Pack", + "audience": "For building production LLM applications.", + "description": "For building production LLM applications.", + "skills": [ + { + "id": "llm-app-patterns", + "summary": "Production-ready LLM patterns." + }, + { + "id": "rag-implementation", + "summary": "Retrieval-Augmented Generation." + }, + { + "id": "prompt-caching", + "summary": "Cache strategies for LLM prompts." + }, + { + "id": "context-window-management", + "summary": "Manage LLM context efficiently." + }, + { + "id": "langfuse", + "summary": "LLM observability and tracing." + } + ] + }, + { + "id": "indie-game-dev", + "name": "Indie Game Dev", + "group": "๐ŸŽฎ Game Development", + "emoji": "๐ŸŽฎ", + "tagline": "The \"Indie Game Dev\" Pack", + "audience": "For building games with AI assistants.", + "description": "For building games with AI assistants.", + "skills": [ + { + "id": "game-development/game-design", + "summary": "Mechanics and loops." + }, + { + "id": "game-development/2d-games", + "summary": "Sprites and physics." + }, + { + "id": "game-development/3d-games", + "summary": "Models and shaders." + }, + { + "id": "unity-developer", + "summary": "Unity 6 LTS development." + }, + { + "id": "godot-gdscript-patterns", + "summary": "Godot 4 GDScript patterns." + }, + { + "id": "algorithmic-art", + "summary": "Generate assets with code." + } + ] + }, + { + "id": "python-pro", + "name": "Python Pro", + "group": "๐Ÿ Backend & Languages", + "emoji": "๐Ÿ", + "tagline": "The \"Python Pro\" Pack", + "audience": "For backend heavyweights and data scientists.", + "description": "For backend heavyweights and data scientists.", + "skills": [ + { + "id": "python-pro", + "summary": "Master Python 3.12+ with modern features." + }, + { + "id": "python-patterns", + "summary": "Idiomatic Python code." + }, + { + "id": "fastapi-pro", + "summary": "High-performance async APIs." + }, + { + "id": "fastapi-templates", + "summary": "Production-ready FastAPI projects." + }, + { + "id": "django-pro", + "summary": "The battery-included framework." + }, + { + "id": "python-testing-patterns", + "summary": "Comprehensive testing with pytest." + }, + { + "id": "async-python-patterns", + "summary": "Python asyncio mastery." + } + ] + }, + { + "id": "typescript-javascript", + "name": "TypeScript & JavaScript", + "group": "๐Ÿ Backend & Languages", + "emoji": "๐ŸŸฆ", + "tagline": "The \"TypeScript & JavaScript\" Pack", + "audience": "For modern web development.", + "description": "For modern web development.", + "skills": [ + { + "id": "typescript-expert", + "summary": "TypeScript mastery and advanced types." + }, + { + "id": "javascript-pro", + "summary": "Modern JavaScript with ES6+." + }, + { + "id": "react-best-practices", + "summary": "React performance optimization." + }, + { + "id": "nodejs-best-practices", + "summary": "Node.js development principles." + }, + { + "id": "nextjs-app-router-patterns", + "summary": "Next.js 14+ App Router." + } + ] + }, + { + "id": "systems-programming", + "name": "Systems Programming", + "group": "๐Ÿ Backend & Languages", + "emoji": "๐Ÿฆ€", + "tagline": "The \"Systems Programming\" Pack", + "audience": "For low-level and performance-critical code.", + "description": "For low-level and performance-critical code.", + "skills": [ + { + "id": "rust-pro", + "summary": "Rust 1.75+ with async patterns." + }, + { + "id": "go-concurrency-patterns", + "summary": "Go concurrency mastery." + }, + { + "id": "golang-pro", + "summary": "Go development expertise." + }, + { + "id": "memory-safety-patterns", + "summary": "Memory-safe programming." + }, + { + "id": "cpp-pro", + "summary": "Modern C++ development." + } + ] + }, + { + "id": "startup-founder", + "name": "Startup Founder", + "group": "๐Ÿฆ„ Product & Business", + "emoji": "๐Ÿฆ„", + "tagline": "The \"Startup Founder\" Pack", + "audience": "For building products, not just code.", + "description": "For building products, not just code.", + "skills": [ + { + "id": "product-manager-toolkit", + "summary": "RICE prioritization, PRD templates." + }, + { + "id": "competitive-landscape", + "summary": "Competitor analysis." + }, + { + "id": "competitor-alternatives", + "summary": "Create comparison pages." + }, + { + "id": "launch-strategy", + "summary": "Product launch planning." + }, + { + "id": "copywriting", + "summary": "Marketing copy that converts." + }, + { + "id": "stripe-integration", + "summary": "Get paid from day one." + } + ] + }, + { + "id": "business-analyst", + "name": "Business Analyst", + "group": "๐Ÿฆ„ Product & Business", + "emoji": "๐Ÿ“Š", + "tagline": "The \"Business Analyst\" Pack", + "audience": "For data-driven decision making.", + "description": "For data-driven decision making.", + "skills": [ + { + "id": "business-analyst", + "summary": "AI-powered analytics and KPIs." + }, + { + "id": "startup-metrics-framework", + "summary": "SaaS metrics and unit economics." + }, + { + "id": "startup-financial-modeling", + "summary": "3-5 year financial projections." + }, + { + "id": "market-sizing-analysis", + "summary": "TAM/SAM/SOM calculations." + }, + { + "id": "kpi-dashboard-design", + "summary": "Effective KPI dashboards." + } + ] + }, + { + "id": "marketing-growth", + "name": "Marketing & Growth", + "group": "๐Ÿฆ„ Product & Business", + "emoji": "๐Ÿ“ˆ", + "tagline": "The \"Marketing & Growth\" Pack", + "audience": "For driving user acquisition and retention.", + "description": "For driving user acquisition and retention.", + "skills": [ + { + "id": "content-creator", + "summary": "SEO-optimized marketing content." + }, + { + "id": "seo-audit", + "summary": "Technical SEO health checks." + }, + { + "id": "programmatic-seo", + "summary": "Create pages at scale." + }, + { + "id": "analytics-tracking", + "summary": "Set up GA4/PostHog correctly." + }, + { + "id": "ab-test-setup", + "summary": "Validated learning experiments." + }, + { + "id": "email-sequence", + "summary": "Automated email campaigns." + } + ] + }, + { + "id": "devops-cloud", + "name": "DevOps & Cloud", + "group": "DevOps & Infrastructure", + "emoji": "๐ŸŒง๏ธ", + "tagline": "The \"DevOps & Cloud\" Pack", + "audience": "For infrastructure and scaling.", + "description": "For infrastructure and scaling.", + "skills": [ + { + "id": "docker-expert", + "summary": "Master containers and multi-stage builds." + }, + { + "id": "aws-serverless", + "summary": "Serverless on AWS (Lambda, DynamoDB)." + }, + { + "id": "kubernetes-architect", + "summary": "K8s architecture and GitOps." + }, + { + "id": "terraform-specialist", + "summary": "Infrastructure as Code mastery." + }, + { + "id": "environment-setup-guide", + "summary": "Standardization for teams." + }, + { + "id": "deployment-procedures", + "summary": "Safe rollout strategies." + }, + { + "id": "bash-linux", + "summary": "Terminal wizardry." + } + ] + }, + { + "id": "observability-monitoring", + "name": "Observability & Monitoring", + "group": "DevOps & Infrastructure", + "emoji": "๐Ÿ“Š", + "tagline": "The \"Observability & Monitoring\" Pack", + "audience": "For production reliability.", + "description": "For production reliability.", + "skills": [ + { + "id": "observability-engineer", + "summary": "Comprehensive monitoring systems." + }, + { + "id": "distributed-tracing", + "summary": "Track requests across microservices." + }, + { + "id": "slo-implementation", + "summary": "Service Level Objectives." + }, + { + "id": "incident-responder", + "summary": "Rapid incident response." + }, + { + "id": "postmortem-writing", + "summary": "Blameless postmortems." + }, + { + "id": "performance-engineer", + "summary": "Application performance optimization." + } + ] + }, + { + "id": "data-analytics", + "name": "Data & Analytics", + "group": "๐Ÿ“Š Data & Analytics", + "emoji": "๐Ÿ“Š", + "tagline": "The \"Data & Analytics\" Pack", + "audience": "For making sense of the numbers.", + "description": "For making sense of the numbers.", + "skills": [ + { + "id": "analytics-tracking", + "summary": "Set up GA4/PostHog correctly." + }, + { + "id": "claude-d3js-skill", + "summary": "Beautiful custom visualizations with D3.js." + }, + { + "id": "sql-pro", + "summary": "Modern SQL with cloud-native databases." + }, + { + "id": "postgres-best-practices", + "summary": "Postgres optimization." + }, + { + "id": "ab-test-setup", + "summary": "Validated learning." + }, + { + "id": "database-architect", + "summary": "Database design from scratch." + } + ] + }, + { + "id": "data-engineering", + "name": "Data Engineering", + "group": "๐Ÿ“Š Data & Analytics", + "emoji": "๐Ÿ”„", + "tagline": "The \"Data Engineering\" Pack", + "audience": "For building data pipelines.", + "description": "For building data pipelines.", + "skills": [ + { + "id": "data-engineer", + "summary": "Data pipeline architecture." + }, + { + "id": "airflow-dag-patterns", + "summary": "Apache Airflow DAGs." + }, + { + "id": "dbt-transformation-patterns", + "summary": "Analytics engineering." + }, + { + "id": "vector-database-engineer", + "summary": "Vector databases for RAG." + }, + { + "id": "embedding-strategies", + "summary": "Embedding model selection." + } + ] + }, + { + "id": "creative-director", + "name": "Creative Director", + "group": "๐ŸŽจ Creative & Content", + "emoji": "๐ŸŽจ", + "tagline": "The \"Creative Director\" Pack", + "audience": "For visuals, content, and branding.", + "description": "For visuals, content, and branding.", + "skills": [ + { + "id": "canvas-design", + "summary": "Generate posters and diagrams." + }, + { + "id": "frontend-design", + "summary": "UI aesthetics." + }, + { + "id": "content-creator", + "summary": "SEO-optimized blog posts." + }, + { + "id": "copy-editing", + "summary": "Polish your prose." + }, + { + "id": "algorithmic-art", + "summary": "Code-generated masterpieces." + }, + { + "id": "interactive-portfolio", + "summary": "Portfolios that land jobs." + } + ] + }, + { + "id": "qa-testing", + "name": "QA & Testing", + "group": "๐Ÿž Quality Assurance", + "emoji": "๐Ÿž", + "tagline": "The \"QA & Testing\" Pack", + "audience": "For breaking things before users do.", + "description": "For breaking things before users do.", + "skills": [ + { + "id": "test-driven-development", + "summary": "Red, Green, Refactor." + }, + { + "id": "systematic-debugging", + "summary": "Debug like Sherlock Holmes." + }, + { + "id": "browser-automation", + "summary": "End-to-end testing with Playwright." + }, + { + "id": "e2e-testing-patterns", + "summary": "Reliable E2E test suites." + }, + { + "id": "ab-test-setup", + "summary": "Validated experiments." + }, + { + "id": "code-review-checklist", + "summary": "Catch bugs in PRs." + }, + { + "id": "test-fixing", + "summary": "Fix failing tests systematically." + } + ] + }, + { + "id": "mobile-developer", + "name": "Mobile Developer", + "group": "๐Ÿ”ง Specialized Packs", + "emoji": "๐Ÿ“ฑ", + "tagline": "The \"Mobile Developer\" Pack", + "audience": "For iOS, Android, and cross-platform apps.", + "description": "For iOS, Android, and cross-platform apps.", + "skills": [ + { + "id": "mobile-developer", + "summary": "Cross-platform mobile development." + }, + { + "id": "react-native-architecture", + "summary": "React Native with Expo." + }, + { + "id": "flutter-expert", + "summary": "Flutter multi-platform apps." + }, + { + "id": "ios-developer", + "summary": "iOS development with Swift." + }, + { + "id": "app-store-optimization", + "summary": "ASO for App Store and Play Store." + } + ] + }, + { + "id": "integration-apis", + "name": "Integration & APIs", + "group": "๐Ÿ”ง Specialized Packs", + "emoji": "๐Ÿ”—", + "tagline": "The \"Integration & APIs\" Pack", + "audience": "For connecting services and building integrations.", + "description": "For connecting services and building integrations.", + "skills": [ + { + "id": "stripe-integration", + "summary": "Payments and subscriptions." + }, + { + "id": "twilio-communications", + "summary": "SMS, voice, WhatsApp." + }, + { + "id": "hubspot-integration", + "summary": "CRM integration." + }, + { + "id": "plaid-fintech", + "summary": "Bank account linking and ACH." + }, + { + "id": "algolia-search", + "summary": "Search implementation." + } + ] + }, + { + "id": "architecture-design", + "name": "Architecture & Design", + "group": "๐Ÿ”ง Specialized Packs", + "emoji": "๐ŸŽฏ", + "tagline": "The \"Architecture & Design\" Pack", + "audience": "For system design and technical decisions.", + "description": "For system design and technical decisions.", + "skills": [ + { + "id": "senior-architect", + "summary": "Comprehensive software architecture." + }, + { + "id": "architecture-patterns", + "summary": "Clean Architecture, DDD, Hexagonal." + }, + { + "id": "microservices-patterns", + "summary": "Microservices architecture." + }, + { + "id": "event-sourcing-architect", + "summary": "Event sourcing and CQRS." + }, + { + "id": "architecture-decision-records", + "summary": "Document technical decisions." + } + ] + }, + { + "id": "ddd-evented-architecture", + "name": "DDD & Evented Architecture", + "group": "๐Ÿ”ง Specialized Packs", + "emoji": "๐Ÿงฑ", + "tagline": "The \"DDD & Evented Architecture\" Pack", + "audience": "For teams modeling complex domains and evolving toward evented systems.", + "description": "For teams modeling complex domains and evolving toward evented systems.", + "skills": [ + { + "id": "domain-driven-design", + "summary": "Route DDD work from strategic modeling to implementation patterns." + }, + { + "id": "ddd-strategic-design", + "summary": "Subdomains, bounded contexts, and ubiquitous language." + }, + { + "id": "ddd-context-mapping", + "summary": "Cross-context integration and anti-corruption boundaries." + }, + { + "id": "ddd-tactical-patterns", + "summary": "Aggregates, value objects, repositories, and domain events." + }, + { + "id": "cqrs-implementation", + "summary": "Read/write model separation." + }, + { + "id": "event-store-design", + "summary": "Event persistence and replay architecture." + }, + { + "id": "saga-orchestration", + "summary": "Cross-context long-running transaction coordination." + }, + { + "id": "projection-patterns", + "summary": "Materialized read models from event streams." + } + ] + }, + { + "id": "automation-builder", + "name": "Automation Builder", + "group": "๐Ÿ”ง Specialized Packs", + "emoji": "๐Ÿค–", + "tagline": "The \"Automation Builder\" Pack", + "audience": "For connecting tools and building repeatable automated workflows.", + "description": "For connecting tools and building repeatable automated workflows.", + "skills": [ + { + "id": "workflow-automation", + "summary": "Design durable automation flows for AI and business systems." + }, + { + "id": "mcp-builder", + "summary": "Create tool interfaces agents can use reliably." + }, + { + "id": "make-automation", + "summary": "Build automations in Make/Integromat." + }, + { + "id": "airtable-automation", + "summary": "Automate Airtable records, bases, and views." + }, + { + "id": "notion-automation", + "summary": "Automate Notion pages, databases, and blocks." + }, + { + "id": "slack-automation", + "summary": "Automate Slack messaging and channel workflows." + }, + { + "id": "googlesheets-automation", + "summary": "Automate spreadsheet updates and data operations." + } + ] + }, + { + "id": "revops-crm-automation", + "name": "RevOps & CRM Automation", + "group": "๐Ÿ”ง Specialized Packs", + "emoji": "๐Ÿ’ผ", + "tagline": "The \"RevOps & CRM Automation\" Pack", + "audience": "For revenue operations, support handoffs, and CRM-heavy automation.", + "description": "For revenue operations, support handoffs, and CRM-heavy automation.", + "skills": [ + { + "id": "hubspot-automation", + "summary": "Automate contacts, companies, deals, and tickets." + }, + { + "id": "sendgrid-automation", + "summary": "Automate email sends, contacts, and templates." + }, + { + "id": "zendesk-automation", + "summary": "Automate support tickets and reply workflows." + }, + { + "id": "google-calendar-automation", + "summary": "Schedule events and manage availability." + }, + { + "id": "outlook-calendar-automation", + "summary": "Automate Outlook meetings and invitations." + }, + { + "id": "stripe-automation", + "summary": "Automate billing, invoices, and subscriptions." + }, + { + "id": "shopify-automation", + "summary": "Automate products, orders, customers, and inventory." + } + ] + }, + { + "id": "commerce-payments", + "name": "Commerce & Payments", + "group": "๐Ÿ”ง Specialized Packs", + "emoji": "๐Ÿ’ณ", + "tagline": "The \"Commerce & Payments\" Pack", + "audience": "For monetization, payments, and commerce workflows.", + "description": "For monetization, payments, and commerce workflows.", + "skills": [ + { + "id": "stripe-integration", + "summary": "Build robust checkout, subscription, and webhook flows." + }, + { + "id": "paypal-integration", + "summary": "Integrate PayPal payments and related flows." + }, + { + "id": "plaid-fintech", + "summary": "Link bank accounts and handle ACH-related use cases." + }, + { + "id": "hubspot-integration", + "summary": "Connect CRM data into product and revenue workflows." + }, + { + "id": "algolia-search", + "summary": "Add search and discovery to commerce experiences." + }, + { + "id": "monetization", + "summary": "Design pricing and monetization systems deliberately." + } + ] + }, + { + "id": "odoo-erp", + "name": "Odoo ERP", + "group": "๐Ÿ”ง Specialized Packs", + "emoji": "๐Ÿข", + "tagline": "The \"Odoo ERP\" Pack", + "audience": "For teams building or operating around Odoo-based business systems.", + "description": "For teams building or operating around Odoo-based business systems.", + "skills": [ + { + "id": "odoo-module-developer", + "summary": "Create custom Odoo modules cleanly." + }, + { + "id": "odoo-orm-expert", + "summary": "Work effectively with Odoo ORM patterns and performance." + }, + { + "id": "odoo-sales-crm-expert", + "summary": "Optimize sales pipelines, leads, and forecasting." + }, + { + "id": "odoo-ecommerce-configurator", + "summary": "Configure storefront, catalog, and order flows." + }, + { + "id": "odoo-performance-tuner", + "summary": "Diagnose and improve slow Odoo instances." + }, + { + "id": "odoo-security-rules", + "summary": "Apply secure access controls and rule design." + }, + { + "id": "odoo-docker-deployment", + "summary": "Deploy and run Odoo in Docker-based environments." + } + ] + }, + { + "id": "azure-ai-cloud", + "name": "Azure AI & Cloud", + "group": "๐Ÿ”ง Specialized Packs", + "emoji": "โ˜๏ธ", + "tagline": "The \"Azure AI & Cloud\" Pack", + "audience": "For building on Azure across cloud, AI, and platform services.", + "description": "For building on Azure across cloud, AI, and platform services.", + "skills": [ + { + "id": "azd-deployment", + "summary": "Ship Azure apps with Azure Developer CLI workflows." + }, + { + "id": "azure-functions", + "summary": "Build serverless workloads with Azure Functions." + }, + { + "id": "azure-ai-openai-dotnet", + "summary": "Use Azure OpenAI from .NET applications." + }, + { + "id": "azure-search-documents-py", + "summary": "Build search, hybrid search, and indexing in Python." + }, + { + "id": "azure-identity-py", + "summary": "Handle Azure authentication flows in Python services." + }, + { + "id": "azure-monitor-opentelemetry-ts", + "summary": "Add telemetry and tracing from TypeScript apps." + } + ] + }, + { + "id": "expo-react-native", + "name": "Expo & React Native", + "group": "๐Ÿ”ง Specialized Packs", + "emoji": "๐Ÿ“ฒ", + "tagline": "The \"Expo & React Native\" Pack", + "audience": "For shipping mobile apps with Expo and React Native.", + "description": "For shipping mobile apps with Expo and React Native.", + "skills": [ + { + "id": "react-native-architecture", + "summary": "Structure production React Native apps well." + }, + { + "id": "expo-api-routes", + "summary": "Build API routes in Expo Router and EAS Hosting." + }, + { + "id": "expo-dev-client", + "summary": "Build and distribute Expo development clients." + }, + { + "id": "expo-tailwind-setup", + "summary": "Set up Tailwind and NativeWind in Expo apps." + }, + { + "id": "expo-cicd-workflows", + "summary": "Automate builds and releases with EAS workflows." + }, + { + "id": "expo-deployment", + "summary": "Deploy Expo apps and manage release flow." + }, + { + "id": "app-store-optimization", + "summary": "Improve App Store and Play Store discoverability." + } + ] + }, + { + "id": "apple-platform-design", + "name": "Apple Platform Design", + "group": "๐Ÿ”ง Specialized Packs", + "emoji": "๐ŸŽ", + "tagline": "The \"Apple Platform Design\" Pack", + "audience": "For teams designing native-feeling Apple platform experiences.", + "description": "For teams designing native-feeling Apple platform experiences.", + "skills": [ + { + "id": "hig-foundations", + "summary": "Learn the core Apple Human Interface Guidelines." + }, + { + "id": "hig-patterns", + "summary": "Apply Apple interaction and UX patterns correctly." + }, + { + "id": "hig-components-layout", + "summary": "Use Apple layout and navigation components well." + }, + { + "id": "hig-inputs", + "summary": "Design for gestures, keyboards, Pencil, focus, and controllers." + }, + { + "id": "hig-components-system", + "summary": "Work with widgets, live activities, and system surfaces." + }, + { + "id": "hig-platforms", + "summary": "Adapt experiences across Apple device families." + } + ] + }, + { + "id": "makepad-builder", + "name": "Makepad Builder", + "group": "๐Ÿ”ง Specialized Packs", + "emoji": "๐Ÿงฉ", + "tagline": "The \"Makepad Builder\" Pack", + "audience": "For building UI-heavy apps with the Makepad ecosystem.", + "description": "For building UI-heavy apps with the Makepad ecosystem.", + "skills": [ + { + "id": "makepad-basics", + "summary": "Start with Makepad fundamentals and mental model." + }, + { + "id": "makepad-layout", + "summary": "Handle sizing, flow, alignment, and layout composition." + }, + { + "id": "makepad-widgets", + "summary": "Build interfaces from Makepad widgets." + }, + { + "id": "makepad-event-action", + "summary": "Wire interaction and event handling correctly." + }, + { + "id": "makepad-shaders", + "summary": "Create GPU-driven visual effects and custom drawing." + }, + { + "id": "makepad-deployment", + "summary": "Package and ship Makepad projects." + } + ] + }, + { + "id": "seo-specialist", + "name": "SEO Specialist", + "group": "๐Ÿ”ง Specialized Packs", + "emoji": "๐Ÿ”Ž", + "tagline": "The \"SEO Specialist\" Pack", + "audience": "For technical SEO, content structure, and search growth.", + "description": "For technical SEO, content structure, and search growth.", + "skills": [ + { + "id": "seo-fundamentals", + "summary": "Build from sound SEO principles and search constraints." + }, + { + "id": "seo-content-planner", + "summary": "Plan clusters, calendars, and content gaps." + }, + { + "id": "seo-content-writer", + "summary": "Produce search-aware content drafts with intent alignment." + }, + { + "id": "seo-structure-architect", + "summary": "Improve hierarchy, internal links, and structure." + }, + { + "id": "seo-cannibalization-detector", + "summary": "Find overlapping pages competing for the same intent." + }, + { + "id": "seo-content-auditor", + "summary": "Audit existing content quality and optimization gaps." + }, + { + "id": "schema-markup", + "summary": "Add structured data to support richer search results." + } + ] + }, + { + "id": "documents-presentations", + "name": "Documents & Presentations", + "group": "๐Ÿ”ง Specialized Packs", + "emoji": "๐Ÿ“„", + "tagline": "The \"Documents & Presentations\" Pack", + "audience": "For document-heavy workflows, spreadsheets, PDFs, and presentations.", + "description": "For document-heavy workflows, spreadsheets, PDFs, and presentations.", + "skills": [ + { + "id": "office-productivity", + "summary": "Coordinate document, spreadsheet, and presentation workflows." + }, + { + "id": "docx-official", + "summary": "Create and edit Word-compatible documents." + }, + { + "id": "pptx-official", + "summary": "Create and edit PowerPoint-compatible presentations." + }, + { + "id": "xlsx-official", + "summary": "Create and analyze spreadsheet files with formulas and formatting." + }, + { + "id": "pdf-official", + "summary": "Extract, generate, and manipulate PDFs programmatically." + }, + { + "id": "google-slides-automation", + "summary": "Automate presentation updates in Google Slides." + }, + { + "id": "google-sheets-automation", + "summary": "Automate reads and writes in Google Sheets." + } + ] + }, + { + "id": "oss-maintainer", + "name": "OSS Maintainer", + "group": "๐Ÿงฐ Maintainer & OSS", + "emoji": "๐Ÿ› ๏ธ", + "tagline": "The \"OSS Maintainer\" Pack", + "audience": "For shipping clean changes in public repositories.", + "description": "For shipping clean changes in public repositories.", + "skills": [ + { + "id": "commit", + "summary": "High-quality conventional commits." + }, + { + "id": "create-pr", + "summary": "PR creation with review-ready context." + }, + { + "id": "requesting-code-review", + "summary": "Ask for targeted, high-signal reviews." + }, + { + "id": "receiving-code-review", + "summary": "Apply feedback with technical rigor." + }, + { + "id": "changelog-automation", + "summary": "Keep release notes and changelogs consistent." + }, + { + "id": "git-advanced-workflows", + "summary": "Rebase, cherry-pick, bisect, recovery." + }, + { + "id": "documentation-templates", + "summary": "Standardize docs and handoffs." + } + ] + }, + { + "id": "skill-author", + "name": "Skill Author", + "group": "๐Ÿงฐ Maintainer & OSS", + "emoji": "๐Ÿงฑ", + "tagline": "The \"Skill Author\" Pack", + "audience": "For creating and maintaining high-quality SKILL.md assets.", + "description": "For creating and maintaining high-quality SKILL.md assets.", + "skills": [ + { + "id": "skill-creator", + "summary": "Design effective new skills." + }, + { + "id": "skill-developer", + "summary": "Implement triggers, hooks, and skill lifecycle." + }, + { + "id": "writing-skills", + "summary": "Improve clarity and structure of skill instructions." + }, + { + "id": "documentation-generation-doc-generate", + "summary": "Generate maintainable technical docs." + }, + { + "id": "lint-and-validate", + "summary": "Validate quality after edits." + }, + { + "id": "verification-before-completion", + "summary": "Confirm changes before claiming done." + } + ] + } + ] +} diff --git a/docs/users/bundles.md b/docs/users/bundles.md index 7f34a990..1a066c01 100644 --- a/docs/users/bundles.md +++ b/docs/users/bundles.md @@ -4,11 +4,11 @@ > These packs are curated starter recommendations for humans. Generated bundle ids in `data/bundles.json` are broader catalog/workflow groupings and do not need to map 1:1 to the editorial packs below. -> **Important:** bundles are not invokable mega-skills such as `@web-wizard` or `/essentials-bundle`. Use the individual skills listed in the pack, or use the activation scripts if you want only that bundle's skills active in your live Antigravity directory. +> **Important:** bundles are installable plugin subsets and activation presets, not invokable mega-skills such as `@web-wizard` or `/essentials-bundle`. Use the individual skills listed in the pack, install the bundle as a dedicated marketplace plugin, or use the activation scripts if you want only that bundle's skills active in your live Antigravity directory. ## Quick Start -1. **Install the repository:** +1. **Install the repository or bundle plugin:** ```bash npx antigravity-awesome-skills @@ -18,13 +18,13 @@ 2. **Choose your bundle** from the list below based on your role or interests. -3. **Use skills** by referencing them in your AI assistant: - - Claude Code: `>> /skill-name help me...` - - Cursor: `@skill-name in chat` +3. **Use bundle plugins or individual skills** in your AI assistant: + - Claude Code: install the matching marketplace bundle plugin, or invoke `>> /skill-name help me...` + - Codex CLI / Codex app: install the matching bundle plugin where plugin marketplaces are available, or invoke `Use skill-name...` + - Cursor: `@skill-name` in chat - Gemini CLI: `Use skill-name...` - - Codex CLI: `Use skill-name...` -If you want a bundle to behave like a focused active subset instead of a reading list, use: +If you want a bundle to behave like a focused active subset instead of a full install, use: - macOS/Linux: `./scripts/activate-skills.sh --clear Essentials` - macOS/Linux: `./scripts/activate-skills.sh --clear "Web Wizard"` @@ -44,6 +44,7 @@ _For everyone. Install these first._ - [`kaizen`](../../skills/kaizen/): Continuous improvement mindset. - [`systematic-debugging`](../../skills/systematic-debugging/): Debug like a pro. + --- ## Security & Compliance @@ -71,6 +72,7 @@ _For building secure applications._ - [`cc-skill-security-review`](../../skills/cc-skill-security-review/): Security checklist for features. - [`pci-compliance`](../../skills/pci-compliance/): Payment card security standards. + --- ## ๐ŸŒ Web Development @@ -109,6 +111,7 @@ _For end-to-end web application development._ - [`database-design`](../../skills/database-design/): Schema design and ORM selection. - [`stripe-integration`](../../skills/stripe-integration/): Payments and subscriptions. + --- ## ๐Ÿค– AI & Agents @@ -134,6 +137,7 @@ _For building production LLM applications._ - [`context-window-management`](../../skills/context-window-management/): Manage LLM context efficiently. - [`langfuse`](../../skills/langfuse/): LLM observability and tracing. + --- ## ๐ŸŽฎ Game Development @@ -149,6 +153,7 @@ _For building games with AI assistants._ - [`godot-gdscript-patterns`](../../skills/godot-gdscript-patterns/): Godot 4 GDScript patterns. - [`algorithmic-art`](../../skills/algorithmic-art/): Generate assets with code. + --- ## ๐Ÿ Backend & Languages @@ -185,6 +190,7 @@ _For low-level and performance-critical code._ - [`memory-safety-patterns`](../../skills/memory-safety-patterns/): Memory-safe programming. - [`cpp-pro`](../../skills/cpp-pro/): Modern C++ development. + --- ## ๐Ÿฆ„ Product & Business @@ -221,6 +227,7 @@ _For driving user acquisition and retention._ - [`ab-test-setup`](../../skills/ab-test-setup/): Validated learning experiments. - [`email-sequence`](../../skills/email-sequence/): Automated email campaigns. + --- ## DevOps & Infrastructure @@ -248,6 +255,7 @@ _For production reliability._ - [`postmortem-writing`](../../skills/postmortem-writing/): Blameless postmortems. - [`performance-engineer`](../../skills/performance-engineer/): Application performance optimization. + --- ## ๐Ÿ“Š Data & Analytics @@ -273,6 +281,7 @@ _For building data pipelines._ - [`vector-database-engineer`](../../skills/vector-database-engineer/): Vector databases for RAG. - [`embedding-strategies`](../../skills/embedding-strategies/): Embedding model selection. + --- ## ๐ŸŽจ Creative & Content @@ -288,6 +297,7 @@ _For visuals, content, and branding._ - [`algorithmic-art`](../../skills/algorithmic-art/): Code-generated masterpieces. - [`interactive-portfolio`](../../skills/interactive-portfolio/): Portfolios that land jobs. + --- ## ๐Ÿž Quality Assurance @@ -304,6 +314,7 @@ _For breaking things before users do._ - [`code-review-checklist`](../../skills/code-review-checklist/): Catch bugs in PRs. - [`test-fixing`](../../skills/test-fixing/): Fix failing tests systematically. + --- ## ๐Ÿ”ง Specialized Packs @@ -467,6 +478,7 @@ _For document-heavy workflows, spreadsheets, PDFs, and presentations._ - [`google-slides-automation`](../../skills/google-slides-automation/): Automate presentation updates in Google Slides. - [`google-sheets-automation`](../../skills/google-sheets-automation/): Automate reads and writes in Google Sheets. + --- ## ๐Ÿงฐ Maintainer & OSS @@ -494,8 +506,6 @@ _For creating and maintaining high-quality SKILL.md assets._ - [`lint-and-validate`](../../skills/lint-and-validate/): Validate quality after edits. - [`verification-before-completion`](../../skills/verification-before-completion/): Confirm changes before claiming done. ---- - ## ๐Ÿ“š How to Use Bundles ### 1) Pick by immediate goal @@ -510,10 +520,10 @@ Pick the minimum set for your current milestone. Expand only when you hit a real ### 3) Invoke skills consistently -- **Claude Code**: `>> /skill-name help me...` +- **Claude Code**: install a bundle plugin or use `>> /skill-name help me...` +- **Codex CLI**: install a bundle plugin where marketplaces are available, or use `Use skill-name...` - **Cursor**: `@skill-name` in chat - **Gemini CLI**: `Use skill-name...` -- **Codex CLI**: `Use skill-name...` ### 4) Build your personal shortlist @@ -587,4 +597,4 @@ Found a skill that should be in a bundle? Or want to create a new bundle? [Open --- -_Last updated: March 2026 | Total Skills: 1,328+ | Total Bundles: 36_ +_Last updated: March 2026 | Total Skills: 1,328+ | Total Bundles: 37_ diff --git a/docs/users/claude-code-skills.md b/docs/users/claude-code-skills.md index 0c73d861..e9f271e5 100644 --- a/docs/users/claude-code-skills.md +++ b/docs/users/claude-code-skills.md @@ -12,6 +12,7 @@ Install the library into Claude Code, then invoke focused skills directly in the - It includes 1,328+ skills instead of a narrow single-domain starter pack. - It supports the standard `.claude/skills/` path and the Claude Code plugin marketplace flow. +- It also ships generated bundle plugins so teams can install focused packs like `Essentials` or `Security Developer` from the marketplace metadata. - It includes onboarding docs, bundles, and workflows so new users do not need to guess where to begin. - It covers both everyday engineering tasks and specialized work like security reviews, infrastructure, product planning, and documentation. diff --git a/docs/users/codex-cli-skills.md b/docs/users/codex-cli-skills.md index 63ea34eb..453b925b 100644 --- a/docs/users/codex-cli-skills.md +++ b/docs/users/codex-cli-skills.md @@ -27,6 +27,8 @@ npx antigravity-awesome-skills --codex If you prefer a plugin-style Codex integration, this repository also ships repo-local plugin metadata in `.agents/plugins/marketplace.json` and `plugins/antigravity-awesome-skills/.codex-plugin/plugin.json`. +It also generates bundle-specific Codex plugins so you can install a curated pack such as `Essentials` or `Web Wizard` as a marketplace plugin instead of loading the full library. + ### Verify the install ```bash diff --git a/docs/users/faq.md b/docs/users/faq.md index 7b4d8e36..d6f67d86 100644 --- a/docs/users/faq.md +++ b/docs/users/faq.md @@ -221,6 +221,7 @@ No. Bundles are curated lists of skills, not standalone invokable mega-skills. Use them in one of these two ways: - pick individual skills from the bundle and invoke those directly +- install the dedicated Claude Code or Codex bundle plugin if you want a marketplace-scoped subset - use the activation scripts if you want only that bundle's skills active in Antigravity Examples: diff --git a/docs/users/usage.md b/docs/users/usage.md index 0c5907f9..112db8bb 100644 --- a/docs/users/usage.md +++ b/docs/users/usage.md @@ -20,28 +20,28 @@ Think of it like installing a toolbox. You have all the tools now, but you need --- -## Step 1: Understanding "Bundles" (This is NOT Another Install!) +## Step 1: Understanding "Bundles" (Recommendations or Focused Installs) **Common confusion:** "Do I need to download each skill separately?" -**Answer: NO!** Here's what bundles actually are: +**Answer: NO!** You do not need to download each skill separately. Here's what bundles actually are: ### What Bundles Are -Bundles are **recommended lists** of skills grouped by role. They help you decide which skills to start using. +Bundles are **curated groups** of skills organized by role. They help you decide which skills to start using, and they can also be exposed as focused marketplace plugins for Claude Code and Codex. **Analogy:** - You installed a toolbox with 1,328+ tools (โœ… done) - Bundles are like **labeled organizer trays** saying: "If you're a carpenter, start with these 10 tools" -- You don't install bundlesโ€”you **pick skills from them** +- You can either **pick skills from the tray** or install that tray as a focused marketplace bundle plugin ### What Bundles Are NOT -โŒ Separate installations -โŒ Different download commands -โŒ Something most users need to activate during normal install +โŒ Separate skill downloads โŒ Invokable mega-skills like `@essentials` or `/web-wizard` +โŒ Something most users need to activate during normal install +โŒ A replacement for invoking the individual skills inside the bundle ### Example: The "Web Wizard" Bundle @@ -52,7 +52,7 @@ When you see the [Web Wizard bundle](bundles.md#-the-web-wizard-pack), it lists: - `tailwind-patterns` - etc. -These are **recommendations** for which skills a web developer should try first. They're already installedโ€”you just need to **use them in your prompts**. +These are **recommendations** for which skills a web developer should try first. If you have the full library installed, you just need to **use them in your prompts**. If you prefer a narrower install surface, you can install the matching bundle plugin in Claude Code or Codex where plugin marketplaces are available. If you want only one bundle active at a time in Antigravity, use the activation scripts instead of trying to invoke the bundle name directly: diff --git a/package.json b/package.json index 0ddccadc..15e5738a 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,13 @@ "validate:references": "node tools/scripts/run-python.js tools/scripts/validate_references.py", "index": "node tools/scripts/run-python.js tools/scripts/generate_index.py", "readme": "node tools/scripts/run-python.js tools/scripts/update_readme.py", + "bundles:sync": "node tools/scripts/run-python.js tools/scripts/sync_editorial_bundles.py", + "bundles:check": "node tools/scripts/run-python.js tools/scripts/sync_editorial_bundles.py --check", "sync:metadata": "node tools/scripts/run-python.js tools/scripts/sync_repo_metadata.py", "sync:github-about": "node tools/scripts/run-python.js tools/scripts/sync_repo_metadata.py --apply-github-about", "sync:contributors": "node tools/scripts/run-python.js tools/scripts/sync_contributors.py", "sync:web-assets": "npm run app:setup && cd apps/web-app && npm run generate:sitemap", - "chain": "npm run validate && npm run index && npm run sync:metadata", + "chain": "npm run validate && npm run index && npm run bundles:sync && npm run sync:metadata", "sync:all": "npm run chain", "sync:release-state": "npm run chain && npm run catalog && npm run sync:web-assets && npm run audit:consistency && npm run check:warning-budget", "sync:repo-state": "npm run chain && npm run catalog && npm run sync:web-assets && npm run sync:contributors && npm run audit:consistency && npm run check:warning-budget", diff --git a/plugins/antigravity-bundle-agent-architect/.codex-plugin/plugin.json b/plugins/antigravity-bundle-agent-architect/.codex-plugin/plugin.json new file mode 100644 index 00000000..880e1ba1 --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-agent-architect", + "version": "8.10.0", + "description": "Install the \"Agent Architect\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "agent-architect", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Agent Architect", + "shortDescription": "AI & Agents ยท 6 curated skills", + "longDescription": "For building AI systems and autonomous agents. Covers Agent Evaluation, LangGraph, and 4 more skills.", + "developerName": "sickn33 and contributors", + "category": "AI & Agents", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-agent-architect/skills/agent-evaluation/SKILL.md b/plugins/antigravity-bundle-agent-architect/skills/agent-evaluation/SKILL.md new file mode 100644 index 00000000..5d134194 --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/skills/agent-evaluation/SKILL.md @@ -0,0 +1,69 @@ +--- +name: agent-evaluation +description: "You're a quality engineer who has seen agents that aced benchmarks fail spectacularly in production. You've learned that evaluating LLM agents is fundamentally different from testing traditional softwareโ€”the same input can produce different outputs, and \"correct\" often has no single answer." +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# Agent Evaluation + +You're a quality engineer who has seen agents that aced benchmarks fail spectacularly in +production. You've learned that evaluating LLM agents is fundamentally different from +testing traditional softwareโ€”the same input can produce different outputs, and "correct" +often has no single answer. + +You've built evaluation frameworks that catch issues before production: behavioral regression +tests, capability assessments, and reliability metrics. You understand that the goal isn't +100% test pass rateโ€”it + +## Capabilities + +- agent-testing +- benchmark-design +- capability-assessment +- reliability-metrics +- regression-testing + +## Requirements + +- testing-fundamentals +- llm-fundamentals + +## Patterns + +### Statistical Test Evaluation + +Run tests multiple times and analyze result distributions + +### Behavioral Contract Testing + +Define and test agent behavioral invariants + +### Adversarial Testing + +Actively try to break agent behavior + +## Anti-Patterns + +### โŒ Single-Run Testing + +### โŒ Only Happy Path Tests + +### โŒ Output String Matching + +## โš ๏ธ Sharp Edges + +| Issue | Severity | Solution | +|-------|----------|----------| +| Agent scores well on benchmarks but fails in production | high | // Bridge benchmark and production evaluation | +| Same test passes sometimes, fails other times | high | // Handle flaky tests in LLM agent evaluation | +| Agent optimized for metric, not actual task | medium | // Multi-dimensional evaluation to prevent gaming | +| Test data accidentally used in training or prompts | critical | // Prevent data leakage in agent evaluation | + +## Related Skills + +Works well with: `multi-agent-orchestration`, `agent-communication`, `autonomous-agents` + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-agent-architect/skills/ai-agents-architect/SKILL.md b/plugins/antigravity-bundle-agent-architect/skills/ai-agents-architect/SKILL.md new file mode 100644 index 00000000..9d84edf3 --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/skills/ai-agents-architect/SKILL.md @@ -0,0 +1,96 @@ +--- +name: ai-agents-architect +description: "I build AI systems that can act autonomously while remaining controllable. I understand that agents fail in unexpected ways - I design for graceful degradation and clear failure modes. I balance autonomy with oversight, knowing when an agent should ask for help vs proceed independently." +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# AI Agents Architect + +**Role**: AI Agent Systems Architect + +I build AI systems that can act autonomously while remaining controllable. +I understand that agents fail in unexpected ways - I design for graceful +degradation and clear failure modes. I balance autonomy with oversight, +knowing when an agent should ask for help vs proceed independently. + +## Capabilities + +- Agent architecture design +- Tool and function calling +- Agent memory systems +- Planning and reasoning strategies +- Multi-agent orchestration +- Agent evaluation and debugging + +## Requirements + +- LLM API usage +- Understanding of function calling +- Basic prompt engineering + +## Patterns + +### ReAct Loop + +Reason-Act-Observe cycle for step-by-step execution + +```javascript +- Thought: reason about what to do next +- Action: select and invoke a tool +- Observation: process tool result +- Repeat until task complete or stuck +- Include max iteration limits +``` + +### Plan-and-Execute + +Plan first, then execute steps + +```javascript +- Planning phase: decompose task into steps +- Execution phase: execute each step +- Replanning: adjust plan based on results +- Separate planner and executor models possible +``` + +### Tool Registry + +Dynamic tool discovery and management + +```javascript +- Register tools with schema and examples +- Tool selector picks relevant tools for task +- Lazy loading for expensive tools +- Usage tracking for optimization +``` + +## Anti-Patterns + +### โŒ Unlimited Autonomy + +### โŒ Tool Overload + +### โŒ Memory Hoarding + +## โš ๏ธ Sharp Edges + +| Issue | Severity | Solution | +|-------|----------|----------| +| Agent loops without iteration limits | critical | Always set limits: | +| Vague or incomplete tool descriptions | high | Write complete tool specs: | +| Tool errors not surfaced to agent | high | Explicit error handling: | +| Storing everything in agent memory | medium | Selective memory: | +| Agent has too many tools | medium | Curate tools per task: | +| Using multiple agents when one would work | medium | Justify multi-agent: | +| Agent internals not logged or traceable | medium | Implement tracing: | +| Fragile parsing of agent outputs | medium | Robust output handling: | +| Agent workflows lost on crash or restart | high | Use durable execution (e.g. DBOS) to persist workflow state: | + +## Related Skills + +Works well with: `rag-engineer`, `prompt-engineer`, `backend`, `mcp-builder`, `dbos-python` + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-agent-architect/skills/langgraph/SKILL.md b/plugins/antigravity-bundle-agent-architect/skills/langgraph/SKILL.md new file mode 100644 index 00000000..76f76792 --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/skills/langgraph/SKILL.md @@ -0,0 +1,292 @@ +--- +name: langgraph +description: "You are an expert in building production-grade AI agents with LangGraph. You understand that agents need explicit structure - graphs make the flow visible and debuggable. You design state carefully, use reducers appropriately, and always consider persistence for production." +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# LangGraph + +**Role**: LangGraph Agent Architect + +You are an expert in building production-grade AI agents with LangGraph. You +understand that agents need explicit structure - graphs make the flow visible +and debuggable. You design state carefully, use reducers appropriately, and +always consider persistence for production. You know when cycles are needed +and how to prevent infinite loops. + +## Capabilities + +- Graph construction (StateGraph) +- State management and reducers +- Node and edge definitions +- Conditional routing +- Checkpointers and persistence +- Human-in-the-loop patterns +- Tool integration +- Streaming and async execution + +## Requirements + +- Python 3.9+ +- langgraph package +- LLM API access (OpenAI, Anthropic, etc.) +- Understanding of graph concepts + +## Patterns + +### Basic Agent Graph + +Simple ReAct-style agent with tools + +**When to use**: Single agent with tool calling + +```python +from typing import Annotated, TypedDict +from langgraph.graph import StateGraph, START, END +from langgraph.graph.message import add_messages +from langgraph.prebuilt import ToolNode +from langchain_openai import ChatOpenAI +from langchain_core.tools import tool + +# 1. Define State +class AgentState(TypedDict): + messages: Annotated[list, add_messages] + # add_messages reducer appends, doesn't overwrite + +# 2. Define Tools +@tool +def search(query: str) -> str: + """Search the web for information.""" + # Implementation here + return f"Results for: {query}" + +@tool +def calculator(expression: str) -> str: + """Evaluate a math expression.""" + return str(eval(expression)) + +tools = [search, calculator] + +# 3. Create LLM with tools +llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) + +# 4. Define Nodes +def agent(state: AgentState) -> dict: + """The agent node - calls LLM.""" + response = llm.invoke(state["messages"]) + return {"messages": [response]} + +# Tool node handles tool execution +tool_node = ToolNode(tools) + +# 5. Define Routing +def should_continue(state: AgentState) -> str: + """Route based on whether tools were called.""" + last_message = state["messages"][-1] + if last_message.tool_calls: + return "tools" + return END + +# 6. Build Graph +graph = StateGraph(AgentState) + +# Add nodes +graph.add_node("agent", agent) +graph.add_node("tools", tool_node) + +# Add edges +graph.add_edge(START, "agent") +graph.add_conditional_edges("agent", should_continue, ["tools", END]) +graph.add_edge("tools", "agent") # Loop back + +# Compile +app = graph.compile() + +# 7. Run +result = app.invoke({ + "messages": [("user", "What is 25 * 4?")] +}) +``` + +### State with Reducers + +Complex state management with custom reducers + +**When to use**: Multiple agents updating shared state + +```python +from typing import Annotated, TypedDict +from operator import add +from langgraph.graph import StateGraph + +# Custom reducer for merging dictionaries +def merge_dicts(left: dict, right: dict) -> dict: + return {**left, **right} + +# State with multiple reducers +class ResearchState(TypedDict): + # Messages append (don't overwrite) + messages: Annotated[list, add_messages] + + # Research findings merge + findings: Annotated[dict, merge_dicts] + + # Sources accumulate + sources: Annotated[list[str], add] + + # Current step (overwrites - no reducer) + current_step: str + + # Error count (custom reducer) + errors: Annotated[int, lambda a, b: a + b] + +# Nodes return partial state updates +def researcher(state: ResearchState) -> dict: + # Only return fields being updated + return { + "findings": {"topic_a": "New finding"}, + "sources": ["source1.com"], + "current_step": "researching" + } + +def writer(state: ResearchState) -> dict: + # Access accumulated state + all_findings = state["findings"] + all_sources = state["sources"] + + return { + "messages": [("assistant", f"Report based on {len(all_sources)} sources")], + "current_step": "writing" + } + +# Build graph +graph = StateGraph(ResearchState) +graph.add_node("researcher", researcher) +graph.add_node("writer", writer) +# ... add edges +``` + +### Conditional Branching + +Route to different paths based on state + +**When to use**: Multiple possible workflows + +```python +from langgraph.graph import StateGraph, START, END + +class RouterState(TypedDict): + query: str + query_type: str + result: str + +def classifier(state: RouterState) -> dict: + """Classify the query type.""" + query = state["query"].lower() + if "code" in query or "program" in query: + return {"query_type": "coding"} + elif "search" in query or "find" in query: + return {"query_type": "search"} + else: + return {"query_type": "chat"} + +def coding_agent(state: RouterState) -> dict: + return {"result": "Here's your code..."} + +def search_agent(state: RouterState) -> dict: + return {"result": "Search results..."} + +def chat_agent(state: RouterState) -> dict: + return {"result": "Let me help..."} + +# Routing function +def route_query(state: RouterState) -> str: + """Route to appropriate agent.""" + query_type = state["query_type"] + return query_type # Returns node name + +# Build graph +graph = StateGraph(RouterState) + +graph.add_node("classifier", classifier) +graph.add_node("coding", coding_agent) +graph.add_node("search", search_agent) +graph.add_node("chat", chat_agent) + +graph.add_edge(START, "classifier") + +# Conditional edges from classifier +graph.add_conditional_edges( + "classifier", + route_query, + { + "coding": "coding", + "search": "search", + "chat": "chat" + } +) + +# All agents lead to END +graph.add_edge("coding", END) +graph.add_edge("search", END) +graph.add_edge("chat", END) + +app = graph.compile() +``` + +## Anti-Patterns + +### โŒ Infinite Loop Without Exit + +**Why bad**: Agent loops forever. +Burns tokens and costs. +Eventually errors out. + +**Instead**: Always have exit conditions: +- Max iterations counter in state +- Clear END conditions in routing +- Timeout at application level + +def should_continue(state): + if state["iterations"] > 10: + return END + if state["task_complete"]: + return END + return "agent" + +### โŒ Stateless Nodes + +**Why bad**: Loses LangGraph's benefits. +State not persisted. +Can't resume conversations. + +**Instead**: Always use state for data flow. +Return state updates from nodes. +Use reducers for accumulation. +Let LangGraph manage state. + +### โŒ Giant Monolithic State + +**Why bad**: Hard to reason about. +Unnecessary data in context. +Serialization overhead. + +**Instead**: Use input/output schemas for clean interfaces. +Private state for internal data. +Clear separation of concerns. + +## Limitations + +- Python-only (TypeScript in early stages) +- Learning curve for graph concepts +- State management complexity +- Debugging can be challenging + +## Related Skills + +Works well with: `crewai`, `autonomous-agents`, `langfuse`, `structured-output` + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/LICENSE.txt b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/LICENSE.txt new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/SKILL.md b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/SKILL.md new file mode 100644 index 00000000..6789790c --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/SKILL.md @@ -0,0 +1,241 @@ +--- +name: mcp-builder +description: "Create MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. The quality of an MCP server is measured by how well it enables LLMs to accomplish real-world tasks." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# MCP Server Development Guide + +## Overview + +Create MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. The quality of an MCP server is measured by how well it enables LLMs to accomplish real-world tasks. + +--- + +# Process + +## ๐Ÿš€ High-Level Workflow + +Creating a high-quality MCP server involves four main phases: + +### Phase 1: Deep Research and Planning + +#### 1.1 Understand Modern MCP Design + +**API Coverage vs. Workflow Tools:** +Balance comprehensive API endpoint coverage with specialized workflow tools. Workflow tools can be more convenient for specific tasks, while comprehensive coverage gives agents flexibility to compose operations. Performance varies by clientโ€”some clients benefit from code execution that combines basic tools, while others work better with higher-level workflows. When uncertain, prioritize comprehensive API coverage. + +**Tool Naming and Discoverability:** +Clear, descriptive tool names help agents find the right tools quickly. Use consistent prefixes (e.g., `github_create_issue`, `github_list_repos`) and action-oriented naming. + +**Context Management:** +Agents benefit from concise tool descriptions and the ability to filter/paginate results. Design tools that return focused, relevant data. Some clients support code execution which can help agents filter and process data efficiently. + +**Actionable Error Messages:** +Error messages should guide agents toward solutions with specific suggestions and next steps. + +#### 1.2 Study MCP Protocol Documentation + +**Navigate the MCP specification:** + +Start with the sitemap to find relevant pages: `https://modelcontextprotocol.io/sitemap.xml` + +Then fetch specific pages with `.md` suffix for markdown format (e.g., `https://modelcontextprotocol.io/specification/draft.md`). + +Key pages to review: +- Specification overview and architecture +- Transport mechanisms (streamable HTTP, stdio) +- Tool, resource, and prompt definitions + +#### 1.3 Study Framework Documentation + +**Recommended stack:** +- **Language**: TypeScript (high-quality SDK support and good compatibility in many execution environments e.g. MCPB. Plus AI models are good at generating TypeScript code, benefiting from its broad usage, static typing and good linting tools) +- **Transport**: Streamable HTTP for remote servers, using stateless JSON (simpler to scale and maintain, as opposed to stateful sessions and streaming responses). stdio for local servers. + +**Load framework documentation:** + +- **MCP Best Practices**: [๐Ÿ“‹ View Best Practices](./reference/mcp_best_practices.md) - Core guidelines + +**For TypeScript (recommended):** +- **TypeScript SDK**: Use WebFetch to load `https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/README.md` +- [โšก TypeScript Guide](./reference/node_mcp_server.md) - TypeScript patterns and examples + +**For Python:** +- **Python SDK**: Use WebFetch to load `https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md` +- [๐Ÿ Python Guide](./reference/python_mcp_server.md) - Python patterns and examples + +#### 1.4 Plan Your Implementation + +**Understand the API:** +Review the service's API documentation to identify key endpoints, authentication requirements, and data models. Use web search and WebFetch as needed. + +**Tool Selection:** +Prioritize comprehensive API coverage. List endpoints to implement, starting with the most common operations. + +--- + +### Phase 2: Implementation + +#### 2.1 Set Up Project Structure + +See language-specific guides for project setup: +- [โšก TypeScript Guide](./reference/node_mcp_server.md) - Project structure, package.json, tsconfig.json +- [๐Ÿ Python Guide](./reference/python_mcp_server.md) - Module organization, dependencies + +#### 2.2 Implement Core Infrastructure + +Create shared utilities: +- API client with authentication +- Error handling helpers +- Response formatting (JSON/Markdown) +- Pagination support + +#### 2.3 Implement Tools + +For each tool: + +**Input Schema:** +- Use Zod (TypeScript) or Pydantic (Python) +- Include constraints and clear descriptions +- Add examples in field descriptions + +**Output Schema:** +- Define `outputSchema` where possible for structured data +- Use `structuredContent` in tool responses (TypeScript SDK feature) +- Helps clients understand and process tool outputs + +**Tool Description:** +- Concise summary of functionality +- Parameter descriptions +- Return type schema + +**Implementation:** +- Async/await for I/O operations +- Proper error handling with actionable messages +- Support pagination where applicable +- Return both text content and structured data when using modern SDKs + +**Annotations:** +- `readOnlyHint`: true/false +- `destructiveHint`: true/false +- `idempotentHint`: true/false +- `openWorldHint`: true/false + +--- + +### Phase 3: Review and Test + +#### 3.1 Code Quality + +Review for: +- No duplicated code (DRY principle) +- Consistent error handling +- Full type coverage +- Clear tool descriptions + +#### 3.2 Build and Test + +**TypeScript:** +- Run `npm run build` to verify compilation +- Test with MCP Inspector: `npx @modelcontextprotocol/inspector` + +**Python:** +- Verify syntax: `python -m py_compile your_server.py` +- Test with MCP Inspector + +See language-specific guides for detailed testing approaches and quality checklists. + +--- + +### Phase 4: Create Evaluations + +After implementing your MCP server, create comprehensive evaluations to test its effectiveness. + +**Load [โœ… Evaluation Guide](./reference/evaluation.md) for complete evaluation guidelines.** + +#### 4.1 Understand Evaluation Purpose + +Use evaluations to test whether LLMs can effectively use your MCP server to answer realistic, complex questions. + +#### 4.2 Create 10 Evaluation Questions + +To create effective evaluations, follow the process outlined in the evaluation guide: + +1. **Tool Inspection**: List available tools and understand their capabilities +2. **Content Exploration**: Use READ-ONLY operations to explore available data +3. **Question Generation**: Create 10 complex, realistic questions +4. **Answer Verification**: Solve each question yourself to verify answers + +#### 4.3 Evaluation Requirements + +Ensure each question is: +- **Independent**: Not dependent on other questions +- **Read-only**: Only non-destructive operations required +- **Complex**: Requiring multiple tool calls and deep exploration +- **Realistic**: Based on real use cases humans would care about +- **Verifiable**: Single, clear answer that can be verified by string comparison +- **Stable**: Answer won't change over time + +#### 4.4 Output Format + +Create an XML file with this structure: + +```xml + + + Find discussions about AI model launches with animal codenames. One model needed a specific safety designation that uses the format ASL-X. What number X was being determined for the model named after a spotted wild cat? + 3 + + + +``` + +--- + +# Reference Files + +## ๐Ÿ“š Documentation Library + +Load these resources as needed during development: + +### Core MCP Documentation (Load First) +- **MCP Protocol**: Start with sitemap at `https://modelcontextprotocol.io/sitemap.xml`, then fetch specific pages with `.md` suffix +- [๐Ÿ“‹ MCP Best Practices](./reference/mcp_best_practices.md) - Universal MCP guidelines including: + - Server and tool naming conventions + - Response format guidelines (JSON vs Markdown) + - Pagination best practices + - Transport selection (streamable HTTP vs stdio) + - Security and error handling standards + +### SDK Documentation (Load During Phase 1/2) +- **Python SDK**: Fetch from `https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md` +- **TypeScript SDK**: Fetch from `https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/README.md` + +### Language-Specific Implementation Guides (Load During Phase 2) +- [๐Ÿ Python Implementation Guide](./reference/python_mcp_server.md) - Complete Python/FastMCP guide with: + - Server initialization patterns + - Pydantic model examples + - Tool registration with `@mcp.tool` + - Complete working examples + - Quality checklist + +- [โšก TypeScript Implementation Guide](./reference/node_mcp_server.md) - Complete TypeScript guide with: + - Project structure + - Zod schema patterns + - Tool registration with `server.registerTool` + - Complete working examples + - Quality checklist + +### Evaluation Guide (Load During Phase 4) +- [โœ… Evaluation Guide](./reference/evaluation.md) - Complete evaluation creation guide with: + - Question creation guidelines + - Answer verification strategies + - XML format specifications + - Example questions and answers + - Running an evaluation with the provided scripts + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/reference/evaluation.md b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/reference/evaluation.md new file mode 100644 index 00000000..87e9bb78 --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/reference/evaluation.md @@ -0,0 +1,602 @@ +# MCP Server Evaluation Guide + +## Overview + +This document provides guidance on creating comprehensive evaluations for MCP servers. Evaluations test whether LLMs can effectively use your MCP server to answer realistic, complex questions using only the tools provided. + +--- + +## Quick Reference + +### Evaluation Requirements +- Create 10 human-readable questions +- Questions must be READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE +- Each question requires multiple tool calls (potentially dozens) +- Answers must be single, verifiable values +- Answers must be STABLE (won't change over time) + +### Output Format +```xml + + + Your question here + Single verifiable answer + + +``` + +--- + +## Purpose of Evaluations + +The measure of quality of an MCP server is NOT how well or comprehensively the server implements tools, but how well these implementations (input/output schemas, docstrings/descriptions, functionality) enable LLMs with no other context and access ONLY to the MCP servers to answer realistic and difficult questions. + +## Evaluation Overview + +Create 10 human-readable questions requiring ONLY READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE, and IDEMPOTENT operations to answer. Each question should be: +- Realistic +- Clear and concise +- Unambiguous +- Complex, requiring potentially dozens of tool calls or steps +- Answerable with a single, verifiable value that you identify in advance + +## Question Guidelines + +### Core Requirements + +1. **Questions MUST be independent** + - Each question should NOT depend on the answer to any other question + - Should not assume prior write operations from processing another question + +2. **Questions MUST require ONLY NON-DESTRUCTIVE AND IDEMPOTENT tool use** + - Should not instruct or require modifying state to arrive at the correct answer + +3. **Questions must be REALISTIC, CLEAR, CONCISE, and COMPLEX** + - Must require another LLM to use multiple (potentially dozens of) tools or steps to answer + +### Complexity and Depth + +4. **Questions must require deep exploration** + - Consider multi-hop questions requiring multiple sub-questions and sequential tool calls + - Each step should benefit from information found in previous questions + +5. **Questions may require extensive paging** + - May need paging through multiple pages of results + - May require querying old data (1-2 years out-of-date) to find niche information + - The questions must be DIFFICULT + +6. **Questions must require deep understanding** + - Rather than surface-level knowledge + - May pose complex ideas as True/False questions requiring evidence + - May use multiple-choice format where LLM must search different hypotheses + +7. **Questions must not be solvable with straightforward keyword search** + - Do not include specific keywords from the target content + - Use synonyms, related concepts, or paraphrases + - Require multiple searches, analyzing multiple related items, extracting context, then deriving the answer + +### Tool Testing + +8. **Questions should stress-test tool return values** + - May elicit tools returning large JSON objects or lists, overwhelming the LLM + - Should require understanding multiple modalities of data: + - IDs and names + - Timestamps and datetimes (months, days, years, seconds) + - File IDs, names, extensions, and mimetypes + - URLs, GIDs, etc. + - Should probe the tool's ability to return all useful forms of data + +9. **Questions should MOSTLY reflect real human use cases** + - The kinds of information retrieval tasks that HUMANS assisted by an LLM would care about + +10. **Questions may require dozens of tool calls** + - This challenges LLMs with limited context + - Encourages MCP server tools to reduce information returned + +11. **Include ambiguous questions** + - May be ambiguous OR require difficult decisions on which tools to call + - Force the LLM to potentially make mistakes or misinterpret + - Ensure that despite AMBIGUITY, there is STILL A SINGLE VERIFIABLE ANSWER + +### Stability + +12. **Questions must be designed so the answer DOES NOT CHANGE** + - Do not ask questions that rely on "current state" which is dynamic + - For example, do not count: + - Number of reactions to a post + - Number of replies to a thread + - Number of members in a channel + +13. **DO NOT let the MCP server RESTRICT the kinds of questions you create** + - Create challenging and complex questions + - Some may not be solvable with the available MCP server tools + - Questions may require specific output formats (datetime vs. epoch time, JSON vs. MARKDOWN) + - Questions may require dozens of tool calls to complete + +## Answer Guidelines + +### Verification + +1. **Answers must be VERIFIABLE via direct string comparison** + - If the answer can be re-written in many formats, clearly specify the output format in the QUESTION + - Examples: "Use YYYY/MM/DD.", "Respond True or False.", "Answer A, B, C, or D and nothing else." + - Answer should be a single VERIFIABLE value such as: + - User ID, user name, display name, first name, last name + - Channel ID, channel name + - Message ID, string + - URL, title + - Numerical quantity + - Timestamp, datetime + - Boolean (for True/False questions) + - Email address, phone number + - File ID, file name, file extension + - Multiple choice answer + - Answers must not require special formatting or complex, structured output + - Answer will be verified using DIRECT STRING COMPARISON + +### Readability + +2. **Answers should generally prefer HUMAN-READABLE formats** + - Examples: names, first name, last name, datetime, file name, message string, URL, yes/no, true/false, a/b/c/d + - Rather than opaque IDs (though IDs are acceptable) + - The VAST MAJORITY of answers should be human-readable + +### Stability + +3. **Answers must be STABLE/STATIONARY** + - Look at old content (e.g., conversations that have ended, projects that have launched, questions answered) + - Create QUESTIONS based on "closed" concepts that will always return the same answer + - Questions may ask to consider a fixed time window to insulate from non-stationary answers + - Rely on context UNLIKELY to change + - Example: if finding a paper name, be SPECIFIC enough so answer is not confused with papers published later + +4. **Answers must be CLEAR and UNAMBIGUOUS** + - Questions must be designed so there is a single, clear answer + - Answer can be derived from using the MCP server tools + +### Diversity + +5. **Answers must be DIVERSE** + - Answer should be a single VERIFIABLE value in diverse modalities and formats + - User concept: user ID, user name, display name, first name, last name, email address, phone number + - Channel concept: channel ID, channel name, channel topic + - Message concept: message ID, message string, timestamp, month, day, year + +6. **Answers must NOT be complex structures** + - Not a list of values + - Not a complex object + - Not a list of IDs or strings + - Not natural language text + - UNLESS the answer can be straightforwardly verified using DIRECT STRING COMPARISON + - And can be realistically reproduced + - It should be unlikely that an LLM would return the same list in any other order or format + +## Evaluation Process + +### Step 1: Documentation Inspection + +Read the documentation of the target API to understand: +- Available endpoints and functionality +- If ambiguity exists, fetch additional information from the web +- Parallelize this step AS MUCH AS POSSIBLE +- Ensure each subagent is ONLY examining documentation from the file system or on the web + +### Step 2: Tool Inspection + +List the tools available in the MCP server: +- Inspect the MCP server directly +- Understand input/output schemas, docstrings, and descriptions +- WITHOUT calling the tools themselves at this stage + +### Step 3: Developing Understanding + +Repeat steps 1 & 2 until you have a good understanding: +- Iterate multiple times +- Think about the kinds of tasks you want to create +- Refine your understanding +- At NO stage should you READ the code of the MCP server implementation itself +- Use your intuition and understanding to create reasonable, realistic, but VERY challenging tasks + +### Step 4: Read-Only Content Inspection + +After understanding the API and tools, USE the MCP server tools: +- Inspect content using READ-ONLY and NON-DESTRUCTIVE operations ONLY +- Goal: identify specific content (e.g., users, channels, messages, projects, tasks) for creating realistic questions +- Should NOT call any tools that modify state +- Will NOT read the code of the MCP server implementation itself +- Parallelize this step with individual sub-agents pursuing independent explorations +- Ensure each subagent is only performing READ-ONLY, NON-DESTRUCTIVE, and IDEMPOTENT operations +- BE CAREFUL: SOME TOOLS may return LOTS OF DATA which would cause you to run out of CONTEXT +- Make INCREMENTAL, SMALL, AND TARGETED tool calls for exploration +- In all tool call requests, use the `limit` parameter to limit results (<10) +- Use pagination + +### Step 5: Task Generation + +After inspecting the content, create 10 human-readable questions: +- An LLM should be able to answer these with the MCP server +- Follow all question and answer guidelines above + +## Output Format + +Each QA pair consists of a question and an answer. The output should be an XML file with this structure: + +```xml + + + Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name? + Website Redesign + + + Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username. + sarah_dev + + + Look for pull requests that modified files in the /api directory and were merged between January 1 and January 31, 2024. How many different contributors worked on these PRs? + 7 + + + Find the repository with the most stars that was created before 2023. What is the repository name? + data-pipeline + + +``` + +## Evaluation Examples + +### Good Questions + +**Example 1: Multi-hop question requiring deep exploration (GitHub MCP)** +```xml + + Find the repository that was archived in Q3 2023 and had previously been the most forked project in the organization. What was the primary programming language used in that repository? + Python + +``` + +This question is good because: +- Requires multiple searches to find archived repositories +- Needs to identify which had the most forks before archival +- Requires examining repository details for the language +- Answer is a simple, verifiable value +- Based on historical (closed) data that won't change + +**Example 2: Requires understanding context without keyword matching (Project Management MCP)** +```xml + + Locate the initiative focused on improving customer onboarding that was completed in late 2023. The project lead created a retrospective document after completion. What was the lead's role title at that time? + Product Manager + +``` + +This question is good because: +- Doesn't use specific project name ("initiative focused on improving customer onboarding") +- Requires finding completed projects from specific timeframe +- Needs to identify the project lead and their role +- Requires understanding context from retrospective documents +- Answer is human-readable and stable +- Based on completed work (won't change) + +**Example 3: Complex aggregation requiring multiple steps (Issue Tracker MCP)** +```xml + + Among all bugs reported in January 2024 that were marked as critical priority, which assignee resolved the highest percentage of their assigned bugs within 48 hours? Provide the assignee's username. + alex_eng + +``` + +This question is good because: +- Requires filtering bugs by date, priority, and status +- Needs to group by assignee and calculate resolution rates +- Requires understanding timestamps to determine 48-hour windows +- Tests pagination (potentially many bugs to process) +- Answer is a single username +- Based on historical data from specific time period + +**Example 4: Requires synthesis across multiple data types (CRM MCP)** +```xml + + Find the account that upgraded from the Starter to Enterprise plan in Q4 2023 and had the highest annual contract value. What industry does this account operate in? + Healthcare + +``` + +This question is good because: +- Requires understanding subscription tier changes +- Needs to identify upgrade events in specific timeframe +- Requires comparing contract values +- Must access account industry information +- Answer is simple and verifiable +- Based on completed historical transactions + +### Poor Questions + +**Example 1: Answer changes over time** +```xml + + How many open issues are currently assigned to the engineering team? + 47 + +``` + +This question is poor because: +- The answer will change as issues are created, closed, or reassigned +- Not based on stable/stationary data +- Relies on "current state" which is dynamic + +**Example 2: Too easy with keyword search** +```xml + + Find the pull request with title "Add authentication feature" and tell me who created it. + developer123 + +``` + +This question is poor because: +- Can be solved with a straightforward keyword search for exact title +- Doesn't require deep exploration or understanding +- No synthesis or analysis needed + +**Example 3: Ambiguous answer format** +```xml + + List all the repositories that have Python as their primary language. + repo1, repo2, repo3, data-pipeline, ml-tools + +``` + +This question is poor because: +- Answer is a list that could be returned in any order +- Difficult to verify with direct string comparison +- LLM might format differently (JSON array, comma-separated, newline-separated) +- Better to ask for a specific aggregate (count) or superlative (most stars) + +## Verification Process + +After creating evaluations: + +1. **Examine the XML file** to understand the schema +2. **Load each task instruction** and in parallel using the MCP server and tools, identify the correct answer by attempting to solve the task YOURSELF +3. **Flag any operations** that require WRITE or DESTRUCTIVE operations +4. **Accumulate all CORRECT answers** and replace any incorrect answers in the document +5. **Remove any ``** that require WRITE or DESTRUCTIVE operations + +Remember to parallelize solving tasks to avoid running out of context, then accumulate all answers and make changes to the file at the end. + +## Tips for Creating Quality Evaluations + +1. **Think Hard and Plan Ahead** before generating tasks +2. **Parallelize Where Opportunity Arises** to speed up the process and manage context +3. **Focus on Realistic Use Cases** that humans would actually want to accomplish +4. **Create Challenging Questions** that test the limits of the MCP server's capabilities +5. **Ensure Stability** by using historical data and closed concepts +6. **Verify Answers** by solving the questions yourself using the MCP server tools +7. **Iterate and Refine** based on what you learn during the process + +--- + +# Running Evaluations + +After creating your evaluation file, you can use the provided evaluation harness to test your MCP server. + +## Setup + +1. **Install Dependencies** + + ```bash + pip install -r scripts/requirements.txt + ``` + + Or install manually: + ```bash + pip install anthropic mcp + ``` + +2. **Set API Key** + + ```bash + export ANTHROPIC_API_KEY=your_api_key_here + ``` + +## Evaluation File Format + +Evaluation files use XML format with `` elements: + +```xml + + + Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name? + Website Redesign + + + Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username. + sarah_dev + + +``` + +## Running Evaluations + +The evaluation script (`scripts/evaluation.py`) supports three transport types: + +**Important:** +- **stdio transport**: The evaluation script automatically launches and manages the MCP server process for you. Do not run the server manually. +- **sse/http transports**: You must start the MCP server separately before running the evaluation. The script connects to the already-running server at the specified URL. + +### 1. Local STDIO Server + +For locally-run MCP servers (script launches the server automatically): + +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a my_mcp_server.py \ + evaluation.xml +``` + +With environment variables: +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a my_mcp_server.py \ + -e API_KEY=abc123 \ + -e DEBUG=true \ + evaluation.xml +``` + +### 2. Server-Sent Events (SSE) + +For SSE-based MCP servers (you must start the server first): + +```bash +python scripts/evaluation.py \ + -t sse \ + -u https://example.com/mcp \ + -H "Authorization: Bearer token123" \ + -H "X-Custom-Header: value" \ + evaluation.xml +``` + +### 3. HTTP (Streamable HTTP) + +For HTTP-based MCP servers (you must start the server first): + +```bash +python scripts/evaluation.py \ + -t http \ + -u https://example.com/mcp \ + -H "Authorization: Bearer token123" \ + evaluation.xml +``` + +## Command-Line Options + +``` +usage: evaluation.py [-h] [-t {stdio,sse,http}] [-m MODEL] [-c COMMAND] + [-a ARGS [ARGS ...]] [-e ENV [ENV ...]] [-u URL] + [-H HEADERS [HEADERS ...]] [-o OUTPUT] + eval_file + +positional arguments: + eval_file Path to evaluation XML file + +optional arguments: + -h, --help Show help message + -t, --transport Transport type: stdio, sse, or http (default: stdio) + -m, --model Claude model to use (default: claude-3-7-sonnet-20250219) + -o, --output Output file for report (default: print to stdout) + +stdio options: + -c, --command Command to run MCP server (e.g., python, node) + -a, --args Arguments for the command (e.g., server.py) + -e, --env Environment variables in KEY=VALUE format + +sse/http options: + -u, --url MCP server URL + -H, --header HTTP headers in 'Key: Value' format +``` + +## Output + +The evaluation script generates a detailed report including: + +- **Summary Statistics**: + - Accuracy (correct/total) + - Average task duration + - Average tool calls per task + - Total tool calls + +- **Per-Task Results**: + - Prompt and expected response + - Actual response from the agent + - Whether the answer was correct (โœ…/โŒ) + - Duration and tool call details + - Agent's summary of its approach + - Agent's feedback on the tools + +### Save Report to File + +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a my_server.py \ + -o evaluation_report.md \ + evaluation.xml +``` + +## Complete Example Workflow + +Here's a complete example of creating and running an evaluation: + +1. **Create your evaluation file** (`my_evaluation.xml`): + +```xml + + + Find the user who created the most issues in January 2024. What is their username? + alice_developer + + + Among all pull requests merged in Q1 2024, which repository had the highest number? Provide the repository name. + backend-api + + + Find the project that was completed in December 2023 and had the longest duration from start to finish. How many days did it take? + 127 + + +``` + +2. **Install dependencies**: + +```bash +pip install -r scripts/requirements.txt +export ANTHROPIC_API_KEY=your_api_key +``` + +3. **Run evaluation**: + +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a github_mcp_server.py \ + -e GITHUB_TOKEN=ghp_xxx \ + -o github_eval_report.md \ + my_evaluation.xml +``` + +4. **Review the report** in `github_eval_report.md` to: + - See which questions passed/failed + - Read the agent's feedback on your tools + - Identify areas for improvement + - Iterate on your MCP server design + +## Troubleshooting + +### Connection Errors + +If you get connection errors: +- **STDIO**: Verify the command and arguments are correct +- **SSE/HTTP**: Check the URL is accessible and headers are correct +- Ensure any required API keys are set in environment variables or headers + +### Low Accuracy + +If many evaluations fail: +- Review the agent's feedback for each task +- Check if tool descriptions are clear and comprehensive +- Verify input parameters are well-documented +- Consider whether tools return too much or too little data +- Ensure error messages are actionable + +### Timeout Issues + +If tasks are timing out: +- Use a more capable model (e.g., `claude-3-7-sonnet-20250219`) +- Check if tools are returning too much data +- Verify pagination is working correctly +- Consider simplifying complex questions \ No newline at end of file diff --git a/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/reference/mcp_best_practices.md b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/reference/mcp_best_practices.md new file mode 100644 index 00000000..b9d343cc --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/reference/mcp_best_practices.md @@ -0,0 +1,249 @@ +# MCP Server Best Practices + +## Quick Reference + +### Server Naming +- **Python**: `{service}_mcp` (e.g., `slack_mcp`) +- **Node/TypeScript**: `{service}-mcp-server` (e.g., `slack-mcp-server`) + +### Tool Naming +- Use snake_case with service prefix +- Format: `{service}_{action}_{resource}` +- Example: `slack_send_message`, `github_create_issue` + +### Response Formats +- Support both JSON and Markdown formats +- JSON for programmatic processing +- Markdown for human readability + +### Pagination +- Always respect `limit` parameter +- Return `has_more`, `next_offset`, `total_count` +- Default to 20-50 items + +### Transport +- **Streamable HTTP**: For remote servers, multi-client scenarios +- **stdio**: For local integrations, command-line tools +- Avoid SSE (deprecated in favor of streamable HTTP) + +--- + +## Server Naming Conventions + +Follow these standardized naming patterns: + +**Python**: Use format `{service}_mcp` (lowercase with underscores) +- Examples: `slack_mcp`, `github_mcp`, `jira_mcp` + +**Node/TypeScript**: Use format `{service}-mcp-server` (lowercase with hyphens) +- Examples: `slack-mcp-server`, `github-mcp-server`, `jira-mcp-server` + +The name should be general, descriptive of the service being integrated, easy to infer from the task description, and without version numbers. + +--- + +## Tool Naming and Design + +### Tool Naming + +1. **Use snake_case**: `search_users`, `create_project`, `get_channel_info` +2. **Include service prefix**: Anticipate that your MCP server may be used alongside other MCP servers + - Use `slack_send_message` instead of just `send_message` + - Use `github_create_issue` instead of just `create_issue` +3. **Be action-oriented**: Start with verbs (get, list, search, create, etc.) +4. **Be specific**: Avoid generic names that could conflict with other servers + +### Tool Design + +- Tool descriptions must narrowly and unambiguously describe functionality +- Descriptions must precisely match actual functionality +- Provide tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) +- Keep tool operations focused and atomic + +--- + +## Response Formats + +All tools that return data should support multiple formats: + +### JSON Format (`response_format="json"`) +- Machine-readable structured data +- Include all available fields and metadata +- Consistent field names and types +- Use for programmatic processing + +### Markdown Format (`response_format="markdown"`, typically default) +- Human-readable formatted text +- Use headers, lists, and formatting for clarity +- Convert timestamps to human-readable format +- Show display names with IDs in parentheses +- Omit verbose metadata + +--- + +## Pagination + +For tools that list resources: + +- **Always respect the `limit` parameter** +- **Implement pagination**: Use `offset` or cursor-based pagination +- **Return pagination metadata**: Include `has_more`, `next_offset`/`next_cursor`, `total_count` +- **Never load all results into memory**: Especially important for large datasets +- **Default to reasonable limits**: 20-50 items is typical + +Example pagination response: +```json +{ + "total": 150, + "count": 20, + "offset": 0, + "items": [...], + "has_more": true, + "next_offset": 20 +} +``` + +--- + +## Transport Options + +### Streamable HTTP + +**Best for**: Remote servers, web services, multi-client scenarios + +**Characteristics**: +- Bidirectional communication over HTTP +- Supports multiple simultaneous clients +- Can be deployed as a web service +- Enables server-to-client notifications + +**Use when**: +- Serving multiple clients simultaneously +- Deploying as a cloud service +- Integration with web applications + +### stdio + +**Best for**: Local integrations, command-line tools + +**Characteristics**: +- Standard input/output stream communication +- Simple setup, no network configuration needed +- Runs as a subprocess of the client + +**Use when**: +- Building tools for local development environments +- Integrating with desktop applications +- Single-user, single-session scenarios + +**Note**: stdio servers should NOT log to stdout (use stderr for logging) + +### Transport Selection + +| Criterion | stdio | Streamable HTTP | +|-----------|-------|-----------------| +| **Deployment** | Local | Remote | +| **Clients** | Single | Multiple | +| **Complexity** | Low | Medium | +| **Real-time** | No | Yes | + +--- + +## Security Best Practices + +### Authentication and Authorization + +**OAuth 2.1**: +- Use secure OAuth 2.1 with certificates from recognized authorities +- Validate access tokens before processing requests +- Only accept tokens specifically intended for your server + +**API Keys**: +- Store API keys in environment variables, never in code +- Validate keys on server startup +- Provide clear error messages when authentication fails + +### Input Validation + +- Sanitize file paths to prevent directory traversal +- Validate URLs and external identifiers +- Check parameter sizes and ranges +- Prevent command injection in system calls +- Use schema validation (Pydantic/Zod) for all inputs + +### Error Handling + +- Don't expose internal errors to clients +- Log security-relevant errors server-side +- Provide helpful but not revealing error messages +- Clean up resources after errors + +### DNS Rebinding Protection + +For streamable HTTP servers running locally: +- Enable DNS rebinding protection +- Validate the `Origin` header on all incoming connections +- Bind to `127.0.0.1` rather than `0.0.0.0` + +--- + +## Tool Annotations + +Provide annotations to help clients understand tool behavior: + +| Annotation | Type | Default | Description | +|-----------|------|---------|-------------| +| `readOnlyHint` | boolean | false | Tool does not modify its environment | +| `destructiveHint` | boolean | true | Tool may perform destructive updates | +| `idempotentHint` | boolean | false | Repeated calls with same args have no additional effect | +| `openWorldHint` | boolean | true | Tool interacts with external entities | + +**Important**: Annotations are hints, not security guarantees. Clients should not make security-critical decisions based solely on annotations. + +--- + +## Error Handling + +- Use standard JSON-RPC error codes +- Report tool errors within result objects (not protocol-level errors) +- Provide helpful, specific error messages with suggested next steps +- Don't expose internal implementation details +- Clean up resources properly on errors + +Example error handling: +```typescript +try { + const result = performOperation(); + return { content: [{ type: "text", text: result }] }; +} catch (error) { + return { + isError: true, + content: [{ + type: "text", + text: `Error: ${error.message}. Try using filter='active_only' to reduce results.` + }] + }; +} +``` + +--- + +## Testing Requirements + +Comprehensive testing should cover: + +- **Functional testing**: Verify correct execution with valid/invalid inputs +- **Integration testing**: Test interaction with external systems +- **Security testing**: Validate auth, input sanitization, rate limiting +- **Performance testing**: Check behavior under load, timeouts +- **Error handling**: Ensure proper error reporting and cleanup + +--- + +## Documentation Requirements + +- Provide clear documentation of all tools and capabilities +- Include working examples (at least 3 per major feature) +- Document security considerations +- Specify required permissions and access levels +- Document rate limits and performance characteristics diff --git a/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/reference/node_mcp_server.md b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/reference/node_mcp_server.md new file mode 100644 index 00000000..f6e5df98 --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/reference/node_mcp_server.md @@ -0,0 +1,970 @@ +# Node/TypeScript MCP Server Implementation Guide + +## Overview + +This document provides Node/TypeScript-specific best practices and examples for implementing MCP servers using the MCP TypeScript SDK. It covers project structure, server setup, tool registration patterns, input validation with Zod, error handling, and complete working examples. + +--- + +## Quick Reference + +### Key Imports +```typescript +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import express from "express"; +import { z } from "zod"; +``` + +### Server Initialization +```typescript +const server = new McpServer({ + name: "service-mcp-server", + version: "1.0.0" +}); +``` + +### Tool Registration Pattern +```typescript +server.registerTool( + "tool_name", + { + title: "Tool Display Name", + description: "What the tool does", + inputSchema: { param: z.string() }, + outputSchema: { result: z.string() } + }, + async ({ param }) => { + const output = { result: `Processed: ${param}` }; + return { + content: [{ type: "text", text: JSON.stringify(output) }], + structuredContent: output // Modern pattern for structured data + }; + } +); +``` + +--- + +## MCP TypeScript SDK + +The official MCP TypeScript SDK provides: +- `McpServer` class for server initialization +- `registerTool` method for tool registration +- Zod schema integration for runtime input validation +- Type-safe tool handler implementations + +**IMPORTANT - Use Modern APIs Only:** +- **DO use**: `server.registerTool()`, `server.registerResource()`, `server.registerPrompt()` +- **DO NOT use**: Old deprecated APIs such as `server.tool()`, `server.setRequestHandler(ListToolsRequestSchema, ...)`, or manual handler registration +- The `register*` methods provide better type safety, automatic schema handling, and are the recommended approach + +See the MCP SDK documentation in the references for complete details. + +## Server Naming Convention + +Node/TypeScript MCP servers must follow this naming pattern: +- **Format**: `{service}-mcp-server` (lowercase with hyphens) +- **Examples**: `github-mcp-server`, `jira-mcp-server`, `stripe-mcp-server` + +The name should be: +- General (not tied to specific features) +- Descriptive of the service/API being integrated +- Easy to infer from the task description +- Without version numbers or dates + +## Project Structure + +Create the following structure for Node/TypeScript MCP servers: + +``` +{service}-mcp-server/ +โ”œโ”€โ”€ package.json +โ”œโ”€โ”€ tsconfig.json +โ”œโ”€โ”€ README.md +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ index.ts # Main entry point with McpServer initialization +โ”‚ โ”œโ”€โ”€ types.ts # TypeScript type definitions and interfaces +โ”‚ โ”œโ”€โ”€ tools/ # Tool implementations (one file per domain) +โ”‚ โ”œโ”€โ”€ services/ # API clients and shared utilities +โ”‚ โ”œโ”€โ”€ schemas/ # Zod validation schemas +โ”‚ โ””โ”€โ”€ constants.ts # Shared constants (API_URL, CHARACTER_LIMIT, etc.) +โ””โ”€โ”€ dist/ # Built JavaScript files (entry point: dist/index.js) +``` + +## Tool Implementation + +### Tool Naming + +Use snake_case for tool names (e.g., "search_users", "create_project", "get_channel_info") with clear, action-oriented names. + +**Avoid Naming Conflicts**: Include the service context to prevent overlaps: +- Use "slack_send_message" instead of just "send_message" +- Use "github_create_issue" instead of just "create_issue" +- Use "asana_list_tasks" instead of just "list_tasks" + +### Tool Structure + +Tools are registered using the `registerTool` method with the following requirements: +- Use Zod schemas for runtime input validation and type safety +- The `description` field must be explicitly provided - JSDoc comments are NOT automatically extracted +- Explicitly provide `title`, `description`, `inputSchema`, and `annotations` +- The `inputSchema` must be a Zod schema object (not a JSON schema) +- Type all parameters and return values explicitly + +```typescript +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +const server = new McpServer({ + name: "example-mcp", + version: "1.0.0" +}); + +// Zod schema for input validation +const UserSearchInputSchema = z.object({ + query: z.string() + .min(2, "Query must be at least 2 characters") + .max(200, "Query must not exceed 200 characters") + .describe("Search string to match against names/emails"), + limit: z.number() + .int() + .min(1) + .max(100) + .default(20) + .describe("Maximum results to return"), + offset: z.number() + .int() + .min(0) + .default(0) + .describe("Number of results to skip for pagination"), + response_format: z.nativeEnum(ResponseFormat) + .default(ResponseFormat.MARKDOWN) + .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable") +}).strict(); + +// Type definition from Zod schema +type UserSearchInput = z.infer; + +server.registerTool( + "example_search_users", + { + title: "Search Example Users", + description: `Search for users in the Example system by name, email, or team. + +This tool searches across all user profiles in the Example platform, supporting partial matches and various search filters. It does NOT create or modify users, only searches existing ones. + +Args: + - query (string): Search string to match against names/emails + - limit (number): Maximum results to return, between 1-100 (default: 20) + - offset (number): Number of results to skip for pagination (default: 0) + - response_format ('markdown' | 'json'): Output format (default: 'markdown') + +Returns: + For JSON format: Structured data with schema: + { + "total": number, // Total number of matches found + "count": number, // Number of results in this response + "offset": number, // Current pagination offset + "users": [ + { + "id": string, // User ID (e.g., "U123456789") + "name": string, // Full name (e.g., "John Doe") + "email": string, // Email address + "team": string, // Team name (optional) + "active": boolean // Whether user is active + } + ], + "has_more": boolean, // Whether more results are available + "next_offset": number // Offset for next page (if has_more is true) + } + +Examples: + - Use when: "Find all marketing team members" -> params with query="team:marketing" + - Use when: "Search for John's account" -> params with query="john" + - Don't use when: You need to create a user (use example_create_user instead) + +Error Handling: + - Returns "Error: Rate limit exceeded" if too many requests (429 status) + - Returns "No users found matching ''" if search returns empty`, + inputSchema: UserSearchInputSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } + }, + async (params: UserSearchInput) => { + try { + // Input validation is handled by Zod schema + // Make API request using validated parameters + const data = await makeApiRequest( + "users/search", + "GET", + undefined, + { + q: params.query, + limit: params.limit, + offset: params.offset + } + ); + + const users = data.users || []; + const total = data.total || 0; + + if (!users.length) { + return { + content: [{ + type: "text", + text: `No users found matching '${params.query}'` + }] + }; + } + + // Prepare structured output + const output = { + total, + count: users.length, + offset: params.offset, + users: users.map((user: any) => ({ + id: user.id, + name: user.name, + email: user.email, + ...(user.team ? { team: user.team } : {}), + active: user.active ?? true + })), + has_more: total > params.offset + users.length, + ...(total > params.offset + users.length ? { + next_offset: params.offset + users.length + } : {}) + }; + + // Format text representation based on requested format + let textContent: string; + if (params.response_format === ResponseFormat.MARKDOWN) { + const lines = [`# User Search Results: '${params.query}'`, "", + `Found ${total} users (showing ${users.length})`, ""]; + for (const user of users) { + lines.push(`## ${user.name} (${user.id})`); + lines.push(`- **Email**: ${user.email}`); + if (user.team) lines.push(`- **Team**: ${user.team}`); + lines.push(""); + } + textContent = lines.join("\n"); + } else { + textContent = JSON.stringify(output, null, 2); + } + + return { + content: [{ type: "text", text: textContent }], + structuredContent: output // Modern pattern for structured data + }; + } catch (error) { + return { + content: [{ + type: "text", + text: handleApiError(error) + }] + }; + } + } +); +``` + +## Zod Schemas for Input Validation + +Zod provides runtime type validation: + +```typescript +import { z } from "zod"; + +// Basic schema with validation +const CreateUserSchema = z.object({ + name: z.string() + .min(1, "Name is required") + .max(100, "Name must not exceed 100 characters"), + email: z.string() + .email("Invalid email format"), + age: z.number() + .int("Age must be a whole number") + .min(0, "Age cannot be negative") + .max(150, "Age cannot be greater than 150") +}).strict(); // Use .strict() to forbid extra fields + +// Enums +enum ResponseFormat { + MARKDOWN = "markdown", + JSON = "json" +} + +const SearchSchema = z.object({ + response_format: z.nativeEnum(ResponseFormat) + .default(ResponseFormat.MARKDOWN) + .describe("Output format") +}); + +// Optional fields with defaults +const PaginationSchema = z.object({ + limit: z.number() + .int() + .min(1) + .max(100) + .default(20) + .describe("Maximum results to return"), + offset: z.number() + .int() + .min(0) + .default(0) + .describe("Number of results to skip") +}); +``` + +## Response Format Options + +Support multiple output formats for flexibility: + +```typescript +enum ResponseFormat { + MARKDOWN = "markdown", + JSON = "json" +} + +const inputSchema = z.object({ + query: z.string(), + response_format: z.nativeEnum(ResponseFormat) + .default(ResponseFormat.MARKDOWN) + .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable") +}); +``` + +**Markdown format**: +- Use headers, lists, and formatting for clarity +- Convert timestamps to human-readable format +- Show display names with IDs in parentheses +- Omit verbose metadata +- Group related information logically + +**JSON format**: +- Return complete, structured data suitable for programmatic processing +- Include all available fields and metadata +- Use consistent field names and types + +## Pagination Implementation + +For tools that list resources: + +```typescript +const ListSchema = z.object({ + limit: z.number().int().min(1).max(100).default(20), + offset: z.number().int().min(0).default(0) +}); + +async function listItems(params: z.infer) { + const data = await apiRequest(params.limit, params.offset); + + const response = { + total: data.total, + count: data.items.length, + offset: params.offset, + items: data.items, + has_more: data.total > params.offset + data.items.length, + next_offset: data.total > params.offset + data.items.length + ? params.offset + data.items.length + : undefined + }; + + return JSON.stringify(response, null, 2); +} +``` + +## Character Limits and Truncation + +Add a CHARACTER_LIMIT constant to prevent overwhelming responses: + +```typescript +// At module level in constants.ts +export const CHARACTER_LIMIT = 25000; // Maximum response size in characters + +async function searchTool(params: SearchInput) { + let result = generateResponse(data); + + // Check character limit and truncate if needed + if (result.length > CHARACTER_LIMIT) { + const truncatedData = data.slice(0, Math.max(1, data.length / 2)); + response.data = truncatedData; + response.truncated = true; + response.truncation_message = + `Response truncated from ${data.length} to ${truncatedData.length} items. ` + + `Use 'offset' parameter or add filters to see more results.`; + result = JSON.stringify(response, null, 2); + } + + return result; +} +``` + +## Error Handling + +Provide clear, actionable error messages: + +```typescript +import axios, { AxiosError } from "axios"; + +function handleApiError(error: unknown): string { + if (error instanceof AxiosError) { + if (error.response) { + switch (error.response.status) { + case 404: + return "Error: Resource not found. Please check the ID is correct."; + case 403: + return "Error: Permission denied. You don't have access to this resource."; + case 429: + return "Error: Rate limit exceeded. Please wait before making more requests."; + default: + return `Error: API request failed with status ${error.response.status}`; + } + } else if (error.code === "ECONNABORTED") { + return "Error: Request timed out. Please try again."; + } + } + return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`; +} +``` + +## Shared Utilities + +Extract common functionality into reusable functions: + +```typescript +// Shared API request function +async function makeApiRequest( + endpoint: string, + method: "GET" | "POST" | "PUT" | "DELETE" = "GET", + data?: any, + params?: any +): Promise { + try { + const response = await axios({ + method, + url: `${API_BASE_URL}/${endpoint}`, + data, + params, + timeout: 30000, + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }); + return response.data; + } catch (error) { + throw error; + } +} +``` + +## Async/Await Best Practices + +Always use async/await for network requests and I/O operations: + +```typescript +// Good: Async network request +async function fetchData(resourceId: string): Promise { + const response = await axios.get(`${API_URL}/resource/${resourceId}`); + return response.data; +} + +// Bad: Promise chains +function fetchData(resourceId: string): Promise { + return axios.get(`${API_URL}/resource/${resourceId}`) + .then(response => response.data); // Harder to read and maintain +} +``` + +## TypeScript Best Practices + +1. **Use Strict TypeScript**: Enable strict mode in tsconfig.json +2. **Define Interfaces**: Create clear interface definitions for all data structures +3. **Avoid `any`**: Use proper types or `unknown` instead of `any` +4. **Zod for Runtime Validation**: Use Zod schemas to validate external data +5. **Type Guards**: Create type guard functions for complex type checking +6. **Error Handling**: Always use try-catch with proper error type checking +7. **Null Safety**: Use optional chaining (`?.`) and nullish coalescing (`??`) + +```typescript +// Good: Type-safe with Zod and interfaces +interface UserResponse { + id: string; + name: string; + email: string; + team?: string; + active: boolean; +} + +const UserSchema = z.object({ + id: z.string(), + name: z.string(), + email: z.string().email(), + team: z.string().optional(), + active: z.boolean() +}); + +type User = z.infer; + +async function getUser(id: string): Promise { + const data = await apiCall(`/users/${id}`); + return UserSchema.parse(data); // Runtime validation +} + +// Bad: Using any +async function getUser(id: string): Promise { + return await apiCall(`/users/${id}`); // No type safety +} +``` + +## Package Configuration + +### package.json + +```json +{ + "name": "{service}-mcp-server", + "version": "1.0.0", + "description": "MCP server for {Service} API integration", + "type": "module", + "main": "dist/index.js", + "scripts": { + "start": "node dist/index.js", + "dev": "tsx watch src/index.ts", + "build": "tsc", + "clean": "rm -rf dist" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.6.1", + "axios": "^1.7.9", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} +``` + +### tsconfig.json + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +## Complete Example + +```typescript +#!/usr/bin/env node +/** + * MCP Server for Example Service. + * + * This server provides tools to interact with Example API, including user search, + * project management, and data export capabilities. + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import axios, { AxiosError } from "axios"; + +// Constants +const API_BASE_URL = "https://api.example.com/v1"; +const CHARACTER_LIMIT = 25000; + +// Enums +enum ResponseFormat { + MARKDOWN = "markdown", + JSON = "json" +} + +// Zod schemas +const UserSearchInputSchema = z.object({ + query: z.string() + .min(2, "Query must be at least 2 characters") + .max(200, "Query must not exceed 200 characters") + .describe("Search string to match against names/emails"), + limit: z.number() + .int() + .min(1) + .max(100) + .default(20) + .describe("Maximum results to return"), + offset: z.number() + .int() + .min(0) + .default(0) + .describe("Number of results to skip for pagination"), + response_format: z.nativeEnum(ResponseFormat) + .default(ResponseFormat.MARKDOWN) + .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable") +}).strict(); + +type UserSearchInput = z.infer; + +// Shared utility functions +async function makeApiRequest( + endpoint: string, + method: "GET" | "POST" | "PUT" | "DELETE" = "GET", + data?: any, + params?: any +): Promise { + try { + const response = await axios({ + method, + url: `${API_BASE_URL}/${endpoint}`, + data, + params, + timeout: 30000, + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }); + return response.data; + } catch (error) { + throw error; + } +} + +function handleApiError(error: unknown): string { + if (error instanceof AxiosError) { + if (error.response) { + switch (error.response.status) { + case 404: + return "Error: Resource not found. Please check the ID is correct."; + case 403: + return "Error: Permission denied. You don't have access to this resource."; + case 429: + return "Error: Rate limit exceeded. Please wait before making more requests."; + default: + return `Error: API request failed with status ${error.response.status}`; + } + } else if (error.code === "ECONNABORTED") { + return "Error: Request timed out. Please try again."; + } + } + return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`; +} + +// Create MCP server instance +const server = new McpServer({ + name: "example-mcp", + version: "1.0.0" +}); + +// Register tools +server.registerTool( + "example_search_users", + { + title: "Search Example Users", + description: `[Full description as shown above]`, + inputSchema: UserSearchInputSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } + }, + async (params: UserSearchInput) => { + // Implementation as shown above + } +); + +// Main function +// For stdio (local): +async function runStdio() { + if (!process.env.EXAMPLE_API_KEY) { + console.error("ERROR: EXAMPLE_API_KEY environment variable is required"); + process.exit(1); + } + + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("MCP server running via stdio"); +} + +// For streamable HTTP (remote): +async function runHTTP() { + if (!process.env.EXAMPLE_API_KEY) { + console.error("ERROR: EXAMPLE_API_KEY environment variable is required"); + process.exit(1); + } + + const app = express(); + app.use(express.json()); + + app.post('/mcp', async (req, res) => { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true + }); + res.on('close', () => transport.close()); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + }); + + const port = parseInt(process.env.PORT || '3000'); + app.listen(port, () => { + console.error(`MCP server running on http://localhost:${port}/mcp`); + }); +} + +// Choose transport based on environment +const transport = process.env.TRANSPORT || 'stdio'; +if (transport === 'http') { + runHTTP().catch(error => { + console.error("Server error:", error); + process.exit(1); + }); +} else { + runStdio().catch(error => { + console.error("Server error:", error); + process.exit(1); + }); +} +``` + +--- + +## Advanced MCP Features + +### Resource Registration + +Expose data as resources for efficient, URI-based access: + +```typescript +import { ResourceTemplate } from "@modelcontextprotocol/sdk/types.js"; + +// Register a resource with URI template +server.registerResource( + { + uri: "file://documents/{name}", + name: "Document Resource", + description: "Access documents by name", + mimeType: "text/plain" + }, + async (uri: string) => { + // Extract parameter from URI + const match = uri.match(/^file:\/\/documents\/(.+)$/); + if (!match) { + throw new Error("Invalid URI format"); + } + + const documentName = match[1]; + const content = await loadDocument(documentName); + + return { + contents: [{ + uri, + mimeType: "text/plain", + text: content + }] + }; + } +); + +// List available resources dynamically +server.registerResourceList(async () => { + const documents = await getAvailableDocuments(); + return { + resources: documents.map(doc => ({ + uri: `file://documents/${doc.name}`, + name: doc.name, + mimeType: "text/plain", + description: doc.description + })) + }; +}); +``` + +**When to use Resources vs Tools:** +- **Resources**: For data access with simple URI-based parameters +- **Tools**: For complex operations requiring validation and business logic +- **Resources**: When data is relatively static or template-based +- **Tools**: When operations have side effects or complex workflows + +### Transport Options + +The TypeScript SDK supports two main transport mechanisms: + +#### Streamable HTTP (Recommended for Remote Servers) + +```typescript +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import express from "express"; + +const app = express(); +app.use(express.json()); + +app.post('/mcp', async (req, res) => { + // Create new transport for each request (stateless, prevents request ID collisions) + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true + }); + + res.on('close', () => transport.close()); + + await server.connect(transport); + await transport.handleRequest(req, res, req.body); +}); + +app.listen(3000); +``` + +#### stdio (For Local Integrations) + +```typescript +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; + +const transport = new StdioServerTransport(); +await server.connect(transport); +``` + +**Transport selection:** +- **Streamable HTTP**: Web services, remote access, multiple clients +- **stdio**: Command-line tools, local development, subprocess integration + +### Notification Support + +Notify clients when server state changes: + +```typescript +// Notify when tools list changes +server.notification({ + method: "notifications/tools/list_changed" +}); + +// Notify when resources change +server.notification({ + method: "notifications/resources/list_changed" +}); +``` + +Use notifications sparingly - only when server capabilities genuinely change. + +--- + +## Code Best Practices + +### Code Composability and Reusability + +Your implementation MUST prioritize composability and code reuse: + +1. **Extract Common Functionality**: + - Create reusable helper functions for operations used across multiple tools + - Build shared API clients for HTTP requests instead of duplicating code + - Centralize error handling logic in utility functions + - Extract business logic into dedicated functions that can be composed + - Extract shared markdown or JSON field selection & formatting functionality + +2. **Avoid Duplication**: + - NEVER copy-paste similar code between tools + - If you find yourself writing similar logic twice, extract it into a function + - Common operations like pagination, filtering, field selection, and formatting should be shared + - Authentication/authorization logic should be centralized + +## Building and Running + +Always build your TypeScript code before running: + +```bash +# Build the project +npm run build + +# Run the server +npm start + +# Development with auto-reload +npm run dev +``` + +Always ensure `npm run build` completes successfully before considering the implementation complete. + +## Quality Checklist + +Before finalizing your Node/TypeScript MCP server implementation, ensure: + +### Strategic Design +- [ ] Tools enable complete workflows, not just API endpoint wrappers +- [ ] Tool names reflect natural task subdivisions +- [ ] Response formats optimize for agent context efficiency +- [ ] Human-readable identifiers used where appropriate +- [ ] Error messages guide agents toward correct usage + +### Implementation Quality +- [ ] FOCUSED IMPLEMENTATION: Most important and valuable tools implemented +- [ ] All tools registered using `registerTool` with complete configuration +- [ ] All tools include `title`, `description`, `inputSchema`, and `annotations` +- [ ] Annotations correctly set (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) +- [ ] All tools use Zod schemas for runtime input validation with `.strict()` enforcement +- [ ] All Zod schemas have proper constraints and descriptive error messages +- [ ] All tools have comprehensive descriptions with explicit input/output types +- [ ] Descriptions include return value examples and complete schema documentation +- [ ] Error messages are clear, actionable, and educational + +### TypeScript Quality +- [ ] TypeScript interfaces are defined for all data structures +- [ ] Strict TypeScript is enabled in tsconfig.json +- [ ] No use of `any` type - use `unknown` or proper types instead +- [ ] All async functions have explicit Promise return types +- [ ] Error handling uses proper type guards (e.g., `axios.isAxiosError`, `z.ZodError`) + +### Advanced Features (where applicable) +- [ ] Resources registered for appropriate data endpoints +- [ ] Appropriate transport configured (stdio or streamable HTTP) +- [ ] Notifications implemented for dynamic server capabilities +- [ ] Type-safe with SDK interfaces + +### Project Configuration +- [ ] Package.json includes all necessary dependencies +- [ ] Build script produces working JavaScript in dist/ directory +- [ ] Main entry point is properly configured as dist/index.js +- [ ] Server name follows format: `{service}-mcp-server` +- [ ] tsconfig.json properly configured with strict mode + +### Code Quality +- [ ] Pagination is properly implemented where applicable +- [ ] Large responses check CHARACTER_LIMIT constant and truncate with clear messages +- [ ] Filtering options are provided for potentially large result sets +- [ ] All network operations handle timeouts and connection errors gracefully +- [ ] Common functionality is extracted into reusable functions +- [ ] Return types are consistent across similar operations + +### Testing and Build +- [ ] `npm run build` completes successfully without errors +- [ ] dist/index.js created and executable +- [ ] Server runs: `node dist/index.js --help` +- [ ] All imports resolve correctly +- [ ] Sample tool calls work as expected \ No newline at end of file diff --git a/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/reference/python_mcp_server.md b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/reference/python_mcp_server.md new file mode 100644 index 00000000..cf7ec996 --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/reference/python_mcp_server.md @@ -0,0 +1,719 @@ +# Python MCP Server Implementation Guide + +## Overview + +This document provides Python-specific best practices and examples for implementing MCP servers using the MCP Python SDK. It covers server setup, tool registration patterns, input validation with Pydantic, error handling, and complete working examples. + +--- + +## Quick Reference + +### Key Imports +```python +from mcp.server.fastmcp import FastMCP +from pydantic import BaseModel, Field, field_validator, ConfigDict +from typing import Optional, List, Dict, Any +from enum import Enum +import httpx +``` + +### Server Initialization +```python +mcp = FastMCP("service_mcp") +``` + +### Tool Registration Pattern +```python +@mcp.tool(name="tool_name", annotations={...}) +async def tool_function(params: InputModel) -> str: + # Implementation + pass +``` + +--- + +## MCP Python SDK and FastMCP + +The official MCP Python SDK provides FastMCP, a high-level framework for building MCP servers. It provides: +- Automatic description and inputSchema generation from function signatures and docstrings +- Pydantic model integration for input validation +- Decorator-based tool registration with `@mcp.tool` + +**For complete SDK documentation, use WebFetch to load:** +`https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md` + +## Server Naming Convention + +Python MCP servers must follow this naming pattern: +- **Format**: `{service}_mcp` (lowercase with underscores) +- **Examples**: `github_mcp`, `jira_mcp`, `stripe_mcp` + +The name should be: +- General (not tied to specific features) +- Descriptive of the service/API being integrated +- Easy to infer from the task description +- Without version numbers or dates + +## Tool Implementation + +### Tool Naming + +Use snake_case for tool names (e.g., "search_users", "create_project", "get_channel_info") with clear, action-oriented names. + +**Avoid Naming Conflicts**: Include the service context to prevent overlaps: +- Use "slack_send_message" instead of just "send_message" +- Use "github_create_issue" instead of just "create_issue" +- Use "asana_list_tasks" instead of just "list_tasks" + +### Tool Structure with FastMCP + +Tools are defined using the `@mcp.tool` decorator with Pydantic models for input validation: + +```python +from pydantic import BaseModel, Field, ConfigDict +from mcp.server.fastmcp import FastMCP + +# Initialize the MCP server +mcp = FastMCP("example_mcp") + +# Define Pydantic model for input validation +class ServiceToolInput(BaseModel): + '''Input model for service tool operation.''' + model_config = ConfigDict( + str_strip_whitespace=True, # Auto-strip whitespace from strings + validate_assignment=True, # Validate on assignment + extra='forbid' # Forbid extra fields + ) + + param1: str = Field(..., description="First parameter description (e.g., 'user123', 'project-abc')", min_length=1, max_length=100) + param2: Optional[int] = Field(default=None, description="Optional integer parameter with constraints", ge=0, le=1000) + tags: Optional[List[str]] = Field(default_factory=list, description="List of tags to apply", max_items=10) + +@mcp.tool( + name="service_tool_name", + annotations={ + "title": "Human-Readable Tool Title", + "readOnlyHint": True, # Tool does not modify environment + "destructiveHint": False, # Tool does not perform destructive operations + "idempotentHint": True, # Repeated calls have no additional effect + "openWorldHint": False # Tool does not interact with external entities + } +) +async def service_tool_name(params: ServiceToolInput) -> str: + '''Tool description automatically becomes the 'description' field. + + This tool performs a specific operation on the service. It validates all inputs + using the ServiceToolInput Pydantic model before processing. + + Args: + params (ServiceToolInput): Validated input parameters containing: + - param1 (str): First parameter description + - param2 (Optional[int]): Optional parameter with default + - tags (Optional[List[str]]): List of tags + + Returns: + str: JSON-formatted response containing operation results + ''' + # Implementation here + pass +``` + +## Pydantic v2 Key Features + +- Use `model_config` instead of nested `Config` class +- Use `field_validator` instead of deprecated `validator` +- Use `model_dump()` instead of deprecated `dict()` +- Validators require `@classmethod` decorator +- Type hints are required for validator methods + +```python +from pydantic import BaseModel, Field, field_validator, ConfigDict + +class CreateUserInput(BaseModel): + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True + ) + + name: str = Field(..., description="User's full name", min_length=1, max_length=100) + email: str = Field(..., description="User's email address", pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$') + age: int = Field(..., description="User's age", ge=0, le=150) + + @field_validator('email') + @classmethod + def validate_email(cls, v: str) -> str: + if not v.strip(): + raise ValueError("Email cannot be empty") + return v.lower() +``` + +## Response Format Options + +Support multiple output formats for flexibility: + +```python +from enum import Enum + +class ResponseFormat(str, Enum): + '''Output format for tool responses.''' + MARKDOWN = "markdown" + JSON = "json" + +class UserSearchInput(BaseModel): + query: str = Field(..., description="Search query") + response_format: ResponseFormat = Field( + default=ResponseFormat.MARKDOWN, + description="Output format: 'markdown' for human-readable or 'json' for machine-readable" + ) +``` + +**Markdown format**: +- Use headers, lists, and formatting for clarity +- Convert timestamps to human-readable format (e.g., "2024-01-15 10:30:00 UTC" instead of epoch) +- Show display names with IDs in parentheses (e.g., "@john.doe (U123456)") +- Omit verbose metadata (e.g., show only one profile image URL, not all sizes) +- Group related information logically + +**JSON format**: +- Return complete, structured data suitable for programmatic processing +- Include all available fields and metadata +- Use consistent field names and types + +## Pagination Implementation + +For tools that list resources: + +```python +class ListInput(BaseModel): + limit: Optional[int] = Field(default=20, description="Maximum results to return", ge=1, le=100) + offset: Optional[int] = Field(default=0, description="Number of results to skip for pagination", ge=0) + +async def list_items(params: ListInput) -> str: + # Make API request with pagination + data = await api_request(limit=params.limit, offset=params.offset) + + # Return pagination info + response = { + "total": data["total"], + "count": len(data["items"]), + "offset": params.offset, + "items": data["items"], + "has_more": data["total"] > params.offset + len(data["items"]), + "next_offset": params.offset + len(data["items"]) if data["total"] > params.offset + len(data["items"]) else None + } + return json.dumps(response, indent=2) +``` + +## Error Handling + +Provide clear, actionable error messages: + +```python +def _handle_api_error(e: Exception) -> str: + '''Consistent error formatting across all tools.''' + if isinstance(e, httpx.HTTPStatusError): + if e.response.status_code == 404: + return "Error: Resource not found. Please check the ID is correct." + elif e.response.status_code == 403: + return "Error: Permission denied. You don't have access to this resource." + elif e.response.status_code == 429: + return "Error: Rate limit exceeded. Please wait before making more requests." + return f"Error: API request failed with status {e.response.status_code}" + elif isinstance(e, httpx.TimeoutException): + return "Error: Request timed out. Please try again." + return f"Error: Unexpected error occurred: {type(e).__name__}" +``` + +## Shared Utilities + +Extract common functionality into reusable functions: + +```python +# Shared API request function +async def _make_api_request(endpoint: str, method: str = "GET", **kwargs) -> dict: + '''Reusable function for all API calls.''' + async with httpx.AsyncClient() as client: + response = await client.request( + method, + f"{API_BASE_URL}/{endpoint}", + timeout=30.0, + **kwargs + ) + response.raise_for_status() + return response.json() +``` + +## Async/Await Best Practices + +Always use async/await for network requests and I/O operations: + +```python +# Good: Async network request +async def fetch_data(resource_id: str) -> dict: + async with httpx.AsyncClient() as client: + response = await client.get(f"{API_URL}/resource/{resource_id}") + response.raise_for_status() + return response.json() + +# Bad: Synchronous request +def fetch_data(resource_id: str) -> dict: + response = requests.get(f"{API_URL}/resource/{resource_id}") # Blocks + return response.json() +``` + +## Type Hints + +Use type hints throughout: + +```python +from typing import Optional, List, Dict, Any + +async def get_user(user_id: str) -> Dict[str, Any]: + data = await fetch_user(user_id) + return {"id": data["id"], "name": data["name"]} +``` + +## Tool Docstrings + +Every tool must have comprehensive docstrings with explicit type information: + +```python +async def search_users(params: UserSearchInput) -> str: + ''' + Search for users in the Example system by name, email, or team. + + This tool searches across all user profiles in the Example platform, + supporting partial matches and various search filters. It does NOT + create or modify users, only searches existing ones. + + Args: + params (UserSearchInput): Validated input parameters containing: + - query (str): Search string to match against names/emails (e.g., "john", "@example.com", "team:marketing") + - limit (Optional[int]): Maximum results to return, between 1-100 (default: 20) + - offset (Optional[int]): Number of results to skip for pagination (default: 0) + + Returns: + str: JSON-formatted string containing search results with the following schema: + + Success response: + { + "total": int, # Total number of matches found + "count": int, # Number of results in this response + "offset": int, # Current pagination offset + "users": [ + { + "id": str, # User ID (e.g., "U123456789") + "name": str, # Full name (e.g., "John Doe") + "email": str, # Email address (e.g., "john@example.com") + "team": str # Team name (e.g., "Marketing") - optional + } + ] + } + + Error response: + "Error: " or "No users found matching ''" + + Examples: + - Use when: "Find all marketing team members" -> params with query="team:marketing" + - Use when: "Search for John's account" -> params with query="john" + - Don't use when: You need to create a user (use example_create_user instead) + - Don't use when: You have a user ID and need full details (use example_get_user instead) + + Error Handling: + - Input validation errors are handled by Pydantic model + - Returns "Error: Rate limit exceeded" if too many requests (429 status) + - Returns "Error: Invalid API authentication" if API key is invalid (401 status) + - Returns formatted list of results or "No users found matching 'query'" + ''' +``` + +## Complete Example + +See below for a complete Python MCP server example: + +```python +#!/usr/bin/env python3 +''' +MCP Server for Example Service. + +This server provides tools to interact with Example API, including user search, +project management, and data export capabilities. +''' + +from typing import Optional, List, Dict, Any +from enum import Enum +import httpx +from pydantic import BaseModel, Field, field_validator, ConfigDict +from mcp.server.fastmcp import FastMCP + +# Initialize the MCP server +mcp = FastMCP("example_mcp") + +# Constants +API_BASE_URL = "https://api.example.com/v1" + +# Enums +class ResponseFormat(str, Enum): + '''Output format for tool responses.''' + MARKDOWN = "markdown" + JSON = "json" + +# Pydantic Models for Input Validation +class UserSearchInput(BaseModel): + '''Input model for user search operations.''' + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True + ) + + query: str = Field(..., description="Search string to match against names/emails", min_length=2, max_length=200) + limit: Optional[int] = Field(default=20, description="Maximum results to return", ge=1, le=100) + offset: Optional[int] = Field(default=0, description="Number of results to skip for pagination", ge=0) + response_format: ResponseFormat = Field(default=ResponseFormat.MARKDOWN, description="Output format") + + @field_validator('query') + @classmethod + def validate_query(cls, v: str) -> str: + if not v.strip(): + raise ValueError("Query cannot be empty or whitespace only") + return v.strip() + +# Shared utility functions +async def _make_api_request(endpoint: str, method: str = "GET", **kwargs) -> dict: + '''Reusable function for all API calls.''' + async with httpx.AsyncClient() as client: + response = await client.request( + method, + f"{API_BASE_URL}/{endpoint}", + timeout=30.0, + **kwargs + ) + response.raise_for_status() + return response.json() + +def _handle_api_error(e: Exception) -> str: + '''Consistent error formatting across all tools.''' + if isinstance(e, httpx.HTTPStatusError): + if e.response.status_code == 404: + return "Error: Resource not found. Please check the ID is correct." + elif e.response.status_code == 403: + return "Error: Permission denied. You don't have access to this resource." + elif e.response.status_code == 429: + return "Error: Rate limit exceeded. Please wait before making more requests." + return f"Error: API request failed with status {e.response.status_code}" + elif isinstance(e, httpx.TimeoutException): + return "Error: Request timed out. Please try again." + return f"Error: Unexpected error occurred: {type(e).__name__}" + +# Tool definitions +@mcp.tool( + name="example_search_users", + annotations={ + "title": "Search Example Users", + "readOnlyHint": True, + "destructiveHint": False, + "idempotentHint": True, + "openWorldHint": True + } +) +async def example_search_users(params: UserSearchInput) -> str: + '''Search for users in the Example system by name, email, or team. + + [Full docstring as shown above] + ''' + try: + # Make API request using validated parameters + data = await _make_api_request( + "users/search", + params={ + "q": params.query, + "limit": params.limit, + "offset": params.offset + } + ) + + users = data.get("users", []) + total = data.get("total", 0) + + if not users: + return f"No users found matching '{params.query}'" + + # Format response based on requested format + if params.response_format == ResponseFormat.MARKDOWN: + lines = [f"# User Search Results: '{params.query}'", ""] + lines.append(f"Found {total} users (showing {len(users)})") + lines.append("") + + for user in users: + lines.append(f"## {user['name']} ({user['id']})") + lines.append(f"- **Email**: {user['email']}") + if user.get('team'): + lines.append(f"- **Team**: {user['team']}") + lines.append("") + + return "\n".join(lines) + + else: + # Machine-readable JSON format + import json + response = { + "total": total, + "count": len(users), + "offset": params.offset, + "users": users + } + return json.dumps(response, indent=2) + + except Exception as e: + return _handle_api_error(e) + +if __name__ == "__main__": + mcp.run() +``` + +--- + +## Advanced FastMCP Features + +### Context Parameter Injection + +FastMCP can automatically inject a `Context` parameter into tools for advanced capabilities like logging, progress reporting, resource reading, and user interaction: + +```python +from mcp.server.fastmcp import FastMCP, Context + +mcp = FastMCP("example_mcp") + +@mcp.tool() +async def advanced_search(query: str, ctx: Context) -> str: + '''Advanced tool with context access for logging and progress.''' + + # Report progress for long operations + await ctx.report_progress(0.25, "Starting search...") + + # Log information for debugging + await ctx.log_info("Processing query", {"query": query, "timestamp": datetime.now()}) + + # Perform search + results = await search_api(query) + await ctx.report_progress(0.75, "Formatting results...") + + # Access server configuration + server_name = ctx.fastmcp.name + + return format_results(results) + +@mcp.tool() +async def interactive_tool(resource_id: str, ctx: Context) -> str: + '''Tool that can request additional input from users.''' + + # Request sensitive information when needed + api_key = await ctx.elicit( + prompt="Please provide your API key:", + input_type="password" + ) + + # Use the provided key + return await api_call(resource_id, api_key) +``` + +**Context capabilities:** +- `ctx.report_progress(progress, message)` - Report progress for long operations +- `ctx.log_info(message, data)` / `ctx.log_error()` / `ctx.log_debug()` - Logging +- `ctx.elicit(prompt, input_type)` - Request input from users +- `ctx.fastmcp.name` - Access server configuration +- `ctx.read_resource(uri)` - Read MCP resources + +### Resource Registration + +Expose data as resources for efficient, template-based access: + +```python +@mcp.resource("file://documents/{name}") +async def get_document(name: str) -> str: + '''Expose documents as MCP resources. + + Resources are useful for static or semi-static data that doesn't + require complex parameters. They use URI templates for flexible access. + ''' + document_path = f"./docs/{name}" + with open(document_path, "r") as f: + return f.read() + +@mcp.resource("config://settings/{key}") +async def get_setting(key: str, ctx: Context) -> str: + '''Expose configuration as resources with context.''' + settings = await load_settings() + return json.dumps(settings.get(key, {})) +``` + +**When to use Resources vs Tools:** +- **Resources**: For data access with simple parameters (URI templates) +- **Tools**: For complex operations with validation and business logic + +### Structured Output Types + +FastMCP supports multiple return types beyond strings: + +```python +from typing import TypedDict +from dataclasses import dataclass +from pydantic import BaseModel + +# TypedDict for structured returns +class UserData(TypedDict): + id: str + name: str + email: str + +@mcp.tool() +async def get_user_typed(user_id: str) -> UserData: + '''Returns structured data - FastMCP handles serialization.''' + return {"id": user_id, "name": "John Doe", "email": "john@example.com"} + +# Pydantic models for complex validation +class DetailedUser(BaseModel): + id: str + name: str + email: str + created_at: datetime + metadata: Dict[str, Any] + +@mcp.tool() +async def get_user_detailed(user_id: str) -> DetailedUser: + '''Returns Pydantic model - automatically generates schema.''' + user = await fetch_user(user_id) + return DetailedUser(**user) +``` + +### Lifespan Management + +Initialize resources that persist across requests: + +```python +from contextlib import asynccontextmanager + +@asynccontextmanager +async def app_lifespan(): + '''Manage resources that live for the server's lifetime.''' + # Initialize connections, load config, etc. + db = await connect_to_database() + config = load_configuration() + + # Make available to all tools + yield {"db": db, "config": config} + + # Cleanup on shutdown + await db.close() + +mcp = FastMCP("example_mcp", lifespan=app_lifespan) + +@mcp.tool() +async def query_data(query: str, ctx: Context) -> str: + '''Access lifespan resources through context.''' + db = ctx.request_context.lifespan_state["db"] + results = await db.query(query) + return format_results(results) +``` + +### Transport Options + +FastMCP supports two main transport mechanisms: + +```python +# stdio transport (for local tools) - default +if __name__ == "__main__": + mcp.run() + +# Streamable HTTP transport (for remote servers) +if __name__ == "__main__": + mcp.run(transport="streamable_http", port=8000) +``` + +**Transport selection:** +- **stdio**: Command-line tools, local integrations, subprocess execution +- **Streamable HTTP**: Web services, remote access, multiple clients + +--- + +## Code Best Practices + +### Code Composability and Reusability + +Your implementation MUST prioritize composability and code reuse: + +1. **Extract Common Functionality**: + - Create reusable helper functions for operations used across multiple tools + - Build shared API clients for HTTP requests instead of duplicating code + - Centralize error handling logic in utility functions + - Extract business logic into dedicated functions that can be composed + - Extract shared markdown or JSON field selection & formatting functionality + +2. **Avoid Duplication**: + - NEVER copy-paste similar code between tools + - If you find yourself writing similar logic twice, extract it into a function + - Common operations like pagination, filtering, field selection, and formatting should be shared + - Authentication/authorization logic should be centralized + +### Python-Specific Best Practices + +1. **Use Type Hints**: Always include type annotations for function parameters and return values +2. **Pydantic Models**: Define clear Pydantic models for all input validation +3. **Avoid Manual Validation**: Let Pydantic handle input validation with constraints +4. **Proper Imports**: Group imports (standard library, third-party, local) +5. **Error Handling**: Use specific exception types (httpx.HTTPStatusError, not generic Exception) +6. **Async Context Managers**: Use `async with` for resources that need cleanup +7. **Constants**: Define module-level constants in UPPER_CASE + +## Quality Checklist + +Before finalizing your Python MCP server implementation, ensure: + +### Strategic Design +- [ ] Tools enable complete workflows, not just API endpoint wrappers +- [ ] Tool names reflect natural task subdivisions +- [ ] Response formats optimize for agent context efficiency +- [ ] Human-readable identifiers used where appropriate +- [ ] Error messages guide agents toward correct usage + +### Implementation Quality +- [ ] FOCUSED IMPLEMENTATION: Most important and valuable tools implemented +- [ ] All tools have descriptive names and documentation +- [ ] Return types are consistent across similar operations +- [ ] Error handling is implemented for all external calls +- [ ] Server name follows format: `{service}_mcp` +- [ ] All network operations use async/await +- [ ] Common functionality is extracted into reusable functions +- [ ] Error messages are clear, actionable, and educational +- [ ] Outputs are properly validated and formatted + +### Tool Configuration +- [ ] All tools implement 'name' and 'annotations' in the decorator +- [ ] Annotations correctly set (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) +- [ ] All tools use Pydantic BaseModel for input validation with Field() definitions +- [ ] All Pydantic Fields have explicit types and descriptions with constraints +- [ ] All tools have comprehensive docstrings with explicit input/output types +- [ ] Docstrings include complete schema structure for dict/JSON returns +- [ ] Pydantic models handle input validation (no manual validation needed) + +### Advanced Features (where applicable) +- [ ] Context injection used for logging, progress, or elicitation +- [ ] Resources registered for appropriate data endpoints +- [ ] Lifespan management implemented for persistent connections +- [ ] Structured output types used (TypedDict, Pydantic models) +- [ ] Appropriate transport configured (stdio or streamable HTTP) + +### Code Quality +- [ ] File includes proper imports including Pydantic imports +- [ ] Pagination is properly implemented where applicable +- [ ] Filtering options are provided for potentially large result sets +- [ ] All async functions are properly defined with `async def` +- [ ] HTTP client usage follows async patterns with proper context managers +- [ ] Type hints are used throughout the code +- [ ] Constants are defined at module level in UPPER_CASE + +### Testing +- [ ] Server runs successfully: `python your_server.py --help` +- [ ] All imports resolve correctly +- [ ] Sample tool calls work as expected +- [ ] Error scenarios handled gracefully \ No newline at end of file diff --git a/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/scripts/connections.py b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/scripts/connections.py new file mode 100644 index 00000000..ffcd0da3 --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/scripts/connections.py @@ -0,0 +1,151 @@ +"""Lightweight connection handling for MCP servers.""" + +from abc import ABC, abstractmethod +from contextlib import AsyncExitStack +from typing import Any + +from mcp import ClientSession, StdioServerParameters +from mcp.client.sse import sse_client +from mcp.client.stdio import stdio_client +from mcp.client.streamable_http import streamablehttp_client + + +class MCPConnection(ABC): + """Base class for MCP server connections.""" + + def __init__(self): + self.session = None + self._stack = None + + @abstractmethod + def _create_context(self): + """Create the connection context based on connection type.""" + + async def __aenter__(self): + """Initialize MCP server connection.""" + self._stack = AsyncExitStack() + await self._stack.__aenter__() + + try: + ctx = self._create_context() + result = await self._stack.enter_async_context(ctx) + + if len(result) == 2: + read, write = result + elif len(result) == 3: + read, write, _ = result + else: + raise ValueError(f"Unexpected context result: {result}") + + session_ctx = ClientSession(read, write) + self.session = await self._stack.enter_async_context(session_ctx) + await self.session.initialize() + return self + except BaseException: + await self._stack.__aexit__(None, None, None) + raise + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Clean up MCP server connection resources.""" + if self._stack: + await self._stack.__aexit__(exc_type, exc_val, exc_tb) + self.session = None + self._stack = None + + async def list_tools(self) -> list[dict[str, Any]]: + """Retrieve available tools from the MCP server.""" + response = await self.session.list_tools() + return [ + { + "name": tool.name, + "description": tool.description, + "input_schema": tool.inputSchema, + } + for tool in response.tools + ] + + async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: + """Call a tool on the MCP server with provided arguments.""" + result = await self.session.call_tool(tool_name, arguments=arguments) + return result.content + + +class MCPConnectionStdio(MCPConnection): + """MCP connection using standard input/output.""" + + def __init__(self, command: str, args: list[str] = None, env: dict[str, str] = None): + super().__init__() + self.command = command + self.args = args or [] + self.env = env + + def _create_context(self): + return stdio_client( + StdioServerParameters(command=self.command, args=self.args, env=self.env) + ) + + +class MCPConnectionSSE(MCPConnection): + """MCP connection using Server-Sent Events.""" + + def __init__(self, url: str, headers: dict[str, str] = None): + super().__init__() + self.url = url + self.headers = headers or {} + + def _create_context(self): + return sse_client(url=self.url, headers=self.headers) + + +class MCPConnectionHTTP(MCPConnection): + """MCP connection using Streamable HTTP.""" + + def __init__(self, url: str, headers: dict[str, str] = None): + super().__init__() + self.url = url + self.headers = headers or {} + + def _create_context(self): + return streamablehttp_client(url=self.url, headers=self.headers) + + +def create_connection( + transport: str, + command: str = None, + args: list[str] = None, + env: dict[str, str] = None, + url: str = None, + headers: dict[str, str] = None, +) -> MCPConnection: + """Factory function to create the appropriate MCP connection. + + Args: + transport: Connection type ("stdio", "sse", or "http") + command: Command to run (stdio only) + args: Command arguments (stdio only) + env: Environment variables (stdio only) + url: Server URL (sse and http only) + headers: HTTP headers (sse and http only) + + Returns: + MCPConnection instance + """ + transport = transport.lower() + + if transport == "stdio": + if not command: + raise ValueError("Command is required for stdio transport") + return MCPConnectionStdio(command=command, args=args, env=env) + + elif transport == "sse": + if not url: + raise ValueError("URL is required for sse transport") + return MCPConnectionSSE(url=url, headers=headers) + + elif transport in ["http", "streamable_http", "streamable-http"]: + if not url: + raise ValueError("URL is required for http transport") + return MCPConnectionHTTP(url=url, headers=headers) + + else: + raise ValueError(f"Unsupported transport type: {transport}. Use 'stdio', 'sse', or 'http'") diff --git a/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/scripts/evaluation.py b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/scripts/evaluation.py new file mode 100644 index 00000000..41778569 --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/scripts/evaluation.py @@ -0,0 +1,373 @@ +"""MCP Server Evaluation Harness + +This script evaluates MCP servers by running test questions against them using Claude. +""" + +import argparse +import asyncio +import json +import re +import sys +import time +import traceback +import xml.etree.ElementTree as ET +from pathlib import Path +from typing import Any + +from anthropic import Anthropic + +from connections import create_connection + +EVALUATION_PROMPT = """You are an AI assistant with access to tools. + +When given a task, you MUST: +1. Use the available tools to complete the task +2. Provide summary of each step in your approach, wrapped in tags +3. Provide feedback on the tools provided, wrapped in tags +4. Provide your final response, wrapped in tags + +Summary Requirements: +- In your tags, you must explain: + - The steps you took to complete the task + - Which tools you used, in what order, and why + - The inputs you provided to each tool + - The outputs you received from each tool + - A summary for how you arrived at the response + +Feedback Requirements: +- In your tags, provide constructive feedback on the tools: + - Comment on tool names: Are they clear and descriptive? + - Comment on input parameters: Are they well-documented? Are required vs optional parameters clear? + - Comment on descriptions: Do they accurately describe what the tool does? + - Comment on any errors encountered during tool usage: Did the tool fail to execute? Did the tool return too many tokens? + - Identify specific areas for improvement and explain WHY they would help + - Be specific and actionable in your suggestions + +Response Requirements: +- Your response should be concise and directly address what was asked +- Always wrap your final response in tags +- If you cannot solve the task return NOT_FOUND +- For numeric responses, provide just the number +- For IDs, provide just the ID +- For names or text, provide the exact text requested +- Your response should go last""" + + +def parse_evaluation_file(file_path: Path) -> list[dict[str, Any]]: + """Parse XML evaluation file with qa_pair elements.""" + try: + tree = ET.parse(file_path) + root = tree.getroot() + evaluations = [] + + for qa_pair in root.findall(".//qa_pair"): + question_elem = qa_pair.find("question") + answer_elem = qa_pair.find("answer") + + if question_elem is not None and answer_elem is not None: + evaluations.append({ + "question": (question_elem.text or "").strip(), + "answer": (answer_elem.text or "").strip(), + }) + + return evaluations + except Exception as e: + print(f"Error parsing evaluation file {file_path}: {e}") + return [] + + +def extract_xml_content(text: str, tag: str) -> str | None: + """Extract content from XML tags.""" + pattern = rf"<{tag}>(.*?)" + matches = re.findall(pattern, text, re.DOTALL) + return matches[-1].strip() if matches else None + + +async def agent_loop( + client: Anthropic, + model: str, + question: str, + tools: list[dict[str, Any]], + connection: Any, +) -> tuple[str, dict[str, Any]]: + """Run the agent loop with MCP tools.""" + messages = [{"role": "user", "content": question}] + + response = await asyncio.to_thread( + client.messages.create, + model=model, + max_tokens=4096, + system=EVALUATION_PROMPT, + messages=messages, + tools=tools, + ) + + messages.append({"role": "assistant", "content": response.content}) + + tool_metrics = {} + + while response.stop_reason == "tool_use": + tool_use = next(block for block in response.content if block.type == "tool_use") + tool_name = tool_use.name + tool_input = tool_use.input + + tool_start_ts = time.time() + try: + tool_result = await connection.call_tool(tool_name, tool_input) + tool_response = json.dumps(tool_result) if isinstance(tool_result, (dict, list)) else str(tool_result) + except Exception as e: + tool_response = f"Error executing tool {tool_name}: {str(e)}\n" + tool_response += traceback.format_exc() + tool_duration = time.time() - tool_start_ts + + if tool_name not in tool_metrics: + tool_metrics[tool_name] = {"count": 0, "durations": []} + tool_metrics[tool_name]["count"] += 1 + tool_metrics[tool_name]["durations"].append(tool_duration) + + messages.append({ + "role": "user", + "content": [{ + "type": "tool_result", + "tool_use_id": tool_use.id, + "content": tool_response, + }] + }) + + response = await asyncio.to_thread( + client.messages.create, + model=model, + max_tokens=4096, + system=EVALUATION_PROMPT, + messages=messages, + tools=tools, + ) + messages.append({"role": "assistant", "content": response.content}) + + response_text = next( + (block.text for block in response.content if hasattr(block, "text")), + None, + ) + return response_text, tool_metrics + + +async def evaluate_single_task( + client: Anthropic, + model: str, + qa_pair: dict[str, Any], + tools: list[dict[str, Any]], + connection: Any, + task_index: int, +) -> dict[str, Any]: + """Evaluate a single QA pair with the given tools.""" + start_time = time.time() + + print(f"Task {task_index + 1}: Running task with question: {qa_pair['question']}") + response, tool_metrics = await agent_loop(client, model, qa_pair["question"], tools, connection) + + response_value = extract_xml_content(response, "response") + summary = extract_xml_content(response, "summary") + feedback = extract_xml_content(response, "feedback") + + duration_seconds = time.time() - start_time + + return { + "question": qa_pair["question"], + "expected": qa_pair["answer"], + "actual": response_value, + "score": int(response_value == qa_pair["answer"]) if response_value else 0, + "total_duration": duration_seconds, + "tool_calls": tool_metrics, + "num_tool_calls": sum(len(metrics["durations"]) for metrics in tool_metrics.values()), + "summary": summary, + "feedback": feedback, + } + + +REPORT_HEADER = """ +# Evaluation Report + +## Summary + +- **Accuracy**: {correct}/{total} ({accuracy:.1f}%) +- **Average Task Duration**: {average_duration_s:.2f}s +- **Average Tool Calls per Task**: {average_tool_calls:.2f} +- **Total Tool Calls**: {total_tool_calls} + +--- +""" + +TASK_TEMPLATE = """ +### Task {task_num} + +**Question**: {question} +**Ground Truth Answer**: `{expected_answer}` +**Actual Answer**: `{actual_answer}` +**Correct**: {correct_indicator} +**Duration**: {total_duration:.2f}s +**Tool Calls**: {tool_calls} + +**Summary** +{summary} + +**Feedback** +{feedback} + +--- +""" + + +async def run_evaluation( + eval_path: Path, + connection: Any, + model: str = "claude-3-7-sonnet-20250219", +) -> str: + """Run evaluation with MCP server tools.""" + print("๐Ÿš€ Starting Evaluation") + + client = Anthropic() + + tools = await connection.list_tools() + print(f"๐Ÿ“‹ Loaded {len(tools)} tools from MCP server") + + qa_pairs = parse_evaluation_file(eval_path) + print(f"๐Ÿ“‹ Loaded {len(qa_pairs)} evaluation tasks") + + results = [] + for i, qa_pair in enumerate(qa_pairs): + print(f"Processing task {i + 1}/{len(qa_pairs)}") + result = await evaluate_single_task(client, model, qa_pair, tools, connection, i) + results.append(result) + + correct = sum(r["score"] for r in results) + accuracy = (correct / len(results)) * 100 if results else 0 + average_duration_s = sum(r["total_duration"] for r in results) / len(results) if results else 0 + average_tool_calls = sum(r["num_tool_calls"] for r in results) / len(results) if results else 0 + total_tool_calls = sum(r["num_tool_calls"] for r in results) + + report = REPORT_HEADER.format( + correct=correct, + total=len(results), + accuracy=accuracy, + average_duration_s=average_duration_s, + average_tool_calls=average_tool_calls, + total_tool_calls=total_tool_calls, + ) + + report += "".join([ + TASK_TEMPLATE.format( + task_num=i + 1, + question=qa_pair["question"], + expected_answer=qa_pair["answer"], + actual_answer=result["actual"] or "N/A", + correct_indicator="โœ…" if result["score"] else "โŒ", + total_duration=result["total_duration"], + tool_calls=json.dumps(result["tool_calls"], indent=2), + summary=result["summary"] or "N/A", + feedback=result["feedback"] or "N/A", + ) + for i, (qa_pair, result) in enumerate(zip(qa_pairs, results)) + ]) + + return report + + +def parse_headers(header_list: list[str]) -> dict[str, str]: + """Parse header strings in format 'Key: Value' into a dictionary.""" + headers = {} + if not header_list: + return headers + + for header in header_list: + if ":" in header: + key, value = header.split(":", 1) + headers[key.strip()] = value.strip() + else: + print(f"Warning: Ignoring malformed header: {header}") + return headers + + +def parse_env_vars(env_list: list[str]) -> dict[str, str]: + """Parse environment variable strings in format 'KEY=VALUE' into a dictionary.""" + env = {} + if not env_list: + return env + + for env_var in env_list: + if "=" in env_var: + key, value = env_var.split("=", 1) + env[key.strip()] = value.strip() + else: + print(f"Warning: Ignoring malformed environment variable: {env_var}") + return env + + +async def main(): + parser = argparse.ArgumentParser( + description="Evaluate MCP servers using test questions", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Evaluate a local stdio MCP server + python evaluation.py -t stdio -c python -a my_server.py eval.xml + + # Evaluate an SSE MCP server + python evaluation.py -t sse -u https://example.com/mcp -H "Authorization: Bearer token" eval.xml + + # Evaluate an HTTP MCP server with custom model + python evaluation.py -t http -u https://example.com/mcp -m claude-3-5-sonnet-20241022 eval.xml + """, + ) + + parser.add_argument("eval_file", type=Path, help="Path to evaluation XML file") + parser.add_argument("-t", "--transport", choices=["stdio", "sse", "http"], default="stdio", help="Transport type (default: stdio)") + parser.add_argument("-m", "--model", default="claude-3-7-sonnet-20250219", help="Claude model to use (default: claude-3-7-sonnet-20250219)") + + stdio_group = parser.add_argument_group("stdio options") + stdio_group.add_argument("-c", "--command", help="Command to run MCP server (stdio only)") + stdio_group.add_argument("-a", "--args", nargs="+", help="Arguments for the command (stdio only)") + stdio_group.add_argument("-e", "--env", nargs="+", help="Environment variables in KEY=VALUE format (stdio only)") + + remote_group = parser.add_argument_group("sse/http options") + remote_group.add_argument("-u", "--url", help="MCP server URL (sse/http only)") + remote_group.add_argument("-H", "--header", nargs="+", dest="headers", help="HTTP headers in 'Key: Value' format (sse/http only)") + + parser.add_argument("-o", "--output", type=Path, help="Output file for evaluation report (default: stdout)") + + args = parser.parse_args() + + if not args.eval_file.exists(): + print(f"Error: Evaluation file not found: {args.eval_file}") + sys.exit(1) + + headers = parse_headers(args.headers) if args.headers else None + env_vars = parse_env_vars(args.env) if args.env else None + + try: + connection = create_connection( + transport=args.transport, + command=args.command, + args=args.args, + env=env_vars, + url=args.url, + headers=headers, + ) + except ValueError as e: + print(f"Error: {e}") + sys.exit(1) + + print(f"๐Ÿ”— Connecting to MCP server via {args.transport}...") + + async with connection: + print("โœ… Connected successfully") + report = await run_evaluation(args.eval_file, connection, args.model) + + if args.output: + args.output.write_text(report) + print(f"\nโœ… Report saved to {args.output}") + else: + print("\n" + report) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/scripts/example_evaluation.xml b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/scripts/example_evaluation.xml new file mode 100644 index 00000000..41e4459b --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/scripts/example_evaluation.xml @@ -0,0 +1,22 @@ + + + Calculate the compound interest on $10,000 invested at 5% annual interest rate, compounded monthly for 3 years. What is the final amount in dollars (rounded to 2 decimal places)? + 11614.72 + + + A projectile is launched at a 45-degree angle with an initial velocity of 50 m/s. Calculate the total distance (in meters) it has traveled from the launch point after 2 seconds, assuming g=9.8 m/sยฒ. Round to 2 decimal places. + 87.25 + + + A sphere has a volume of 500 cubic meters. Calculate its surface area in square meters. Round to 2 decimal places. + 304.65 + + + Calculate the population standard deviation of this dataset: [12, 15, 18, 22, 25, 30, 35]. Round to 2 decimal places. + 7.61 + + + Calculate the pH of a solution with a hydrogen ion concentration of 3.5 ร— 10^-5 M. Round to 2 decimal places. + 4.46 + + diff --git a/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/scripts/requirements.txt b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/scripts/requirements.txt new file mode 100644 index 00000000..e73e5d1e --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/skills/mcp-builder/scripts/requirements.txt @@ -0,0 +1,2 @@ +anthropic>=0.39.0 +mcp>=1.1.0 diff --git a/plugins/antigravity-bundle-agent-architect/skills/prompt-engineering/SKILL.md b/plugins/antigravity-bundle-agent-architect/skills/prompt-engineering/SKILL.md new file mode 100644 index 00000000..959d7e28 --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/skills/prompt-engineering/SKILL.md @@ -0,0 +1,177 @@ +--- +name: prompt-engineering +description: "Expert guide on prompt engineering patterns, best practices, and optimization techniques. Use when user wants to improve prompts, learn prompting strategies, or debug agent behavior." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Prompt Engineering Patterns + +Advanced prompt engineering techniques to maximize LLM performance, reliability, and controllability. + +## Core Capabilities + +### 1. Few-Shot Learning + +Teach the model by showing examples instead of explaining rules. Include 2-5 input-output pairs that demonstrate the desired behavior. Use when you need consistent formatting, specific reasoning patterns, or handling of edge cases. More examples improve accuracy but consume tokensโ€”balance based on task complexity. + +**Example:** + +```markdown +Extract key information from support tickets: + +Input: "My login doesn't work and I keep getting error 403" +Output: {"issue": "authentication", "error_code": "403", "priority": "high"} + +Input: "Feature request: add dark mode to settings" +Output: {"issue": "feature_request", "error_code": null, "priority": "low"} + +Now process: "Can't upload files larger than 10MB, getting timeout" +``` + +### 2. Chain-of-Thought Prompting + +Request step-by-step reasoning before the final answer. Add "Let's think step by step" (zero-shot) or include example reasoning traces (few-shot). Use for complex problems requiring multi-step logic, mathematical reasoning, or when you need to verify the model's thought process. Improves accuracy on analytical tasks by 30-50%. + +**Example:** + +```markdown +Analyze this bug report and determine root cause. + +Think step by step: + +1. What is the expected behavior? +2. What is the actual behavior? +3. What changed recently that could cause this? +4. What components are involved? +5. What is the most likely root cause? + +Bug: "Users can't save drafts after the cache update deployed yesterday" +``` + +### 3. Prompt Optimization + +Systematically improve prompts through testing and refinement. Start simple, measure performance (accuracy, consistency, token usage), then iterate. Test on diverse inputs including edge cases. Use A/B testing to compare variations. Critical for production prompts where consistency and cost matter. + +**Example:** + +```markdown +Version 1 (Simple): "Summarize this article" +โ†’ Result: Inconsistent length, misses key points + +Version 2 (Add constraints): "Summarize in 3 bullet points" +โ†’ Result: Better structure, but still misses nuance + +Version 3 (Add reasoning): "Identify the 3 main findings, then summarize each" +โ†’ Result: Consistent, accurate, captures key information +``` + +### 4. Template Systems + +Build reusable prompt structures with variables, conditional sections, and modular components. Use for multi-turn conversations, role-based interactions, or when the same pattern applies to different inputs. Reduces duplication and ensures consistency across similar tasks. + +**Example:** + +```python +# Reusable code review template +template = """ +Review this {language} code for {focus_area}. + +Code: +{code_block} + +Provide feedback on: +{checklist} +""" + +# Usage +prompt = template.format( + language="Python", + focus_area="security vulnerabilities", + code_block=user_code, + checklist="1. SQL injection\n2. XSS risks\n3. Authentication" +) +``` + +### 5. System Prompt Design + +Set global behavior and constraints that persist across the conversation. Define the model's role, expertise level, output format, and safety guidelines. Use system prompts for stable instructions that shouldn't change turn-to-turn, freeing up user message tokens for variable content. + +**Example:** + +```markdown +System: You are a senior backend engineer specializing in API design. + +Rules: + +- Always consider scalability and performance +- Suggest RESTful patterns by default +- Flag security concerns immediately +- Provide code examples in Python +- Use early return pattern + +Format responses as: + +1. Analysis +2. Recommendation +3. Code example +4. Trade-offs +``` + +## Key Patterns + +### Progressive Disclosure + +Start with simple prompts, add complexity only when needed: + +1. **Level 1**: Direct instruction + + - "Summarize this article" + +2. **Level 2**: Add constraints + + - "Summarize this article in 3 bullet points, focusing on key findings" + +3. **Level 3**: Add reasoning + + - "Read this article, identify the main findings, then summarize in 3 bullet points" + +4. **Level 4**: Add examples + - Include 2-3 example summaries with input-output pairs + +### Instruction Hierarchy + +``` +[System Context] โ†’ [Task Instruction] โ†’ [Examples] โ†’ [Input Data] โ†’ [Output Format] +``` + +### Error Recovery + +Build prompts that gracefully handle failures: + +- Include fallback instructions +- Request confidence scores +- Ask for alternative interpretations when uncertain +- Specify how to indicate missing information + +## Best Practices + +1. **Be Specific**: Vague prompts produce inconsistent results +2. **Show, Don't Tell**: Examples are more effective than descriptions +3. **Test Extensively**: Evaluate on diverse, representative inputs +4. **Iterate Rapidly**: Small changes can have large impacts +5. **Monitor Performance**: Track metrics in production +6. **Version Control**: Treat prompts as code with proper versioning +7. **Document Intent**: Explain why prompts are structured as they are + +## Common Pitfalls + +- **Over-engineering**: Starting with complex prompts before trying simple ones +- **Example pollution**: Using examples that don't match the target task +- **Context overflow**: Exceeding token limits with excessive examples +- **Ambiguous instructions**: Leaving room for multiple interpretations +- **Ignoring edge cases**: Not testing on unusual or boundary inputs + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-agent-architect/skills/rag-engineer/SKILL.md b/plugins/antigravity-bundle-agent-architect/skills/rag-engineer/SKILL.md new file mode 100644 index 00000000..13f541cc --- /dev/null +++ b/plugins/antigravity-bundle-agent-architect/skills/rag-engineer/SKILL.md @@ -0,0 +1,95 @@ +--- +name: rag-engineer +description: "I bridge the gap between raw documents and LLM understanding. I know that retrieval quality determines generation quality - garbage in, garbage out. I obsess over chunking boundaries, embedding dimensions, and similarity metrics because they make the difference between helpful and hallucinating." +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# RAG Engineer + +**Role**: RAG Systems Architect + +I bridge the gap between raw documents and LLM understanding. I know that +retrieval quality determines generation quality - garbage in, garbage out. +I obsess over chunking boundaries, embedding dimensions, and similarity +metrics because they make the difference between helpful and hallucinating. + +## Capabilities + +- Vector embeddings and similarity search +- Document chunking and preprocessing +- Retrieval pipeline design +- Semantic search implementation +- Context window optimization +- Hybrid search (keyword + semantic) + +## Requirements + +- LLM fundamentals +- Understanding of embeddings +- Basic NLP concepts + +## Patterns + +### Semantic Chunking + +Chunk by meaning, not arbitrary token counts + +```javascript +- Use sentence boundaries, not token limits +- Detect topic shifts with embedding similarity +- Preserve document structure (headers, paragraphs) +- Include overlap for context continuity +- Add metadata for filtering +``` + +### Hierarchical Retrieval + +Multi-level retrieval for better precision + +```javascript +- Index at multiple chunk sizes (paragraph, section, document) +- First pass: coarse retrieval for candidates +- Second pass: fine-grained retrieval for precision +- Use parent-child relationships for context +``` + +### Hybrid Search + +Combine semantic and keyword search + +```javascript +- BM25/TF-IDF for keyword matching +- Vector similarity for semantic matching +- Reciprocal Rank Fusion for combining scores +- Weight tuning based on query type +``` + +## Anti-Patterns + +### โŒ Fixed Chunk Size + +### โŒ Embedding Everything + +### โŒ Ignoring Evaluation + +## โš ๏ธ Sharp Edges + +| Issue | Severity | Solution | +|-------|----------|----------| +| Fixed-size chunking breaks sentences and context | high | Use semantic chunking that respects document structure: | +| Pure semantic search without metadata pre-filtering | medium | Implement hybrid filtering: | +| Using same embedding model for different content types | medium | Evaluate embeddings per content type: | +| Using first-stage retrieval results directly | medium | Add reranking step: | +| Cramming maximum context into LLM prompt | medium | Use relevance thresholds: | +| Not measuring retrieval quality separately from generation | high | Separate retrieval evaluation: | +| Not updating embeddings when source documents change | medium | Implement embedding refresh: | +| Same retrieval strategy for all query types | medium | Implement hybrid search: | + +## Related Skills + +Works well with: `ai-agents-architect`, `prompt-engineer`, `database-architect`, `backend` + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-apple-platform-design/.codex-plugin/plugin.json b/plugins/antigravity-bundle-apple-platform-design/.codex-plugin/plugin.json new file mode 100644 index 00000000..c1109a38 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-apple-platform-design", + "version": "8.10.0", + "description": "Install the \"Apple Platform Design\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "apple-platform-design", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Apple Platform Design", + "shortDescription": "Specialized Packs ยท 6 curated skills", + "longDescription": "For teams designing native-feeling Apple platform experiences. Covers Hig Foundations, Hig Patterns, and 4 more skills.", + "developerName": "sickn33 and contributors", + "category": "Specialized Packs", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/SKILL.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/SKILL.md new file mode 100644 index 00000000..a1f32ca7 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/SKILL.md @@ -0,0 +1,95 @@ +--- +name: hig-components-layout +description: Apple Human Interface Guidelines for layout and navigation components. +risk: unknown +source: community +date_added: '2026-02-27' +--- + +# Apple HIG: Layout and Navigation Components + +Check for `.claude/apple-design-context.md` before asking questions. Use existing context and only ask for information not already covered. + +## Key Principles + +1. **Organize hierarchically.** Structure information from broad categories to specific details. Sidebars for top-level sections, lists for browsable items, detail views for individual content. + +2. **Use standard navigation patterns.** Tab bars for flat navigation between peer sections (iPhone). Sidebars for deep hierarchical navigation (iPad, Mac). Match the pattern to the information architecture and platform. + +3. **Adapt to screen size.** Three-column on iPad collapses to single-column on iPhone. Use size classes and adaptive APIs (NavigationSplitView) for automatic adaptation. + +4. **Support multitasking on iPad.** Respond gracefully to Split View, Slide Over, and Stage Manager. Test at every split ratio and size class transition. + +5. **Maintain spatial consistency on visionOS.** Windows, volumes, and ornaments in shared space. Position predictably. Use ornaments for toolbars and controls without occluding content. + +6. **Use scroll views for overflow content.** Enable paging for discrete content units. Support pull-to-refresh where appropriate. Respect safe areas. + +7. **Keep navigation predictable.** Users should always know where they are, how they got there, and how to go back. Use back buttons, breadcrumbs, and clear section titles. + +8. **Prefer system components.** UINavigationController, UISplitViewController, NavigationSplitView, and TabView provide built-in adaptivity, accessibility, and state restoration. + +## Reference Index + +| Reference | Topic | Key content | +|---|---|---| +| [sidebars.md](references/sidebars.md) | Sidebars | Source lists, selection state, collapsible sections, iPad/Mac patterns | +| [column-views.md](references/column-views.md) | Column Views | Finder-style browsing, progressive disclosure through columns | +| [outline-views.md](references/outline-views.md) | Outline Views | Expandable hierarchies, disclosure triangles, tree structures | +| [split-views.md](references/split-views.md) | Split Views | Two/three column layouts, NavigationSplitView, adaptive collapse | +| [tab-views.md](references/tab-views.md) | Tab Views | Segmented tabs, page-style tabs, macOS tab grouping | +| [tab-bars.md](references/tab-bars.md) | Tab Bars | Bottom tab bars (iOS), badge counts, max tab count | +| [scroll-views.md](references/scroll-views.md) | Scroll Views | Paging, scroll indicators, content insets, pull-to-refresh | +| [windows.md](references/windows.md) | Windows | macOS/visionOS window management, sizing, full-screen, restoration | +| [panels.md](references/panels.md) | Panels | Inspector panels, utility panels, floating panels, macOS conventions | +| [lists-and-tables.md](references/lists-and-tables.md) | Lists and Tables | Plain/grouped/inset-grouped styles, swipe actions, section headers | +| [boxes.md](references/boxes.md) | Boxes | Content grouping containers, labeled boxes, macOS grouping | +| [ornaments.md](references/ornaments.md) | Ornaments | visionOS toolbar attachments, positioning, visibility | + +## Navigation Pattern Selection + +| App Structure | Recommended Pattern | Platform Adaptation | +|---|---|---| +| 3-5 peer top-level sections | Tab Bar | iPhone: bottom tab bar. iPad: sidebar (`.sidebarAdaptable`, iPadOS 18+). Mac: sidebar or toolbar tabs | +| Deep hierarchical content | Sidebar + NavigationSplitView | iPhone: single column stack. iPad: two/three columns. Mac: full multi-column | +| Deep file/folder tree | Column View | Mac: Finder-style. iPad: adaptable. iPhone: push navigation | +| Flat list with detail | Split View (two column) | iPhone: push/pop stack. iPad/Mac: primary + detail columns | +| Document-based with inspectors | Window + Panels | Mac: main window with inspector. iPad: sheet or popover | +| Spatial app with tools | Window + Ornaments | visionOS: ornaments on window. Other platforms: toolbars | + +## Layout Adaptation Checklist + +- [ ] **Compact width (iPhone portrait):** Navigation collapses to single stack? Tab bars visible? +- [ ] **Regular width (iPad landscape, Mac):** Navigation expands to sidebar + detail? Space used well? +- [ ] **Multitasking (iPad):** Adapts at every split ratio? Works in Slide Over? +- [ ] **Accessibility:** Supports Dynamic Type at all sizes? VoiceOver order logical? +- [ ] **Orientation:** Content reflows between portrait and landscape? +- [ ] **visionOS:** Windows positioned ergonomically? Ornaments accessible? Depth meaningful? + +## Output Format + +1. **Recommended navigation pattern** with rationale for the app's information architecture. +2. **Layout hierarchy** from root container down (e.g., TabView > NavigationSplitView > List > Detail). +3. **Platform adaptation** across targeted platforms and size classes. +4. **Size class behavior** at each transition. + +## Questions to Ask + +1. What is the app's information architecture? (Sections, hierarchy depth, top-level categories?) +2. How many top-level sections? +3. Which platforms? +4. Need multitasking on iPad? +5. SwiftUI or UIKit? + +## Related Skills + +- **hig-foundations** -- Layout spacing, margins, safe areas, alignment +- **hig-platforms** -- Platform-specific navigation conventions +- **hig-patterns** -- Multitasking, full-screen, and launching patterns +- **hig-components-content** -- Content displayed within layout containers + +--- + +*Built by [Raintree Technology](https://raintree.technology) ยท [More developer tools](https://raintree.technology)* + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/boxes.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/boxes.md new file mode 100644 index 00000000..df7b2bd6 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/boxes.md @@ -0,0 +1,48 @@ +--- +title: "Boxes | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/boxes + +# Boxes + +A box creates a visually distinct group of logically related information and components. + +![A stylized representation of a group of interface elements within a rounded rectangle. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/6e253271e9888e8d596d1d5b601d90f3/components-box-intro%402x.png) + +By default, a box uses a visible border or background color to separate its contents from the rest of the interface. A box can also include a title. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/boxes#Best-practices) + +**Prefer keeping a box relatively small in comparison with its containing view.** As a boxโ€™s size gets close to the size of the containing window or screen, it becomes less effective at communicating the separation of grouped content, and it can crowd other content. + +**Consider using padding and alignment to communicate additional grouping within a box.** A boxโ€™s border is a distinct visual element โ€” adding nested boxes to define subgroups can make your interface feel busy and constrained. + +## [Content](https://developer.apple.com/design/human-interface-guidelines/boxes#Content) + +**Provide a succinct introductory title if it helps clarify the boxโ€™s contents.** The appearance of a box helps people understand that its contents are related, but it might make sense to provide more detail about the relationship. Also, a title can help VoiceOver users predict the content they encounter within the box. + +**If you need a title, write a brief phrase that describes the contents.** Use sentence-style capitalization. Avoid ending punctuation unless you use a box in a settings pane, where you append a colon to the title. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/boxes#Platform-considerations) + + _No additional considerations for visionOS. Not supported in tvOS or watchOS._ + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/boxes#iOS-iPadOS) + +By default, iOS and iPadOS use the secondary and tertiary background [colors](https://developer.apple.com/design/human-interface-guidelines/color) in boxes. + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/boxes#macOS) + +By default, macOS displays a boxโ€™s title above it. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/boxes#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/boxes#Related) + +[Layout](https://developer.apple.com/design/human-interface-guidelines/layout) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/boxes#Developer-documentation) + +[`GroupBox`](https://developer.apple.com/documentation/SwiftUI/GroupBox) โ€” SwiftUI + +[`NSBox`](https://developer.apple.com/documentation/AppKit/NSBox) โ€” AppKit + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/column-views.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/column-views.md new file mode 100644 index 00000000..8ef7ab30 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/column-views.md @@ -0,0 +1,44 @@ +--- +title: "Column views | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/column-views + +# Column views + +A column view โ€” also called a _browser_ โ€” lets people view and navigate a data hierarchy using a series of vertical columns. + +![A stylized representation of three columns containing a list of folders, images, and file information. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/34ebf07677359428c45cbda0b9c1641e/components-column-view-intro%402x.png) + +Each column represents one level of the hierarchy and contains horizontal rows of data items. Within a column, any parent item that contains nested child items is marked with a triangle icon. When people select a parent, the next column displays its children. People can continue navigating in this way until they reach an item with no children, and can also navigate back up the hierarchy to explore other branches of data. + +Note + +If you need to manage the presentation of hierarchical content in your iPadOS or visionOS app, consider using a [split view](https://developer.apple.com/design/human-interface-guidelines/split-views). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/column-views#Best-practices) + +Consider using a column view when you have a deep data hierarchy in which people tend to navigate back and forth frequently between levels, and you donโ€™t need the sorting capabilities that a [list or table](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables) provides. For example, Finder offers a column view (in addition to icon, list, and gallery views) for navigating directory structures. + +**Show the root level of your data hierarchy in the first column.** People know they can quickly scroll back to the first column to begin navigating the hierarchy from the top again. + +**Consider showing information about the selected item when there are no nested items to display.** The Finder, for example, shows a preview of the selected item and information like the creation date, modification date, file type, and size. + +**Let people resize columns.** This is especially important if the names of some data items are too long to fit within the default column width. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/column-views#Platform-considerations) + + _Not supported in iOS, iPadOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/column-views#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/column-views#Related) + +[Lists and tables](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables) + +[Outline views](https://developer.apple.com/design/human-interface-guidelines/outline-views) + +[Split views](https://developer.apple.com/design/human-interface-guidelines/split-views) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/column-views#Developer-documentation) + +[`NSBrowser`](https://developer.apple.com/documentation/AppKit/NSBrowser) โ€” AppKit + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/lists-and-tables.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/lists-and-tables.md new file mode 100644 index 00000000..71515003 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/lists-and-tables.md @@ -0,0 +1,99 @@ +--- +title: "Lists and tables | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/lists-and-tables + +# Lists and tables + +Lists and tables present data in one or more columns of rows. + +![A stylized representation of a three-row table with header and footer text. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/c3e26d2515ac05cae7aba2704f8640d6/components-lists-and-tables-intro%402x.png) + +A table or list can represent data thatโ€™s organized in groups or hierarchies, and it can support user interactions like selecting, adding, deleting, and reordering. Apps and games in all platforms can use tables to present content and options; many apps use lists to express an overall information hierarchy and help people navigate it. For example, iOS Settings uses a hierarchy of lists to help people choose options, and several apps โ€” such as Mail in iPadOS and macOS โ€” use a table within a [split view](https://developer.apple.com/design/human-interface-guidelines/split-views). + +Sometimes, people need to work with complex data in a multicolumn table or a spreadsheet. Apps that offer productivity tasks often use a table to represent various characteristics or attributes of the data in separate, sortable columns. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables#Best-practices) + +**Prefer displaying text in a list or table.** A table can include any type of content, but the row-based format is especially well suited to making text easy to scan and read. If you have items that vary widely in size โ€” or you need to display a large number of images โ€” consider using a [collection](https://developer.apple.com/design/human-interface-guidelines/collections) instead. + +**Let people edit a table when it makes sense.** People appreciate being able to reorder a list, even if they canโ€™t add or remove items. In iOS and iPadOS, people must enter an edit mode before they can select table items. + +**Provide appropriate feedback when people select a list item.** The feedback can vary depending on whether selecting the item reveals a new view or toggles the itemโ€™s state. In general, a table that helps people navigate through a hierarchy persistently highlights the selected row to clarify the path people are taking. In contrast, a table that lists options often highlights a row only briefly before adding an image โ€” such as a checkmark โ€” indicating that the item is selected. + +## [Content](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables#Content) + +**Keep item text succinct so row content is comfortable to read.** Short, succinct text can help minimize truncation and wrapping, making text easier to read and scan. If each item consists of a large amount of text, consider alternatives that help you avoid displaying over-large table rows. For example, you could list item titles only, letting people choose an item to reveal its content in a detail view. + +**Consider ways to preserve readability of text that might otherwise get clipped or truncated.** When a table is narrow โ€” for example, if people can vary its width โ€” you want content to remain recognizable and easy to read. Sometimes, an ellipsis in the middle of text can make an item easier to distinguish because it preserves both the beginning and the end of the content. + +**Use descriptive column headings in a multicolumn table.** Use nouns or short noun phrases with [title-style capitalization](https://support.apple.com/guide/applestyleguide/c-apsgb744e4a3/web#apdca93e113f1d64), and donโ€™t add ending punctuation. If you donโ€™t include a column heading in a single-column table view, use a label or a header to help people understand the context. + +## [Style](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables#Style) + +**Choose a table or list style that coordinates with your data and platform.** Some styles use visual details to help communicate grouping and hierarchy or to provide specific experiences. In iOS and iPadOS, for example, the grouped style uses headers, footers, and additional space to separate groups of data; the elliptical style available in watchOS makes items appear as if theyโ€™re rolling off a rounded surface as people scroll; and macOS defines a bordered style that uses alternating row backgrounds to help make large tables easier to use. For developer guidance, see [`ListStyle`](https://developer.apple.com/documentation/SwiftUI/ListStyle). + +**Choose a row style that fits the information you need to display.** For example, you might need to display a small image in the leading end of a row, followed by a brief explanatory label. Some platforms provide built-in row styles you can use to arrange content in list rows, such as the [`UIListContentConfiguration`](https://developer.apple.com/documentation/UIKit/UIListContentConfiguration-swift.struct) API you can use to lay out content in a listโ€™s rows, headers, and footers in iOS, iPadOS, and tvOS. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables#Platform-considerations) + +### [iOS, iPadOS, visionOS](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables#iOS-iPadOS-visionOS) + +**Use an info button only to reveal more information about a rowโ€™s content.** An info button โ€” called a _detail disclosure button_ when it appears in a list row โ€” doesnโ€™t support navigation through a hierarchical table or list. If you need to let people drill into a list or table rowโ€™s subviews, use a disclosure indicator accessory control. For developer guidance, see [`UITableViewCell.AccessoryType.disclosureIndicator`](https://developer.apple.com/documentation/UIKit/UITableViewCell/AccessoryType-swift.enum/disclosureIndicator). + +![An illustration of a grouped list of rows. Each list item includes an info button at the trailing end of the row.](https://docs-assets.developer.apple.com/published/fd301d26835e0341b95eaa2027f200f2/info-button-in-list%402x.png)An info button shows details about a list item; it doesnโ€™t support navigation. + +![An illustration of a grouped list of rows. Each list item includes a right-pointing chevron at the trailing end of the row.](https://docs-assets.developer.apple.com/published/dcb3678fe458846713b03756ab5e1a28/disclosure-indicator-in-list%402x.png)A disclosure indicator reveals the next level in a hierarchy; it doesnโ€™t show details about the item. + +**Avoid adding an index to a table that displays controls โ€” like disclosure indicators โ€” in the trailing ends of its rows.** An _index_ typically consists of the letters in an alphabet, displayed vertically at the trailing side of a list. People can jump to a specific section in the list by choosing the index letter that maps to it. Because both the index and elements like disclosure indicators appear on the trailing side of a list, it can be difficult for people to use one element without activating the other. + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables#macOS) + +**When it provides value, let people click a column heading to sort a table view based on that column**. If people click the heading of a column thatโ€™s already sorted, re-sort the data in the opposite direction. + +**Let people resize columns.** Data displayed in a table view often varies in width. People appreciate resizing columns to help them concentrate on different areas or reveal clipped data. + +**Consider using alternating row colors in a multicolumn table.** Alternating colors can help people track row values across columns, especially in a wide table. + +**Use an outline view instead of a table view to present hierarchical data.** An [outline view](https://developer.apple.com/design/human-interface-guidelines/outline-views) looks like a table view, but includes disclosure triangles for exposing nested levels of data. For example, an outline view might display folders and the items they contain. + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables#tvOS) + +**Confirm that images near a table still look good as each row highlights and slightly increases in size when it becomes focused.** A focused rowโ€™s corners can also become rounded, which may affect the appearance of images on either side of it. Account for this effect as you prepare images, and donโ€™t add your own masks to round the corners. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables#watchOS) + +**When possible, limit the number of rows.** Short lists are easier for people to scan, but sometimes people expect a long list of items. For example, if people subscribe to a large number of podcasts, they might think somethingโ€™s wrong if they canโ€™t view all their items. You can help make a long list more manageable by listing the most relevant items and providing a way for people to view more. + +**Constrain the length of detail views if you want to support vertical page-based navigation.** People use vertical page-based navigation to swipe vertically among the detail items of different list rows. Navigating in this way saves time because people donโ€™t need to return to the list to tap a new detail item, but it works only when detail views are short. If your detail views scroll, people wonโ€™t be able to use vertical page-based navigation to swipe among them. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables#Related) + +[Collections](https://developer.apple.com/design/human-interface-guidelines/collections) + +[Outline views](https://developer.apple.com/design/human-interface-guidelines/outline-views) + +[Layout](https://developer.apple.com/design/human-interface-guidelines/layout) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables#Developer-documentation) + +[`List`](https://developer.apple.com/documentation/SwiftUI/List) โ€” SwiftUI + +[Tables](https://developer.apple.com/documentation/SwiftUI/Tables) โ€” SwiftUI + +[`UITableView`](https://developer.apple.com/documentation/UIKit/UITableView) โ€” UIKit + +[`NSTableView`](https://developer.apple.com/documentation/AppKit/NSTableView) โ€” AppKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/49/1636D358-5C36-4027-B204-81FFE4D05B7D/3455_wide_250x141_1x.jpg) Stacks, Grids, and Outlines in SwiftUI ](https://developer.apple.com/videos/play/wwdc2020/10031) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables#Change-log) + +Date| Changes +---|--- +June 21, 2023| Updated to include guidance for visionOS. +June 5, 2023| Updated guidance to reflect changes in watchOS 10. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/ornaments.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/ornaments.md new file mode 100644 index 00000000..ae943ee4 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/ornaments.md @@ -0,0 +1,56 @@ +--- +title: "Ornaments | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/ornaments + +# Ornaments + +In visionOS, an ornament presents controls and information related to a window, without crowding or obscuring the windowโ€™s contents. + +![A stylized representation of an ornament at the bottom of a window shown on top of a grid that suggests the canvas of a design tool. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/a9012c3e7b1c5d47a4788aefd7a5b48c/components-ornaments-intro%402x.png) + +An ornament floats in a plane thatโ€™s parallel to its associated window and slightly in front of it along the z-axis. If the associated window moves, the ornament moves with it, maintaining its relative position; if the windowโ€™s contents scroll, the controls or information in the ornament remain unchanged. + +Ornaments can appear on any edge of a window and can contain UI components like buttons, segmented controls, and other views. The system uses ornaments to create and manage components like [toolbars](https://developer.apple.com/design/human-interface-guidelines/toolbars), [tab bars](https://developer.apple.com/design/human-interface-guidelines/tab-bars), and video playback controls; you can use an ornament to create a custom component. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/ornaments#Best-practices) + +**Consider using an ornament to present frequently needed controls or information in a consistent location that doesnโ€™t clutter the window.** Because an ornament stays close to its window, people always know where to find it. For example, Music uses an ornament to offer Now Playing controls, ensuring that these controls remain in a predictable location thatโ€™s easy to find. + +**In general, keep an ornament visible.** It can make sense to hide an ornament when people dive into a windowโ€™s content โ€” for example, when they watch a video or view a photo โ€” but in most cases, people appreciate having consistent access to an ornamentโ€™s controls. + +**If you need to display multiple ornaments, prioritize the overall visual balance of the window.** Ornaments help elevate important actions, but they can sometimes distract from your content. When necessary, consider constraining the total number of ornaments to avoid increasing a windowโ€™s visual weight and making your app feel more complicated. If you decide to remove an ornament, you can relocate its elements into the main window. + +**Aim to keep an ornamentโ€™s width the same or narrower than the width of the associated window.** If an ornament is wider than its window, it can interfere with a tab bar or other vertical content on the windowโ€™s side. + +**Consider using borderless buttons in an ornament.** By default, an ornamentโ€™s background is [glass](https://developer.apple.com/design/human-interface-guidelines/materials#visionOS), so if you place a button directly on the background, it may not need a visible border. When people look at a borderless button in an ornament, the system automatically applies the hover affect to it (for guidance, see [Eyes](https://developer.apple.com/design/human-interface-guidelines/eyes)). + +**Use system-provided toolbars and tab bars unless you need to create custom components.** In visionOS, toolbars and tab bars automatically appear as ornaments, so you donโ€™t need to use an ornament to create these components. For developer guidance, see [Toolbars](https://developer.apple.com/documentation/SwiftUI/Toolbars) and [`TabView`](https://developer.apple.com/documentation/SwiftUI/TabView). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/ornaments#Platform-considerations) + + _Not supported in iOS, iPadOS, macOS, tvOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/ornaments#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/ornaments#Related) + +[Layout](https://developer.apple.com/design/human-interface-guidelines/layout) + +[Toolbars](https://developer.apple.com/design/human-interface-guidelines/toolbars) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/ornaments#Developer-documentation) + +[`ornament(visibility:attachmentAnchor:contentAlignment:ornament:)`](https://developer.apple.com/documentation/SwiftUI/View/ornament\(visibility:attachmentAnchor:contentAlignment:ornament:\)) โ€” SwiftUI + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/ornaments#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/38E4EE32-29B5-4478-B8B6-35B8ACA67B16/8130_wide_250x141_1x.jpg) Design for spatial user interfaces ](https://developer.apple.com/videos/play/wwdc2023/10076) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/ornaments#Change-log) + +Date| Changes +---|--- +February 2, 2024| Added guidance on using multiple ornaments. +December 5, 2023| Removed a statement about using ornaments to present supplementary items. +June 21, 2023| New page. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/outline-views.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/outline-views.md new file mode 100644 index 00000000..da03e0ef --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/outline-views.md @@ -0,0 +1,64 @@ +--- +title: "Outline views | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/outline-views + +# Outline views + +An outline view presents hierarchical data in a scrolling list of cells that are organized into columns and rows. + +![A stylized representation of a list of folders and images, displayed in an outline view containing four columns: \[Name\], \[Date Modified\], \[Size\], and \[Kind\]. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/30462b13b59c89c7ba9e142a2fcef05b/components-outline-view-intro%402x.png) + +An outline view includes at least one column that contains primary hierarchical data, such as a set of parent containers and their children. You can add columns, as needed, to display attributes that supplement the primary data; for example, sizes and modification dates. Parent containers have disclosure triangles that expand to reveal their children. + +Finder windows offer an outline view for navigating the file system. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/outline-views#Best-practices) + +Outline views work well to display text-based content and often appear in the leading side of a [split view](https://developer.apple.com/design/human-interface-guidelines/split-views), with related content on the opposite side. + +**Use a table instead of an outline view to present data thatโ€™s not hierarchical.** For guidance, see [Lists and tables](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables). + +**Expose data hierarchy in the first column only.** Other columns can display attributes that apply to the hierarchical data in the primary column. + +**Use descriptive column headings to provide context.** Use nouns or short noun phrases with [title-style capitalization](https://help.apple.com/applestyleguide/#/apsgb744e4a3?sub=apdca93e113f1d64) and no punctuation; in particular, avoid adding a trailing colon. Always provide column headings in a multi-column outline view. If you donโ€™t include a column heading in a single-column outline view, use a label or other means to make sure thereโ€™s enough context. + +**Consider letting people click column headings to sort an outline view.** In a sortable outline view, people can click a column heading to perform an ascending or descending sort based on that column. You can implement additional sorting based on secondary columns behind the scenes, if necessary. If people click the primary column heading, sorting occurs at each hierarchy level. For example, in the Finder, all top-level folders are sorted, then the items within each folder are sorted. If people click the heading of a column thatโ€™s already sorted, the folders and their contents are sorted again in the opposite direction. + +**Let people resize columns.** Data displayed in an outline view often varies in width. Itโ€™s important to let people adjust column width as needed to reveal data thatโ€™s wider than the column. + +**Make it easy for people to expand or collapse nested containers.** For example, clicking a disclosure triangle for a folder in a Finder window expands only that folder. However, Option-clicking the disclosure triangle expands all of its subfolders. + +**Retain peopleโ€™s expansion choices.** If people expand various levels of an outline view to reach a specific item, store the state so you can display it again the next time. This way, people wonโ€™t need to navigate back to the same place again. + +**Consider using alternating row colors in multi-column outline views.** Alternating colors can make it easier for people to track row values across columns, especially in wide outline views. + +**Let people edit data if it makes sense in your app.** In an editable outline view cell, people expect to be able to single-click a cell to edit its contents. Note that a cell can respond differently to a double click. For example, an outline view listing files might let people single-click a fileโ€™s name to edit it, but double-click a fileโ€™s name to open the file. You can also let people reorder, add, and remove rows if it would be useful. + +**Consider using a centered ellipsis to truncate cell text instead of clipping it.** An ellipsis in the middle preserves the beginning and end of the cell text, which can make the content more distinct and recognizable than clipped text. + +**Consider offering a search field to help people find values quickly in a lengthy outline view.** Windows with an outline view as the primary feature often include a search field in the toolbar. For guidance, see [Search fields](https://developer.apple.com/design/human-interface-guidelines/search-fields). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/outline-views#Platform-considerations) + + _Not supported in iOS, iPadOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/outline-views#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/outline-views#Related) + +[Column views](https://developer.apple.com/design/human-interface-guidelines/column-views) + +[Lists and tables](https://developer.apple.com/design/human-interface-guidelines/lists-and-tables) + +[Split views](https://developer.apple.com/design/human-interface-guidelines/split-views) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/outline-views#Developer-documentation) + +[`OutlineGroup`](https://developer.apple.com/documentation/SwiftUI/OutlineGroup) โ€” SwiftUI + +[`NSOutlineView`](https://developer.apple.com/documentation/AppKit/NSOutlineView) โ€” AppKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/outline-views#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/49/1636D358-5C36-4027-B204-81FFE4D05B7D/3455_wide_250x141_1x.jpg) Stacks, Grids, and Outlines in SwiftUI ](https://developer.apple.com/videos/play/wwdc2020/10031) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/panels.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/panels.md new file mode 100644 index 00000000..c3d0cc57 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/panels.md @@ -0,0 +1,75 @@ +--- +title: "Panels | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/panels + +# Panels + +In a macOS app, a panel typically floats above other open windows providing supplementary controls, options, or information related to the active window or current selection. + +![A stylized representation of a panel floating above a window. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/37f7c9e6dd4c635ccbae68b50200a74c/components-panel-intro%402x.png) + +In general, a panel has a less prominent appearance than an appโ€™s [main window](https://developer.apple.com/design/human-interface-guidelines/windows#macOS-window-states). When the situation calls for it, a panel can also use a dark, translucent style to support a heads-up display (or _HUD_) experience. + +When your app runs in other platforms, consider using a modal view to present supplementary content thatโ€™s relevant to the current task or selection. For guidance, see [Modality](https://developer.apple.com/design/human-interface-guidelines/modality). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/panels#Best-practices) + +**Use a panel to give people quick access to important controls or information related to the content theyโ€™re working with.** For example, you might use a panel to provide controls or settings that affect the selected item in the active document or window. + +**Consider using a panel to present inspector functionality.** An _inspector_ displays the details of the currently selected item, automatically updating its contents when the item changes or when people select a new item. In contrast, if you need to present an _Info_ window โ€” which always maintains the same contents, even when the selected item changes โ€” use a regular window, not a panel. Depending on the layout of your app, you might also consider using a [split view](https://developer.apple.com/design/human-interface-guidelines/split-views) pane to present an inspector. + +**Prefer simple adjustment controls in a panel.** As much as possible, avoid including controls that require typing text or selecting items to act upon because these actions can require multiple steps. Instead, consider using controls like sliders and steppers because these components can give people more direct control. + +**Write a brief title that describes the panelโ€™s purpose.** Because a panel often floats above other open windows in your app, it needs a title bar so people can position it where they want. Create a short title using a noun โ€” or a noun phrase with [title-style capitalization](https://support.apple.com/guide/applestyleguide/c-apsgb744e4a3/web#apdca93e113f1d64) โ€” that can help people recognize the panel onscreen. For example, macOS provides familiar panels titled โ€œFontsโ€ and โ€œColors,โ€ and many apps use the title โ€œInspector.โ€ + +**Show and hide panels appropriately.** When your app becomes active, bring all of its open panels to the front, regardless of which window was active when the panel opened. When your app is inactive, hide all of its panels. + +**Avoid including panels in the Window menuโ€™s documents list.** Itโ€™s fine to include commands for showing or hiding panels in the [Window menu](https://developer.apple.com/design/human-interface-guidelines/the-menu-bar#Window-menu), but panels arenโ€™t documents or standard app windows, and they donโ€™t belong in the Window menuโ€™s list. + +**In general, avoid making a panelโ€™s minimize button available.** People donโ€™t usually need to minimize a panel, because it displays only when needed and disappears when the app is inactive. + +**Refer to panels by title in your interface and in help documentation.** In menus, use the panelโ€™s title without including the term _panel_ : for example, โ€œShow Fonts,โ€ โ€œShow Colors,โ€ and โ€œShow Inspector.โ€ In help documentation, it can be confusing to introduce โ€œpanelโ€ as a different type of window, so itโ€™s generally best to refer to a panel by its title or โ€” when it adds clarity โ€” by appending _window_ to the title. For example, the title โ€œInspectorโ€ often supplies enough context to stand on its own, whereas it can be clearer to use โ€œFonts windowโ€ and โ€œColors windowโ€ instead of just โ€œFontsโ€ and โ€œColors.โ€ + +## [HUD-style panels](https://developer.apple.com/design/human-interface-guidelines/panels#HUD-style-panels) + +A HUD-style panel serves the same function as a standard panel, but its appearance is darker and translucent. HUDs work well in apps that present highly visual content or that provide an immersive experience, such as media editing or a full-screen slide show. For example, QuickTime Player uses a HUD to display inspector information without obstructing too much content. + +![A screenshot of a translucent HUD panel, used to display inspector information for a movie file, including the filename, format, frames per second, data rate, and the frame size of the movie content.](https://docs-assets.developer.apple.com/published/f3fccb3f4ad6963af1310c8f98c5a0f7/hud-style-panel%402x.png) + +**Prefer standard panels.** People can be distracted or confused by a HUD when thereโ€™s no logical reason for its presence. Also, a HUD might not match the current appearance setting. In general, use a HUD only: + + * In a media-oriented app that presents movies, photos, or slides + + * When a standard panel would obscure essential content + + * When you donโ€™t need to include controls โ€” with the exception of the disclosure triangle, most system-provided controls donโ€™t match a HUDโ€™s appearance. + + + + +**Maintain one panel style when your app switches modes.** For example, if you use a HUD when your app is in full-screen mode, prefer maintaining the HUD style when people take your app out of full-screen mode. + +**Use color sparingly in HUDs.** Too much color in the dark appearance of a HUD can be distracting. Often, you need only small amounts of high-contrast color to highlight important information in a HUD. + +**Keep HUDs small.** HUDs are designed to be unobtrusively useful, so letting them grow too large defeats their primary purpose. Donโ€™t let a HUD obscure the content it adjusts, and make sure it doesnโ€™t compete with the content for peopleโ€™s attention. + +For developer guidance, see [`hudWindow`](https://developer.apple.com/documentation/AppKit/NSWindow/StyleMask-swift.struct/hudWindow). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/panels#Platform-considerations) + + _Not supported in iOS, iPadOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/panels#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/panels#Related) + +[Windows](https://developer.apple.com/design/human-interface-guidelines/windows) + +[Modality](https://developer.apple.com/design/human-interface-guidelines/modality) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/panels#Developer-documentation) + +[`NSPanel`](https://developer.apple.com/documentation/AppKit/NSPanel) โ€” AppKit + +[`hudWindow`](https://developer.apple.com/documentation/AppKit/NSWindow/StyleMask-swift.struct/hudWindow) โ€” AppKit + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/scroll-views.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/scroll-views.md new file mode 100644 index 00000000..0f10a347 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/scroll-views.md @@ -0,0 +1,123 @@ +--- +title: "Scroll views | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/scroll-views + +# Scroll views + +A scroll view lets people view content thatโ€™s larger than the viewโ€™s boundaries by moving the content vertically or horizontally. + +![A stylized representation of a scrollable image view. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/395072e6a9ec2890d242c1d967a7cbe4/components-scroll-view-intro%402x.png) + +The scroll view itself has no appearance, but it can display a translucent _scroll indicator_ that typically appears after people begin scrolling the viewโ€™s content. Although the appearance and behavior of scroll indicators can vary per platform, all indicators provide visual feedback about the scrolling action. For example, in iOS, iPadOS, macOS, visionOS, and watchOS, the indicator shows whether the currently visible content is near the beginning, middle, or end of the view. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/scroll-views#Best-practices) + +**Support default scrolling gestures and keyboard shortcuts.** People are accustomed to the systemwide scrolling behavior and expect it to work everywhere. If you build custom scrolling for a view, make sure your scroll indicators use the elastic behavior that people expect. + +**Make it apparent when content is scrollable.** Because scroll indicators arenโ€™t always visible, it can be helpful to make it obvious when content extends beyond the view. For example, displaying partial content at the edge of a view indicates that thereโ€™s more content in that direction. Although most people immediately try scrolling a view to discover if additional content is available, itโ€™s considerate to draw their attention to it. + +**Avoid putting a scroll view inside another scroll view with the same orientation.** Nesting scroll views that have the same orientation can create an unpredictable interface thatโ€™s difficult to control. Itโ€™s alright to place a horizontal scroll view inside a vertical scroll view (or vice versa), however. + +**Consider supporting page-by-page scrolling if it makes sense for your content.** In some situations, people appreciate scrolling by a fixed amount of content per interaction instead of scrolling continuously. On most platforms, you can define the size of such a _page_ โ€” typically the current height or width of the view โ€” and define an interaction that scrolls one page at a time. To help maintain context during page-by-page scrolling, you can define a unit of overlap, such as a line of text, a row of glyphs, or part of a picture, and subtract the unit from the page size. For developer guidance, see [`PagingScrollTargetBehavior`](https://developer.apple.com/documentation/SwiftUI/PagingScrollTargetBehavior). + +**In some cases, scroll automatically to help people find their place.** Although people initiate almost all scrolling, automatic scrolling can be helpful when relevant content is no longer in view, such as when: + + * Your app performs an operation that selects content or places the insertion point in an area thatโ€™s currently hidden. For example, when your app locates text that people are searching for, scroll the content to bring the new selection into view. + + * People start entering information in a location thatโ€™s not currently visible. For example, if the insertion point is on one page and people navigate to another page, scroll back to the insertion point as soon as they begin to enter text. + + * The pointer moves past the edge of the view while people are making a selection. In this case, follow the pointer by scrolling in the direction it moves. + + * People select something and scroll to a new location before acting on the selection. In this case, scroll until the selection is in view before performing the operation. + + + + +In all cases, automatically scroll the content only as much as necessary to help people retain context. For example, if part of a selection is visible, you donโ€™t need to scroll the entire selection into view. + +**If you support zoom, set appropriate maximum and minimum scale values.** For example, zooming in on text until a single character fills the screen doesnโ€™t make sense in most situations. + +## [Scroll edge effects](https://developer.apple.com/design/human-interface-guidelines/scroll-views#Scroll-edge-effects) + +In iOS, iPadOS, and macOS, a _scroll edge effect_ is a variable blur that provides a transition between a content area and an area with [Liquid Glass](https://developer.apple.com/design/human-interface-guidelines/materials#Liquid-Glass) controls, such as [toolbars](https://developer.apple.com/design/human-interface-guidelines/toolbars). In most cases, the system applies a scroll edge effect automatically when a pinned element overlaps with scrolling content. If you use custom controls or layouts, the effect might not appear, and you may need to add it manually. For developer guidance, see [`ScrollEdgeEffectStyle`](https://developer.apple.com/documentation/SwiftUI/ScrollEdgeEffectStyle) and [`UIScrollEdgeEffect`](https://developer.apple.com/documentation/UIKit/UIScrollEdgeEffect). + +There are two styles of scroll edge effect: soft and hard. + + * Use a [`soft`](https://developer.apple.com/documentation/SwiftUI/ScrollEdgeEffectStyle/soft) edge effect in most cases, especially in iOS and iPadOS, to provide a subtle transition that works well for toolbars and interactive elements like buttons. + + * Use a [`hard`](https://developer.apple.com/documentation/SwiftUI/ScrollEdgeEffectStyle/hard) edge effect primarily in macOS for a stronger, more opaque boundary thatโ€™s ideal for interactive text, backless controls, or pinned table headers that need extra clarity. + + + + +**Only use a scroll edge effect when a scroll view is adjacent to floating interface elements.** Scroll edge effects arenโ€™t decorative. They donโ€™t block or darken like overlays; they exist to clarify where controls and content meet. + +**Apply one scroll edge effect per view.** In split view layouts on iPad and Mac, each pane can have its own scroll edge effect; in this case, keep them consistent in height to maintain alignment. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/scroll-views#Platform-considerations) + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/scroll-views#iOS-iPadOS) + +**Consider showing a page control when a scroll view is in page-by-page mode.** [Page controls](https://developer.apple.com/design/human-interface-guidelines/page-controls) show how many pages, screens, or other chunks of content are available and indicates which one is currently visible. For example, Weather uses a page control to indicate movement between peopleโ€™s saved locations. If you show a page control with a scroll view, donโ€™t show the scrolling indicator on the same axis to avoid confusing people with redundant controls. + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/scroll-views#macOS) + +In macOS, a _scroll indicator_ is commonly called a _scroll bar_. + +**If necessary, use small or mini scroll bars in a panel.** When space is tight, you can use smaller scroll bars in panels that need to coexist with other windows. Be sure to use the same size for all controls in such a panel. + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/scroll-views#tvOS) + +Views in tvOS can scroll, but they arenโ€™t treated as distinct objects with scroll indicators. Instead, when content exceeds the size of the screen, the system automatically scrolls the interface to keep focused items visible. + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/scroll-views#visionOS) + +In visionOS, the scroll indicator has a small, fixed size to help communicate that people can scroll efficiently without making large movements. To make it easy to find, the scroll indicator always appears in a predictable location with respect to the window: vertically centered at the trailing edge during vertical scrolling and horizontally centered at the windowโ€™s bottom edge during horizontal scrolling. + +When people begin swiping content in the direction they want it to scroll, the scroll indicator appears at the windowโ€™s edge, visually reinforcing the effect of their gesture and providing feedback about the contentโ€™s current position and overall length. When people look at the scroll indicator and begin a drag gesture, the indicator enables a jog bar experience that lets people manipulate the scrolling speed instead of the contentโ€™s position. In this experience, the scroll indicator reveals tick marks that speed up or slow down as people make small adjustments to their gesture, providing visual feedback that helps people precisely control scrolling acceleration. + +Video with custom controls. + +Content description: A recording showing a scroll indicator on a long page in the Notes app. As the viewer drags the page quickly, the indicator shows tick marks that match the scrolling speed. + +Play + +**If necessary, account for the size of the scroll indicator.** Although the indicatorโ€™s overall size is small, itโ€™s a little thicker than the same component in iOS. If your content uses tight margins, consider increasing them to prevent the scroll indicator from overlapping the content. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/scroll-views#watchOS) + +**Prefer vertically scrolling content.** People are accustomed to using the Digital Crown to navigate to and within apps on Apple Watch. If your app contains a single list or content view, rotating the Digital Crown scrolls vertically when your appโ€™s content is taller than the height of the display. + +**Use tab views to provide page-by-page scrolling.** watchOS displays tab views as pages. If you place tab views in a vertical stack, people can rotate the Digital Crown to move vertically through full-screen pages of content. In this scenario, the system displays a page indicator next to the Digital Crown that shows people where they are in the content, both within the current page and within a set of pages. For guidance, see [Tab views](https://developer.apple.com/design/human-interface-guidelines/tab-views). + +**When displaying paged content, consider limiting the content of an individual page to a single screen height.** Embracing this constraint clarifies the purpose of each page, helping you create a more glanceable design. However, if your app has long pages, people can still use the Digital Crown both to navigate between shorter pages and to scroll content in a longer page because the page indicator expands into a scroll indicator when necessary. Use variable-height pages judiciously and place them after fixed-height pages when possible. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/scroll-views#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/scroll-views#Related) + +[Page controls](https://developer.apple.com/design/human-interface-guidelines/page-controls) + +[Gestures](https://developer.apple.com/design/human-interface-guidelines/gestures) + +[Pointing devices](https://developer.apple.com/design/human-interface-guidelines/pointing-devices) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/scroll-views#Developer-documentation) + +[`ScrollView`](https://developer.apple.com/documentation/SwiftUI/ScrollView) + +[`UIScrollView`](https://developer.apple.com/documentation/UIKit/UIScrollView) + +[`NSScrollView`](https://developer.apple.com/documentation/AppKit/NSScrollView) + +[`WKPageOrientation`](https://developer.apple.com/documentation/WatchKit/WKPageOrientation) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/scroll-views#Change-log) + +Date| Changes +---|--- +July 28, 2025| Added guidance for scroll edge effects. +February 2, 2024| Added artwork showing the behavior of the visionOS scroll indicator. +December 5, 2023| Described the visionOS scroll indicator and added guidance for integrating it with window layout. +June 5, 2023| Updated guidance for using scroll views in watchOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/sidebars.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/sidebars.md new file mode 100644 index 00000000..cbf43b08 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/sidebars.md @@ -0,0 +1,109 @@ +--- +title: "Sidebars | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/sidebars + +# Sidebars + +A sidebar appears on the leading side of a view and lets people navigate between sections in your app or game. + +![A stylized representation of the top portion of a window's sidebar displaying a title, a section, and some folders. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/d8bde769da53e8facee9d89e4362b83c/components-sidebar-intro%402x.png) + +A sidebar floats above content without being anchored to the edges of the view. It provides a broad, flat view of an appโ€™s information hierarchy, giving people access to several peer content areas or modes at the same time. + +A sidebar requires a large amount of vertical and horizontal space. When space is limited or you want to devote more of the screen to other information or functionality, a more compact control such as a [tab bar](https://developer.apple.com/design/human-interface-guidelines/tab-bars) may provide a better navigation experience. For guidance, see [Layout](https://developer.apple.com/design/human-interface-guidelines/layout). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/sidebars#Best-practices) + +**Extend content beneath the sidebar.** In iOS, iPadOS, and macOS, as with other controls such as toolbars and tab bars, sidebars float above content in the [Liquid Glass](https://developer.apple.com/design/human-interface-guidelines/materials#Liquid-Glass) layer. To reinforce the separation and floating appearance of the sidebar, extend content beneath it either by letting it horizontally scroll or applying a background extension view, which mirrors adjacent content to give the impression of stretching it under the sidebar. For developer guidance, see [`backgroundExtensionEffect()`](https://developer.apple.com/documentation/SwiftUI/View/backgroundExtensionEffect\(\)). + +![A screenshot of the leading side of an app on iPad. An image spans the upper part of the window, stopping at the edge of the sidebar.](https://docs-assets.developer.apple.com/published/d50ee5db90fbe0cae8f34304aa315053/sidebars-extend-content-beneath-sidebar-incorrect%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![A screenshot of the leading side of an app on iPad. An image spans the upper part of the window, and uses a background extension effect to flip, blur, and extend the image beneath the sidebar to the edge of the window.](https://docs-assets.developer.apple.com/published/5cdac1170561cddf1930b4d74325c4dd/sidebars-extend-content-beneath-sidebar-correct%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +**When possible, let people customize the contents of a sidebar.** A sidebar lets people navigate to important areas in your app, so it works well when people can decide which areas are most important and in what order they appear. + +**Group hierarchy with disclosure controls if your app has a lot of content.** Using [disclosure controls](https://developer.apple.com/design/human-interface-guidelines/disclosure-controls) helps keep the sidebarโ€™s vertical space to a manageable level. + +**Consider using familiar symbols to represent items in the sidebar.** [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) provides a wide range of customizable symbols you can use to represent items in your app. If you need to use a custom icon, consider creating a [custom symbol](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Custom-symbols) rather than using a bitmap image. Download the SF Symbols app from [Apple Design Resources](https://developer.apple.com/design/resources/#sf-symbols). + +**Consider letting people hide the sidebar.** People sometimes want to hide the sidebar to create more room for content details or to reduce distraction. When possible, let people hide and show the sidebar using the platform-specific interactions they already know. For example, in iPadOS, people expect to use the built-in edge swipe gesture; in macOS, you can include a show/hide button or add Show Sidebar and Hide Sidebar commands to your appโ€™s View menu. In visionOS, a window typically expands to accommodate a sidebar, so people rarely need to hide it. Avoid hiding the sidebar by default to ensure that it remains discoverable. + +**In general, show no more than two levels of hierarchy in a sidebar.** When a data hierarchy is deeper than two levels, consider using a split view interface that includes a content list between the sidebar items and detail view. + +**If you need to include two levels of hierarchy in a sidebar, use succinct, descriptive labels to title each group.** To help keep labels short, omit unnecessary words. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/sidebars#Platform-considerations) + + _No additional considerations for tvOS. Not supported in watchOS._ + +### [iOS](https://developer.apple.com/design/human-interface-guidelines/sidebars#iOS) + +**Avoid using a sidebar.** A sidebar takes up a lot of space in landscape orientation and isnโ€™t available in portrait orientation. Instead, consider using a [tab bar](https://developer.apple.com/design/human-interface-guidelines/tab-bars), which takes less space and remains visible in both orientations. + +### [iPadOS](https://developer.apple.com/design/human-interface-guidelines/sidebars#iPadOS) + +When you use the [`sidebarAdaptable`](https://developer.apple.com/documentation/SwiftUI/TabViewStyle/sidebarAdaptable) style of tab view to present a sidebar, you choose whether to display a sidebar or a tab bar when your app opens. Both variations include a button that people can use to switch between them. This style also responds automatically to rotation and window resizing, providing a version of the control thatโ€™s appropriate to the width of the view. + +Developer note + +To display a sidebar only, use [`NavigationSplitView`](https://developer.apple.com/documentation/SwiftUI/NavigationSplitView) to present a sidebar in the primary pane of a split view, or use [`UISplitViewController`](https://developer.apple.com/documentation/UIKit/UISplitViewController). + +**Consider using a tab bar first.** A tab bar provides more space to feature content, and offers enough flexibility to navigate between many appsโ€™ main areas. If you need to expose more areas than fit in a tab bar, the tab barโ€™s convertible sidebar-style appearance can provide access to content that people use less frequently. For guidance, see [Tab bars](https://developer.apple.com/design/human-interface-guidelines/tab-bars). + +**If necessary, apply the correct appearance to a sidebar.** If youโ€™re not using SwiftUI to create a sidebar, you can use the [`UICollectionLayoutListConfiguration.Appearance.sidebar`](https://developer.apple.com/documentation/UIKit/UICollectionLayoutListConfiguration-swift.struct/Appearance-swift.enum/sidebar) appearance of a collection view list layout. For developer guidance, see [`UICollectionLayoutListConfiguration.Appearance`](https://developer.apple.com/documentation/UIKit/UICollectionLayoutListConfiguration-swift.struct/Appearance-swift.enum). + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/sidebars#macOS) + +A sidebarโ€™s row height, text, and glyph size depend on its overall size, which can be small, medium, or large. You can set the size programmatically, but people can also change it by selecting a different sidebar icon size in General settings. + +**Avoid stylizing your app by specifying a fixed color for all sidebar icons.** By default, sidebar icons use the current [accent color](https://developer.apple.com/design/human-interface-guidelines/color#App-accent-colors) and people expect to see their chosen accent color throughout all the apps they use. Although a fixed color can help clarify the meaning of an icon, you want to make sure that most sidebar icons display the color people choose. + +**Consider automatically hiding and revealing a sidebar when its container window resizes.** For example, reducing the size of a Mail viewer window can automatically collapse its sidebar, making more room for message content. + +**Avoid putting critical information or actions at the bottom of a sidebar.** People often relocate a window in a way that hides its bottom edge. + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/sidebars#visionOS) + +**If your appโ€™s hierarchy is deep, consider using a sidebar within a tab in a tab bar.** In this situation, a sidebar can support secondary navigation within the tab. If you do this, be sure to prevent selections in the sidebar from changing which tab is currently open. + +![A partial screenshot of the Music app in visionOS. The app's window includes a sidebar for navigating the music library, and the secondary pane includes a grid of playlists.](https://docs-assets.developer.apple.com/published/5e381525f4cccac8e9eb979fe4c984c6/visionos-sidebar-music%402x.png) + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/sidebars#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/sidebars#Related) + +[Split views](https://developer.apple.com/design/human-interface-guidelines/split-views) + +[Tab bars](https://developer.apple.com/design/human-interface-guidelines/tab-bars) + +[Layout](https://developer.apple.com/design/human-interface-guidelines/layout) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/sidebars#Developer-documentation) + +[`sidebarAdaptable`](https://developer.apple.com/documentation/SwiftUI/TabViewStyle/sidebarAdaptable) โ€” SwiftUI + +[`NavigationSplitView`](https://developer.apple.com/documentation/SwiftUI/NavigationSplitView) โ€” SwiftUI + +[`sidebar`](https://developer.apple.com/documentation/SwiftUI/ListStyle/sidebar) โ€” SwiftUI + +[`UICollectionLayoutListConfiguration`](https://developer.apple.com/documentation/UIKit/UICollectionLayoutListConfiguration-swift.struct) โ€” UIKit + +[`NSSplitViewController`](https://developer.apple.com/documentation/AppKit/NSSplitViewController) โ€” AppKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/sidebars#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/873F40BE-101A-4C0D-99F0-F5C7CE7B47A3/10046_wide_250x141_1x.jpg) Elevate the design of your iPad app ](https://developer.apple.com/videos/play/wwdc2025/208) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/sidebars#Change-log) + +Date| Changes +---|--- +June 9, 2025| Added guidance for extending content beneath the sidebar. +August 6, 2024| Updated guidance to include the SwiftUI adaptable sidebar style. +December 5, 2023| Added artwork for iPadOS. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/split-views.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/split-views.md new file mode 100644 index 00000000..2dc62e1d --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/split-views.md @@ -0,0 +1,110 @@ +--- +title: "Split views | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/split-views + +# Split views + +A split view manages the presentation of multiple adjacent panes of content, each of which can contain a variety of components, including tables, collections, images, and custom views. + +![A stylized representation of a window consisting of three areas: a sidebar, a canvas, and an inspector. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/68c529d6dd40b4b46f1862f1cdbadec4/components-split-view-intro%402x.png) + +Typically, you use a split view to show multiple levels of your appโ€™s hierarchy at once and support navigation between them. In this scenario, selecting an item in the viewโ€™s primary pane displays the itemโ€™s contents in the secondary pane. Similarly, a split view can display a tertiary pane if items in the secondary pane contain additional content. + +Itโ€™s common to use a split view to display a [sidebar](https://developer.apple.com/design/human-interface-guidelines/sidebars) for navigation, where the leading pane lists the top-level items or collections in an app, and the secondary and optional tertiary panes can present child collections and item details. Rarely, you might also use a split view to provide groups of functionality that supplement the primary view โ€” for example, Keynote in macOS uses split view panes to present the slide navigator, the presenter notes, and the inspector pane in areas that surround the main slide canvas. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/split-views#Best-practices) + +**To support navigation, persistently highlight the current selection in each pane that leads to the detail view.** The selected appearance clarifies the relationship between the content in various panes and helps people stay oriented. + +**Consider letting people drag and drop content between panes.** Because a split view provides access to multiple levels of hierarchy, people can conveniently move content from one part of your app to another by dragging items to different panes. For guidance, see [Drag and drop](https://developer.apple.com/design/human-interface-guidelines/drag-and-drop). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/split-views#Platform-considerations) + +### [iOS](https://developer.apple.com/design/human-interface-guidelines/split-views#iOS) + +**Prefer using a split view in a regular โ€” not a compact โ€” environment.** A split view needs horizontal space in which to display multiple panes. In a compact environment, such as iPhone in portrait orientation, itโ€™s difficult to display multiple panes without wrapping or truncating the content, making it less legible and harder to interact with. + +### [iPadOS](https://developer.apple.com/design/human-interface-guidelines/split-views#iPadOS) + +In iPadOS, a split view can include either two vertical panes, like Mail, or three vertical panes, like Keynote. + +**Account for narrow, compact, and intermediate window widths.** Since iPad windows are fluidly resizable, itโ€™s important to consider the design of a split view layout at multiple widths. In particular, ensure that itโ€™s possible to navigate between the various panes in a logical way. For guidance, see [Layout](https://developer.apple.com/design/human-interface-guidelines/layout). For developer guidance, see [`NavigationSplitView`](https://developer.apple.com/documentation/SwiftUI/NavigationSplitView) and [`UISplitViewController`](https://developer.apple.com/documentation/UIKit/UISplitViewController). + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/split-views#macOS) + +In macOS, you can arrange the panes of a split view vertically, horizontally, or both. A split view includes dividers between panes that can support dragging to resize them. For developer guidance, see [`VSplitView`](https://developer.apple.com/documentation/SwiftUI/VSplitView) and [`HSplitView`](https://developer.apple.com/documentation/SwiftUI/HSplitView). + + * Vertical + * Horizontal + * Multiple + + + +![An illustration of a laptop screen that shows two panes stacked vertically.](https://docs-assets.developer.apple.com/published/8c23f101a012db47a8e2350e50432617/vertical-split-view%402x.png) + +![An illustration of a laptop screen that shows two panes arranged side by side, with a narrower pane on the left and a wider pane on the right.](https://docs-assets.developer.apple.com/published/713be8f9e61a9578b26087ad71ca6b23/horizontal-split-view%402x.png) + +![An illustration of a laptop screen divided into three panes, split both vertically and horizontally.](https://docs-assets.developer.apple.com/published/3e315fbb8f8ade8b2d3d4f105f8c4482/multiple-split-view%402x.png) + +**Set reasonable defaults for minimum and maximum pane sizes.** If people can resize the panes in your appโ€™s split view, make sure to use sizes that keep the divider visible. If a pane gets too small, the divider can seem to disappear, becoming difficult to use. + +**Consider letting people hide a pane when it makes sense.** If your app includes an editing area, for example, consider letting people hide other panes to reduce distractions or allow more room for editing โ€” in Keynote, people can hide the navigator and presenter notes panes when they want to edit slide content. + +**Provide multiple ways to reveal hidden panes.** For example, you might provide a toolbar button or a menu command โ€” including a keyboard shortcut โ€” that people can use to restore a hidden pane. + +**Prefer the thin divider style.** The thin divider measures one point in width, giving you maximum space for content while remaining easy for people to use. Avoid using thicker divider styles unless you have a specific need. For example, if both sides of a divider present table rows that use strong linear elements that might make a thin divider hard to distinguish, it might work to use a thicker divider. For developer guidance, see [`NSSplitView.DividerStyle`](https://developer.apple.com/documentation/AppKit/NSSplitView/DividerStyle-swift.enum). + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/split-views#tvOS) + +In tvOS, a split view can work well to help people filter content. When people choose a filter category in the primary pane, your app can display the results in the secondary pane. + +**Choose a split view layout that keeps the panes looking balanced.** By default, a split view devotes a third of the screen width to the primary pane and two-thirds to the secondary pane, but you can also specify a half-and-half layout. + +**Display a single title above a split view, helping people understand the content as a whole.** People already know how to use a split view to navigate and filter content; they donโ€™t need titles that describe what each pane contains. + +**Choose the titleโ€™s alignment based on the type of content the secondary pane contains.** Specifically, when the secondary pane contains a content collection, consider centering the title in the window. In contrast, if the secondary pane contains a single main view of important content, consider placing the title above the primary view to give the content more room. + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/split-views#visionOS) + +**To display supplementary information, prefer a split view instead of a new window.** A split view gives people convenient access to more information without leaving the current context, whereas a new window may confuse people who are trying to navigate or reposition content. Opening more windows also requires you to carefully manage the relationship between views in your app or game. If you need to request a small amount of information or present a simple task that someone must complete before returning to their main task, use a [sheet](https://developer.apple.com/design/human-interface-guidelines/sheets). + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/split-views#watchOS) + +In watchOS, the split view displays either the list view or a detail view as a full-screen view. + +**Automatically display the most relevant detail view.** When your app launches, show people the most pertinent information. For example, display information relevant to their location, the time, or their recent actions. + +**If your app displays multiple detail pages, place the detail views in a vertical[tab view](https://developer.apple.com/design/human-interface-guidelines/tab-views).** People can then use the Digital Crown to scroll between the detail viewโ€™s tabs. watchOS also displays a page indicator next to the Digital Crown, indicating the number of tabs and the currently selected tab. + +![A screenshot showing a detail view with a vertical tab on Apple Watch. The page indicator next to the Digital Crown shows that the fifth tab is currently selected.](https://docs-assets.developer.apple.com/published/3f36258648d54880e800568e88b5076b/split-view-watch-vertical-tab%402x.png) + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/split-views#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/split-views#Related) + +[Sidebars](https://developer.apple.com/design/human-interface-guidelines/sidebars) + +[Tab bars](https://developer.apple.com/design/human-interface-guidelines/tab-bars) + +[Layout](https://developer.apple.com/design/human-interface-guidelines/layout) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/split-views#Developer-documentation) + +[`NavigationSplitView`](https://developer.apple.com/documentation/SwiftUI/NavigationSplitView) โ€” SwiftUI + +[`UISplitViewController`](https://developer.apple.com/documentation/UIKit/UISplitViewController) โ€” UIKit + +[`NSSplitViewController`](https://developer.apple.com/documentation/AppKit/NSSplitViewController) โ€” AppKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/split-views#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/A8CAF870-197F-4982-83D8-56513E5D7D0B/10000_wide_250x141_1x.jpg) Make your UIKit app more flexible ](https://developer.apple.com/videos/play/wwdc2025/282) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/split-views#Change-log) + +Date| Changes +---|--- +June 9, 2025| Added iOS and iPadOS platform considerations. +December 5, 2023| Added guidance for split views in visionOS. +June 5, 2023| Added guidance for split views in watchOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/tab-bars.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/tab-bars.md new file mode 100644 index 00000000..4d42f566 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/tab-bars.md @@ -0,0 +1,173 @@ +--- +title: "Tab bars | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/tab-bars + +# Tab bars + +A tab bar lets people navigate between top-level sections of your app. + +![A stylized representation of a tab bar containing four placeholder icons with names. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/8737d6baf5cdb223521eb4dbe3cb45e5/components-tab-bar-intro%402x.png) + +Tab bars help people understand the different types of information or functionality that an app provides. They also let people quickly switch between sections of the view while preserving the current navigation state within each section. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/tab-bars#Best-practices) + +**Use a tab bar to support navigation, not to provide actions.** A tab bar lets people navigate among different sections of an app, like the Alarm, Stopwatch, and Timer tabs in the Clock app. If you need to provide controls that act on elements in the current view, use a [toolbar](https://developer.apple.com/design/human-interface-guidelines/toolbars) instead. + +**Make sure the tab bar is visible when people navigate to different sections of your app.** If you hide the tab bar, people can forget which area of the app theyโ€™re in. The exception is when a modal view covers the tab bar, because a modal is temporary and self-contained. + +**Use the appropriate number of tabs required to help people navigate your app.** As a representation of your appโ€™s hierarchy, itโ€™s important to weigh the complexity of additional tabs against the need for people to frequently access each section; keep in mind that itโ€™s generally easier to navigate among fewer tabs. Where available, consider a sidebar or a tab bar that adapts to a sidebar as an alternative for an app with a complex information structure. + +**Avoid overflow tabs.** Depending on device size and orientation, the number of visible tabs can be smaller than the total number of tabs. If horizontal space limits the number of visible tabs, the trailing tab becomes a More tab in iOS and iPadOS, revealing the remaining items in a separate list. The More tab makes it harder for people to reach and notice content on tabs that are hidden, so limit scenarios in your app where this can happen. + +**Donโ€™t disable or hide tab bar buttons, even when their content is unavailable.** Having tab bar buttons available in some cases but not others makes your appโ€™s interface appear unstable and unpredictable. If a section is empty, explain why its content is unavailable. + +**Include tab labels to help with navigation.** A tab label appears beneath or beside a tab bar icon, and can aid navigation by clearly describing the type of content or functionality the tab contains. Use single words whenever possible. + +**Consider using SF Symbols to provide familiar, scalable tab bar icons.** When you use [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols), tab bar icons automatically adapt to different contexts. For example, the tab bar can be regular or compact, depending on the device and orientation. Tab bar icons appear above tab labels in compact views, whereas in regular views, the icons and labels appear side by side. Prefer filled symbols or icons for consistency with the platform. + +![An illustration of two iPhone devices side by side. The first iPhone is in landscape orientation with a tab bar at the bottom of the screen, with tab bar icons on the leading edge of each tab and tab labels on the trailing edge. The second iPhone is in portrait orientation with a tab bar at the bottom of the screen, with tab bar icons above their respective tab labels.](https://docs-assets.developer.apple.com/published/6871e7b24b6da37f753c61deba02c8ab/tab-bar-landscape%402x.png) + +If youโ€™re creating custom tab bar icons, see [Apple Design Resources](https://developer.apple.com/design/resources/) for tab bar icon dimensions. + +![A diagram of a tab bar, with callouts indicating the location of the tab bar icon and tab label.](https://docs-assets.developer.apple.com/published/eb47e442c964d54ed32f9324c71511d1/tab-bar-anatomy-callouts%402x.png) + +**Use a badge to indicate that critical information is available.** You can display a badge โ€” a red oval containing white text and either a number or an exclamation point โ€” on a tab to indicate that thereโ€™s new or updated information in the section that warrants a personโ€™s attention. Reserve badges for critical information so you donโ€™t dilute their impact and meaning. For guidance, see [Notifications](https://developer.apple.com/design/human-interface-guidelines/notifications). + +![An illustration of the bottom half of an iPhone in portrait orientation, with a tab bar at the bottom of the screen. Two of the tabs have red circular badges attached, indicating the presence of critical information.](https://docs-assets.developer.apple.com/published/29a93bc69eaa415e2e3d5440474a8d36/tab-bar-badges-iphone%402x.png) + +**Avoid applying a similar color to tab labels and content layer backgrounds.** If your app already has bright, colorful content in the content layer, prefer a monochromatic appearance for tab bars, or choose an accent color with sufficient visual differentiation. For more guidance, see [Liquid Glass color](https://developer.apple.com/design/human-interface-guidelines/color#Liquid-Glass-color). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/tab-bars#Platform-considerations) + + _No additional considerations for macOS. Not supported in watchOS._ + +### [iOS](https://developer.apple.com/design/human-interface-guidelines/tab-bars#iOS) + +A tab bar floats above content at the bottom of the screen. Its items rest on a [Liquid Glass](https://developer.apple.com/design/human-interface-guidelines/materials#Liquid-Glass) background that allows content beneath to peek through. + +For tab bars with an attached accessory, like the MiniPlayer in Music, you can choose to minimize the tab bar and move the accessory inline with it when a person scrolls down. A person can exit the minimized state by tapping a tab or scrolling to the top of the view. For developer guidance, see [`TabBarMinimizeBehavior`](https://developer.apple.com/documentation/SwiftUI/TabBarMinimizeBehavior) and [`UITabBarController.MinimizeBehavior`](https://developer.apple.com/documentation/UIKit/UITabBarController/MinimizeBehavior). + +![An illustration of the bottom half of an iPhone in portrait orientation, with the Music app open. The MiniPlayer is open above the tab bar at the bottom of the screen.](https://docs-assets.developer.apple.com/published/1b8fb04a802aacd9c9f46ba7b16be080/tab-bar-with-accessory-expanded%402x.png) + +A tab bar with an attached accessory, expanded + +![An illustration of the bottom half of an iPhone in portrait orientation, with the Music app open. The tab bar is minimized into the currently open tab at the leading bottom corner of the screen, with the MiniPlayer at the bottom center, and the search tab in the trailing corner.](https://docs-assets.developer.apple.com/published/d074ff4013a38155a887ceeecf2417fa/tab-bar-with-accessory-collapsed%402x.png) + +A tab bar with an attached accessory, minimized + +A tab bar can include a distinct search item at the trailing end. For guidance, see [Search fields](https://developer.apple.com/design/human-interface-guidelines/search-fields). + +### [iPadOS](https://developer.apple.com/design/human-interface-guidelines/tab-bars#iPadOS) + +The system displays a tab bar near the top of the screen. You can choose to have the tab bar appear as a fixed element, or with a button that converts it to a sidebar. For developer guidance, see [`tabBarOnly`](https://developer.apple.com/documentation/SwiftUI/TabViewStyle/tabBarOnly) and [`sidebarAdaptable`](https://developer.apple.com/documentation/SwiftUI/TabViewStyle/sidebarAdaptable). + + * Tab bar + * Sidebar + + + +![A screenshot showing the Music app on iPad with the tab bar near the top of the screen.](https://docs-assets.developer.apple.com/published/66af6b050f67a05a82c5df2acb99913a/ipad-tab-bar-music-app%402x.png) + +![A screenshot showing the Music app on iPad with the tab bar converted to a sidebar on the leading edge of the screen.](https://docs-assets.developer.apple.com/published/cb52cc194e4067efff244c3b991a02a4/ipad-sidebar-music-app%402x.png) + +Note + +To present a sidebar without the option to convert it to a tab bar, use a [navigation split view](https://developer.apple.com/documentation/swiftui/navigationsplitview) instead of a tab view. For guidance, see [Sidebars](https://developer.apple.com/design/human-interface-guidelines/sidebars). + +**Prefer a tab bar for navigation.** A tab bar provides access to the sections of your app that people use most. If your app is more complex, you can provide the option to convert the tab bar to a sidebar so people can access a wider set of navigation options. + +**Let people customize the tab bar.** In apps with a lot of sections that people might want to access, it can be useful to let people select items that they use frequently and add them to the tab bar, or remove items that they use less frequently. For example, in the Music app, a person can choose a favorite playlist to display in the tab bar. If you let people select their own tabs, aim for a default list of five or fewer to preserve continuity between compact and regular view sizes. For developer guidance, see [`TabViewCustomization`](https://developer.apple.com/documentation/SwiftUI/TabViewCustomization) and [`UITab.Placement`](https://developer.apple.com/documentation/UIKit/UITab/Placement). + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/tab-bars#tvOS) + +A tab bar is highly customizable. For example, you can: + + * Specify a tint, color, or image for the tab bar background + + * Choose a font for tab items, including a different font for the selected item + + * Specify tints for selected and unselected items + + * Add button icons, like settings and search + + + + +By default, a tab bar is translucent, and only the selected tab is opaque. When people use the remote to focus on the tab bar, the selected tab includes a drop shadow that emphasizes its selected state. The height of a tab bar is 68 points, and its top edge is 46 points from the top of the screen; you canโ€™t change either of these values. + +If there are more items than can fit in the tab bar, the system truncates the rightmost item by applying a fade effect that begins at the right side of the tab bar. If there are enough items to cause scrolling, the system also applies a truncating fade effect that starts from the left side. + +**Be aware of tab bar scrolling behaviors.** By default, people can scroll the tab bar offscreen when the current tab contains a single main view. You can see examples of this behavior in the Watch Now, Movies, TV Show, Sports, and Kids tabs in the TV app. The exception is when a screen contains a split view, such as the TV appโ€™s Library tab or an appโ€™s Settings screen. In this case, the tab bar remains pinned at the top of the view while people scroll the content within the primary and secondary panes of the split view. Regardless of a tabโ€™s contents, focus always returns to the tab bar at the top of the page when people press Menu on the remote. + +**In a live-viewing app, organize tabs in a consistent way.** For the best experience, organize content in live-streaming apps with tabs in the following order: + + * Live content + + * Cloud DVR or other recorded content + + * Other content + + + + +For additional guidance, see [Live-viewing apps](https://developer.apple.com/design/human-interface-guidelines/live-viewing-apps). + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/tab-bars#visionOS) + +In visionOS, a tab bar is always vertical, floating in a position thatโ€™s fixed relative to the windowโ€™s leading side. When people look at a tab bar, it automatically expands; to open a specific tab, people look at the tab and tap. While a tab bar is expanded, it can temporarily obscure the content behind it. + +Video with custom controls. + +Content description: A recording showing a closeup of a tab bar along the side of an app's window in visionOS. The tab bar includes only symbols. The currently selected tab receives the hover effect, showing that someone is looking at it, and the bar expands to display both symbols and labels. + +Play + +**Supply a symbol and a text label for each tab.** A tabโ€™s symbol is always visible in the tab bar. When people look at the tab bar, the system reveals tab labels, too. Even though the tab bar expands, you need to keep tab labels short so people can read them at a glance. + +![A screenshot showing a collapsed tab bar containing only symbols.](https://docs-assets.developer.apple.com/published/60282ea47a438f5b2bd84705212b44e4/visionos-tab-bar-collapsed%402x.png)Collapsed + +![A screenshot showing an expanded tab bar containing both symbols and labels.](https://docs-assets.developer.apple.com/published/df1a14ce3d5e2743bfdfb0fea47fc340/visionos-tab-bar-expanded%402x.png)Expanded + +**If it makes sense in your app, consider using a sidebar within a tab.** If your appโ€™s hierarchy is deep, you might want to use a [sidebar](https://developer.apple.com/design/human-interface-guidelines/sidebars) to support secondary navigation within a tab. If you do this, be sure to prevent selections in the sidebar from changing which tab is currently open. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/tab-bars#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/tab-bars#Related) + +[Tab views](https://developer.apple.com/design/human-interface-guidelines/tab-views) + +[Toolbars](https://developer.apple.com/design/human-interface-guidelines/toolbars) + +[Sidebars](https://developer.apple.com/design/human-interface-guidelines/sidebars) + +[Materials](https://developer.apple.com/design/human-interface-guidelines/materials) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/tab-bars#Developer-documentation) + +[`TabView`](https://developer.apple.com/documentation/SwiftUI/TabView) โ€” SwiftUI + +[`TabViewBottomAccessoryPlacement`](https://developer.apple.com/documentation/SwiftUI/TabViewBottomAccessoryPlacement) โ€” SwiftUI + +[Enhancing your appโ€™s content with tab navigation](https://developer.apple.com/documentation/SwiftUI/Enhancing-your-app-content-with-tab-navigation) โ€” SwiftUI + +[`UITabBar`](https://developer.apple.com/documentation/UIKit/UITabBar) โ€” UIKit + +[Elevating your iPad app with a tab bar and sidebar](https://developer.apple.com/documentation/UIKit/elevating-your-ipad-app-with-a-tab-bar-and-sidebar) โ€” UIKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/tab-bars#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/1AAA030E-2ECA-47D8-AE09-6D7B72A840F6/10044_wide_250x141_1x.jpg) Get to know the new design system ](https://developer.apple.com/videos/play/wwdc2025/356) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/873F40BE-101A-4C0D-99F0-F5C7CE7B47A3/10046_wide_250x141_1x.jpg) Elevate the design of your iPad app ](https://developer.apple.com/videos/play/wwdc2025/208) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/tab-bars#Change-log) + +Date| Changes +---|--- +December 16, 2025| Updated guidance for Liquid Glass. +July 28, 2025| Added guidance for Liquid Glass. +September 9, 2024| Added art representing the tab bar in iPadOS 18. +August 6, 2024| Updated with guidance for the tab bar in iPadOS 18. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/tab-views.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/tab-views.md new file mode 100644 index 00000000..fb3b0310 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/tab-views.md @@ -0,0 +1,68 @@ +--- +title: "Tab views | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/tab-views + +# Tab views + +A tab view presents multiple mutually exclusive panes of content in the same area, which people can switch between using a tabbed control. + +![A stylized representation of a view with two labeled tabs, the first of which is selected. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/4b2dbd07b3c6fe1d349d6db6aad5890b/components-tab-view-intro%402x.png) + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/tab-views#Best-practices) + +**Use a tab view to present closely related areas of content.** The appearance of a tab view provides a strong visual indication of enclosure. People expect each tab to display content that is in some way similar or related to the content in the other tabs. + +**Make sure the controls within a pane affect content only in the same pane.** Panes are mutually exclusive, so ensure theyโ€™re fully self-contained. + +**Provide a label for each tab that describes the contents of its pane.** A good label helps people predict the contents of a pane before clicking or tapping its tab. In general, use nouns or short noun phrases for tab labels. A verb or short verb phrase may make sense in some contexts. Use title-style capitalization for tab labels. + +**Avoid using a pop-up button to switch between tabs.** A tabbed control is efficient because it requires a single click or tap to make a selection, whereas a pop-up button requires two. A tabbed control also presents all choices onscreen at the same time, whereas people must click a pop-up button to see its choices. Note that a pop-up button can be a reasonable alternative in cases where there are too many panes of content to reasonably display with tabs. + +**Avoid providing more than six tabs in a tab view.** Having more than six tabs can be overwhelming and create layout issues. If you need to present six or more tabs, consider another way to implement the interface. For example, you could instead present each tab as a view option in a pop-up button menu. + +For developer guidance, see [`NSTabView`](https://developer.apple.com/documentation/AppKit/NSTabView). + +## [Anatomy](https://developer.apple.com/design/human-interface-guidelines/tab-views#Anatomy) + +The tabbed control appears on the top edge of the content area. You can choose to hide the control, which is appropriate for an app that switches between panes programmatically. + +![An illustration of a window in which a three-tab tabbed control is centered on the top edge of the content view.](https://docs-assets.developer.apple.com/published/05bb7fbc6365c3bab10db218644756c3/tab-views-top%402x.png) + +When you hide the tabbed control, the content area can be borderless, bezeled, or bordered with a line. A borderless view can be solid or transparent. + +**In general, inset a tab view by leaving a margin of window-body area on all sides of a tab view.** This layout looks clean and leaves room for additional controls that arenโ€™t directly related to the contents of the tab view. You can extend a tab view to meet the window edges, but this layout is unusual. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/tab-views#Platform-considerations) + + _Not supported in iOS, iPadOS, tvOS, or visionOS._ + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/tab-views#iOS-iPadOS) + +For similar functionality, consider using a [segmented control](https://developer.apple.com/design/human-interface-guidelines/segmented-controls) instead. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/tab-views#watchOS) + +watchOS displays tab views using [page controls](https://developer.apple.com/design/human-interface-guidelines/components/presentation/page-controls). For developer guidance, see [`TabView`](https://developer.apple.com/documentation/SwiftUI/TabView) and [`verticalPage`](https://developer.apple.com/documentation/SwiftUI/TabViewStyle/verticalPage). + +![An illustration showing the page control next to the Digital Crown on Apple Watch. The current dot is enlarged, indicating that people can scroll through the current content, as well as scroll between pages.](https://docs-assets.developer.apple.com/published/10938a94cb663210f148e0fbce431e70/tab-view-watch-vertical%402x.png) + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/tab-views#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/tab-views#Related) + +[Tab bars](https://developer.apple.com/design/human-interface-guidelines/tab-bars) + +[Segmented controls](https://developer.apple.com/design/human-interface-guidelines/segmented-controls) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/tab-views#Developer-documentation) + +[`TabView`](https://developer.apple.com/documentation/SwiftUI/TabView) โ€” SwiftUI + +[`NSTabView`](https://developer.apple.com/documentation/AppKit/NSTabView) โ€” AppKit + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/tab-views#Change-log) + +Date| Changes +---|--- +June 5, 2023| Added guidance for using tab views in watchOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/windows.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/windows.md new file mode 100644 index 00000000..17fd3813 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-layout/references/windows.md @@ -0,0 +1,188 @@ +--- +title: "Windows | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/windows + +# Windows + +A window presents UI views and components in your app or game. + +![A stylized representation of a window with close, minimize, and full-screen buttons. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/3c5ea22db1d7d414c160c95ed7f62ec9/components-window-intro%402x.png) + +In iPadOS, macOS, and visionOS, windows help define the visual boundaries of app content and separate it from other areas of the system, and enable multitasking workflows both within and between apps. Windows include system-provided interface elements such as frames and window controls that let people open, close, resize, and relocate them. + +Conceptually, apps use two types of windows to display content: + + * A _primary_ window presents the main navigation and content of an app, and actions associated with them. + + * An _auxiliary_ window presents a specific task or area in an app. Dedicated to one experience, an auxiliary window doesnโ€™t allow navigation to other app areas, and it typically includes a button people use to close it after completing the task. + + + + +For guidance laying out content within a window on any platform, see [Layout](https://developer.apple.com/design/human-interface-guidelines/layout); for guidance laying out content in Apple Vision Pro space, see [Spatial layout](https://developer.apple.com/design/human-interface-guidelines/spatial-layout). For developer guidance, see [Windows](https://developer.apple.com/documentation/SwiftUI/Windows). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/windows#Best-practices) + +**Make sure that your windows adapt fluidly to different sizes to support multitasking and multiwindow workflows.** For guidance, see [Layout](https://developer.apple.com/design/human-interface-guidelines/layout) and [Multitasking](https://developer.apple.com/design/human-interface-guidelines/multitasking). + +**Choose the right moment to open a new window.** Opening content in a separate window is great for helping people multitask or preserve context. For example, Mail opens a new window whenever someone selects the Compose action, so both the new message and the existing email are visible at the same time. However, opening new windows excessively creates clutter and can make navigating your app more confusing. Avoid opening new windows as default behavior unless it makes sense for your app. + +**Consider providing the option to view content in a new window.** While itโ€™s best to avoid opening new windows as default behavior unless it benefits your user experience, itโ€™s also great to give people the flexibility of viewing content in multiple ways. Consider letting people view content in a new window using a command in a [context menu](https://developer.apple.com/design/human-interface-guidelines/context-menus) or in the [File menu](https://developer.apple.com/design/human-interface-guidelines/the-menu-bar#File-menu). For developer guidance, see [`OpenWindowAction`](https://developer.apple.com/documentation/SwiftUI/OpenWindowAction). + +**Avoid creating custom window UI.** System-provided windows look and behave in a way that people understand and recognize. Avoid making custom window frames or controls, and donโ€™t try to replicate the system-provided appearance. Doing so without perfectly matching the systemโ€™s look and behavior can make your app feel broken. + +**Use the term _window_ in user-facing content.** The system refers to app windows as _windows_ regardless of type. Using different terms โ€” including _scene_ , which refers to window implementation โ€” is likely to confuse people. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/windows#Platform-considerations) + + _Not supported in iOS, tvOS, or watchOS._ + +### [iPadOS](https://developer.apple.com/design/human-interface-guidelines/windows#iPadOS) + +Windows present in one of two ways depending on a personโ€™s choice in Multitasking & Gestures settings. + + * **Full screen.** App windows fill the entire screen, and people switch between them โ€” or between multiple windows of the same app โ€” using the app switcher. + + * **Windowed.** People can freely resize app windows. Multiple windows can be onscreen at once, and people can reposition them and bring them to the front. The system remembers window size and placement even when an app is closed. + + + + + * Full screen + * Windowed + + + +![A screenshot of the Notes app in full screen on iPad, with an open document titled Nature Walks. The app interface fills the entire screen, with no visible border to the window.](https://docs-assets.developer.apple.com/published/5daa697ab73d7e08de1e4fa78a56bfcb/windows-ipad-notes-fullscreen%402x.png) + +![A screenshot of the Notes app in a window on iPad, with an open document titled Nature Walks. The document window occupies the center of the screen, with the Home Screen background filling the rest of the screen behind it, and the Dock at the bottom.](https://docs-assets.developer.apple.com/published/0d1eca9806c6d60816eb9b6f436c28d3/windows-ipad-notes-windowed%402x.png) + +**Make sure window controls donโ€™t overlap toolbar items.** When windowed, app windows include window controls at the leading edge of the toolbar. If your app has toolbar buttons at the leading edge, they might be hidden by window controls when they appear. To prevent this, instead of placing buttons directly on the leading edge, move them inward when the window controls appear. + +**Consider letting people use a gesture to open content in a new window.** For example, people can use the pinch gesture to expand a Notes item into a new window. For developer guidance, see [`collectionView(_:sceneActivationConfigurationForItemAt:point:)`](https://developer.apple.com/documentation/UIKit/UICollectionViewDelegate/collectionView\(_:sceneActivationConfigurationForItemAt:point:\)) (to transition from a collection view item), or [`UIWindowScene.ActivationInteraction`](https://developer.apple.com/documentation/UIKit/UIWindowScene/ActivationInteraction) (to transition from an item in any other view). + +Tip + +If you only need to let people view one file, you can present it without creating your own window, but you must support multiple windows in your app. For developer guidance, see [`QLPreviewSceneActivationConfiguration`](https://developer.apple.com/documentation/QuickLook/QLPreviewSceneActivationConfiguration). + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/windows#macOS) + +In macOS, people typically run several apps at the same time, often viewing windows from multiple apps on one desktop and switching frequently between different windows โ€” moving, resizing, minimizing, and revealing the windows to suit their work style. + +To learn about setting up a window to display your game in macOS, see [Managing your game window for Metal in macOS](https://developer.apple.com/documentation/Metal/managing-your-game-window-for-metal-in-macos). + +#### [macOS window anatomy](https://developer.apple.com/design/human-interface-guidelines/windows#macOS-window-anatomy) + +A macOS window consists of a frame and a body area. People can move a window by dragging the frame and can often resize the window by dragging its edges. + +The _frame_ of a window appears above the body area and can include window controls and a [toolbar](https://developer.apple.com/design/human-interface-guidelines/toolbars). In rare cases, a window can also display a bottom bar, which is a part of the frame that appears below body content. + +#### [macOS window states](https://developer.apple.com/design/human-interface-guidelines/windows#macOS-window-states) + +A macOS window can have one of three states: + + * **Main.** The frontmost window that people view is an appโ€™s main window. There can be only one main window per app. + + * **Key.** Also called the _active window_ , the key window accepts peopleโ€™s input. There can be only one key window onscreen at a time. Although the front appโ€™s main window is usually the key window, another window โ€” such as a panel floating above the main window โ€” might be key instead. People typically click a window to make it key; when people click an appโ€™s Dock icon to bring all of that appโ€™s windows forward, only the most recently accessed window becomes key. + + * **Inactive.** A window thatโ€™s not in the foreground is an inactive window. + + + + +The system gives main, key, and inactive windows different appearances to help people visually identify them. For example, the key window uses color in the title bar options for closing, minimizing, and zooming; inactive windows and main windows that arenโ€™t key use gray in these options. Also, inactive windows donโ€™t use [vibrancy](https://developer.apple.com/design/human-interface-guidelines/materials) (an effect that can pull color into a window from the content underneath it), which makes them appear subdued and seem visually farther away than the main and key windows. + +![An illustration of a stack of three windows, as follows: An inactive window in the background, an appโ€™s main window in the middle, and a key window appearing above the other two windows.](https://docs-assets.developer.apple.com/published/7ecd910726f347fb452d9ecd2b492d22/window-states%402x.png) + +Note + +Some windows โ€” typically, panels like Colors or Fonts โ€” become the key window only when people click the windowโ€™s title bar or a component that requires keyboard input, such as a text field. + +**Make sure custom windows use the system-defined appearances.** People rely on the visual differences between windows to help them identify the foreground window and know which window will accept their input. When you use system-provided components, a windowโ€™s background and button appearances update automatically when the window changes state; if you use custom implementations, you need to do this work yourself. + +**Avoid putting critical information or actions in a bottom bar, because people often relocate a window in a way that hides its bottom edge.** If you must include one, use it only to display a small amount of information directly related to a windowโ€™s contents or to a selected item within it. For example, Finder uses a bottom bar (called the status bar) to display the total number of items in a window, the number of selected items, and how much space is available on the disk. A bottom bar is small, so if you have more information to display, consider using an inspector, which typically presents information on the trailing side of a split view. + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/windows#visionOS) + +visionOS defines two main window styles: default and volumetric. Both a default window (called a _window_) and a volumetric window (called a _volume_) can display 2D and 3D content, and people can view multiple windows and volumes at the same time in both the Shared Space and a Full Space. + +![An illustration representing a window in visionOS. The illustration consists of two parallel rounded rectangles, slightly separated and displayed on an angle, positioned above a window bar.](https://docs-assets.developer.apple.com/published/e8dc51484c2e5f3289a5f6a878f4c47d/visionos-window-style-2d-window%402x.png)A window + +![An illustration representing a volume in visionOS. The illustration consists of a translucent cube. The base of the cube is darker than the other sides. The front of the cube is positioned above a window bar.](https://docs-assets.developer.apple.com/published/92d953d099f72f9909c47bad408f4c9b/visionos-window-style-3d-volume%402x.png)A volume + +Note + +visionOS also defines the _plain_ window style, which is similar to the default style, except that the upright plane doesnโ€™t use the glass background. For developer guidance, see [`PlainWindowStyle`](https://developer.apple.com/documentation/SwiftUI/PlainWindowStyle). + +The system defines the initial position of the first window or volume people open in your app or game. In both the Shared Space and a Full Space, people can move windows and volumes to new locations. + +#### [visionOS windows](https://developer.apple.com/design/human-interface-guidelines/windows#visionOS-windows) + +The default window style consists of an upright plane that uses an unmodifiable background [material](https://developer.apple.com/design/human-interface-guidelines/materials) called _glass_ and includes a close button, window bar, and resize controls that let people close, move, and resize the window. A window can also include a Share button, [tab bar](https://developer.apple.com/design/human-interface-guidelines/tab-bars), [toolbar](https://developer.apple.com/design/human-interface-guidelines/toolbars), and one or more [ornaments](https://developer.apple.com/design/human-interface-guidelines/ornaments). By default, visionOS uses dynamic [scale](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Scale) to help a windowโ€™s size appear to remain consistent regardless of its proximity to the viewer. For developer guidance, see [`DefaultWindowStyle`](https://developer.apple.com/documentation/SwiftUI/DefaultWindowStyle). + +![A screenshot of a window for an app named 'Hello World' in visionOS. The window includes text and buttons for entering different experiences.](https://docs-assets.developer.apple.com/published/95650cb19e1930e6b08ca5aa3b5b06a0/visionos-window-2d%402x.png)A window + +**Prefer using a window to present a familiar interface and to support familiar tasks.** Help people feel at home in your app by displaying an interface theyโ€™re already comfortable with, reserving more [immersive experiences](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences) for the meaningful content and activities you offer. If you want to showcase bounded 3D content like a game board, consider using a [volume](https://developer.apple.com/design/human-interface-guidelines/windows#visionOS-volumes). + +**Retain the windowโ€™s glass background.** The default glass background helps your content feel like part of peopleโ€™s surroundings while adapting dynamically to lighting and using specular reflections and shadows to communicate the windowโ€™s scale and position. Removing the glass material tends to cause UI elements and text to become less legible and to no longer appear related to each other; using an opaque background obscures peopleโ€™s surroundings and can make a window feel constricting and heavy. + +**Choose an initial window size that minimizes empty areas within it.** By default, a window measures 1280x720 pt. When a window first opens, the system places it about two meters in front of the wearer, giving it an apparent width of about three meters. Too much empty space inside a window can make it look unnecessarily large while also obscuring other content in peopleโ€™s space. + +**Aim for an initial shape that suits a windowโ€™s content.** For example, a default Keynote window is wide because slides are wide, whereas a default Safari window is tall because most webpages are much longer than they are wide. For games, a tower-building game is likely to open in a taller window than a driving game. + +**Choose a minimum and maximum size for each window to help keep your content looking great.** People appreciate being able to resize windows as they customize their space, but you need to make sure your layout adjusts well across all sizes. If you donโ€™t set a minimum and maximum size for a window, people could make it so small that UI elements overlap or so large that your app or game becomes unusable. For developer guidance, see [Positioning and sizing windows](https://developer.apple.com/documentation/visionOS/positioning-and-sizing-windows). + +![A screenshot of a window for an app in visionOS. The window includes text that discusses objects in orbit, and it includes buttons for viewing a satellite, the moon, and a telescope. The satellite button is selected and a 3D satellite is displayed.](https://docs-assets.developer.apple.com/published/db1e41fe4000281898003f792ff037c8/visionos-window-2d-with-volume%402x.png)A window containing 3D content + +**Minimize the depth of 3D content you display in a window.** The system adds highlights and shadows to the views and controls within a window, giving them the appearance of [depth](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Depth) and helping them feel more substantial, especially when people view the window from an angle. Although you can display 3D content in a window, the system clips it if the content extends too far from the windowโ€™s surface. To display 3D content that has greater depth, use a volume. + +#### [visionOS volumes](https://developer.apple.com/design/human-interface-guidelines/windows#visionOS-volumes) + +You can use a volume to display 2D or 3D content that people can view from any angle. A volume includes window-management controls just like a window, but unlike in a window, a volumeโ€™s close button and window bar shift position to face the viewer as they move around the volume. For developer guidance, see [`VolumetricWindowStyle`](https://developer.apple.com/documentation/SwiftUI/VolumetricWindowStyle). + +![A screenshot of a volume containing a 3D globe in visionOS, beside a window.](https://docs-assets.developer.apple.com/published/99098a290c36254e48329511216e1d5a/visionos-window-3d%402x.png)A volume + +**Prefer using a volume to display rich, 3D content.** In contrast, if you want to present a familiar, UI-centric interface, it generally works best to use a [window](https://developer.apple.com/design/human-interface-guidelines/windows#visionOS-windows). + +**Place 2D content so it looks good from multiple angles.** Because a personโ€™s perspective changes as they move around a volume, the location of 2D content within it might appear to change in ways that donโ€™t make sense. To pin 2D content to specific areas of 3D content inside a volume, you can use an attachment. + +**In general, use dynamic scaling.** Dynamic scaling helps a volumeโ€™s content remain comfortably legible and easy to interact with, even when itโ€™s far away from the viewer. On the other hand, if you want a volumeโ€™s content to represent a real-world object, like a product in a retail app, you can use fixed scaling (this is the default). + +**Take advantage of the default baseplate appearance to help people discern the edges of a volume.** In visionOS 2 and later, the system automatically makes a volumeโ€™s horizontal โ€œfloor,โ€ or _baseplate_ , visible by displaying a gentle glow around its border when people look at it. If your content doesnโ€™t fill the volume, the system-provided glow can help people become aware of the volumeโ€™s edges, which can be particularly useful in keeping the resize control easy to find. On the other hand, if your content is full bleed or fills the volumeโ€™s bounds โ€” or if you display a custom baseplate appearance โ€” you may not want the default glow. + +**Consider offering high-value content in an ornament.** In visionOS 2 and later, a volume can include an ornament in addition to a toolbar and tab bar. You can use an ornament to reduce clutter in a volume and elevate important views or controls. When you use an attachment anchor to specify the ornamentโ€™s location, such as `topBack` or `bottomFront`, the ornament remains in the same position, relative to the viewerโ€™s perspective, as they move around the volume. Be sure to avoid placing an ornament on the same edge as a toolbar or tab bar, and prefer creating only one additional ornament to avoid overshadowing the important content in your volume. For developer guidance, see [`ornament(visibility:attachmentAnchor:contentAlignment:ornament:)`](https://developer.apple.com/documentation/SwiftUI/View/ornament\(visibility:attachmentAnchor:contentAlignment:ornament:\)). + +**Choose an alignment that supports the way people interact with your volume.** As people move a volume, the baseplate can remain parallel to the floor of a personโ€™s surroundings, or it can tilt to match the angle at which a person is looking. In general, a volume that remains parallel to the floor works well for content that people donโ€™t interact with much, whereas a volume that tilts to match where a person is looking can keep content comfortably usable, even when the viewer is reclining. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/windows#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/windows#Related) + +[Layout](https://developer.apple.com/design/human-interface-guidelines/layout) + +[Split views](https://developer.apple.com/design/human-interface-guidelines/split-views) + +[Multitasking](https://developer.apple.com/design/human-interface-guidelines/multitasking) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/windows#Developer-documentation) + +[Windows](https://developer.apple.com/documentation/SwiftUI/Windows) โ€” SwiftUI + +[`WindowGroup`](https://developer.apple.com/documentation/SwiftUI/WindowGroup) โ€” SwiftUI + +[`UIWindow`](https://developer.apple.com/documentation/UIKit/UIWindow) โ€” UIKit + +[`NSWindow`](https://developer.apple.com/documentation/AppKit/NSWindow) โ€” AppKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/windows#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/873F40BE-101A-4C0D-99F0-F5C7CE7B47A3/10046_wide_250x141_1x.jpg) Elevate the design of your iPad app ](https://developer.apple.com/videos/play/wwdc2025/208) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/windows#Change-log) + +Date| Changes +---|--- +June 9, 2025| Added best practices, and updated with guidance for resizable windows in iPadOS. +June 10, 2024| Updated to include guidance for using volumes in visionOS 2 and added game-specific examples. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/SKILL.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/SKILL.md new file mode 100644 index 00000000..e504853d --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/SKILL.md @@ -0,0 +1,101 @@ +--- +name: hig-components-system +description: 'Apple HIG guidance for system experience components: widgets, live activities, notifications, complications, home screen quick actions, top shelf, watch faces, app clips, and app shortcuts.' +risk: unknown +source: community +date_added: '2026-02-27' +--- + +# Apple HIG: System Experiences + +Check for `.claude/apple-design-context.md` before asking questions. Use existing context and only ask for information not already covered. + +## Key Principles + +### General + +1. **Glanceable, immediate value.** System experiences bring your app's most important content to surfaces the user sees without launching your app. Design for seconds of attention. + +2. **Respect platform context.** A Lock Screen widget has different constraints than a Home Screen widget. A complication is far smaller than a top shelf item. + +### Widgets + +3. **Show relevant information, not everything.** Display the most useful subset, updated appropriately. + +4. **Support multiple sizes with distinct layouts.** Each size should be a thoughtful design, not a scaled version of another. + +5. **Deep-link on tap.** Take users to the relevant content, not the app's root screen. + +### Live Activities + +6. **Track events with a clear start and end.** Deliveries, scores, timers, rides. Design for both Dynamic Island and Lock Screen. + +7. **Stay updated and timely.** Stale data undermines trust. End promptly when the event concludes. + +### Notifications + +8. **Respect user attention.** Only send notifications for information users genuinely care about. No promotional or low-value notifications. + +9. **Actionable and self-contained.** Include enough context to understand and act without opening the app. Support notification actions. Use threading and grouping. + +### Complications + +10. **Focused data on the watch face.** Design for the smallest useful representation. Support multiple families. Budget updates wisely. + +### Home Screen Quick Actions + +11. **3-4 most common tasks.** Short titles, optional subtitles, relevant SF Symbol icons. + +### Top Shelf + +12. **tvOS showcase.** Feature content that entices: new episodes, featured items, recent content. + +### App Clips + +13. **Instant, focused functionality within a strict size budget.** Load quickly without App Store download. Only what's needed for the immediate task, then offer full app install. + +### App Shortcuts + +14. **Surface key actions to Siri and Spotlight.** Define shortcuts for frequent tasks. Use natural, conversational trigger phrases. + +## Reference Index + +| Reference | Topic | Key content | +|---|---|---| +| [widgets.md](references/widgets.md) | Widgets | Glanceable info, sizes, deep linking, timeline | +| [live-activities.md](references/live-activities.md) | Live Activities | Real-time tracking, Dynamic Island, Lock Screen | +| [notifications.md](references/notifications.md) | Notifications | Attention, actions, grouping, content | +| [complications.md](references/complications.md) | Complications | Watch face data, families, budgeted updates | +| [home-screen-quick-actions.md](references/home-screen-quick-actions.md) | Quick actions | Haptic Touch, common tasks, SF Symbols | +| [top-shelf.md](references/top-shelf.md) | Top shelf | Featured content, showcase | +| [app-clips.md](references/app-clips.md) | App Clips | Instant use, lightweight, focused task, NFC/QR | +| [watch-faces.md](references/watch-faces.md) | Watch faces | Custom complications, face sharing | +| [app-shortcuts.md](references/app-shortcuts.md) | App Shortcuts | Siri, Spotlight, voice triggers | + +## Output Format + +1. **System experience recommendation** -- which surface best fits the use case. +2. **Content strategy** -- what to display, priority, what to omit. +3. **Update frequency** -- refresh rate including system budget constraints. +4. **Size/family variants** -- which to support and how layout adapts. +5. **Deep link behavior** -- where tapping takes the user. + +## Questions to Ask + +1. What information needs to surface outside the app? +2. Which platform? +3. How frequently does the data update? +4. What is the primary glanceable need? + +## Related Skills + +- **hig-components-status** -- Progress indicators in widgets or Live Activities +- **hig-inputs** -- Interaction patterns for system experiences (Digital Crown for complications) +- **hig-technologies** -- Siri for App Shortcuts, HealthKit for complications, NFC for App Clips + +--- + +*Built by [Raintree Technology](https://raintree.technology) ยท [More developer tools](https://raintree.technology)* + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/app-clips.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/app-clips.md new file mode 100644 index 00000000..62cf006f --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/app-clips.md @@ -0,0 +1,387 @@ +--- +title: "App Clips | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/app-clips + +# App Clips + +An App Clip is a lightweight version of your app or game that provides an on-the-go or demo experience thatโ€™s instantly available. + +![A sketch of an app icon surrounded by a dashed line, suggesting an App Clip. The image is overlaid with rectangular and circular grid lines and is tinted blue to subtly reflect the blue in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/45d063cf460141fa261373aeb111078f/technologies-app-clips-intro%402x.png) + +App Clips deliver an experience from your app or game without requiring people to download the full app from the App Store. App Clips focus on a fast solution to a task or contain a demo that showcases the full app or game, and they remain on the device for a limited amount of time while preserving peopleโ€™s privacy. + +People discover and launch App Clips in a variety of situations and contexts. At a physical location, people launch an App Clip by scanning an App Clip Code, NFC tag, or a QR code. An App Clip Code tends to be the best way for people to discover and launch your App Clip because its distinct design is immediately recognizable, and people trust it to offer a fast, secure way to launch an App Clip. + +On their device, people launch an App Clip from location-based suggestions they permit in Siri Suggestions, the Maps app, Smart App Banners on websites, App Clip cards in Safari, and by tapping links others share with them in the Messages app. Starting with iOS 17, an app can include links and App Clip previews that people tap to launch another appโ€™s App Clip. + +![A screenshot of an iPhone Lock Screen. The bottom half of the screen shows the App Clip card for a donut shopโ€™s App Clip as it appears when the person invokes the App Clip.](https://docs-assets.developer.apple.com/published/513ba0042660873295cdbab4a21dccac/app-clips-hero-1%402x.png) + +![A screenshot of a donut shopโ€™s App Clip on iPhone as it appears when a person confirms the App Clipโ€™s launch on the App Clip card. The App Clip displays a list with various donuts the person can order.](https://docs-assets.developer.apple.com/published/80ef3fe27d095cad107352f204283551/app-clips-hero-2%402x.png) + +Consider creating an App Clip if your app provides an in-the-moment experience that helps people perform a task over a finite amount of time. For example: + + * A rental bike could come with an App Clip Code that people tap or scan to launch an App Clip that lets them rent the bike. + + * A coffee shop could offer an App Clip for fast advance orders that customers launch from a Smart App Banner or an App Clip card on the shopโ€™s website. Customers could share a link to the website from the Messages app, which recipients then tap to launch the App Clip from within Messages. + + * A food truck could create marketing material (for example, a poster to promote a seasonal dish) that includes an App Clip Code. People can scan the App Clip Code with the Camera app on their device and instantly launch the App Clip to order the seasonal dish. + + * A restaurant could let diners pay for a meal by launching an App Clip from the Maps app or a suggestion from Siri Suggestions, or by holding their device close to an App Clip Code or NFC tag at their table. + + * A museum could have visitors scan App Clip Codes or QR codes on labels next to displayed works to launch an App Clip that reveals augmented reality content or provides audio commentary. + + + + +Consider creating an App Clip to let people experience your app or game before committing to a purchase or subscription. Focus on providing people with an opportunity to experience and understand your app or game. For example: + + * A game might offer an App Clip that lets people play a demo version of the game, including a tutorial and the first level of the game. + + * A fitness app might offer an App Clip with a free workout and a guided meditation. + + * A text editor might allow people to create and save a document using the demo App Clip. + + + + +For developer guidance, see [App Clips](https://developer.apple.com/documentation/AppClip). + +## [Designing your App Clip](https://developer.apple.com/design/human-interface-guidelines/app-clips#Designing-your-App-Clip) + +**Allow people to complete a task or a demo in your App Clip.** Donโ€™t require people to install the full app to experience the entire demo, to complete a task, or to finish a level in a game. + +**Focus on essential features.** Interactions with App Clips are quick and focused. Limit features to whatโ€™s necessary to accomplish the task at hand. Reserve advanced or complex features for the app. If you offer a demo version of your full app, focus on essential features that give people a good sense of your game or your appโ€™s functionality. + +**Donโ€™t use App Clips solely for marketing purposes.** App Clips need to provide real value and help people accomplish tasks. Donโ€™t use them as a means to advertise services or products, and donโ€™t display ads in your App Clip. + +**Avoid using web views in your App Clip.** App Clips use native components and frameworks to offer an app-quality experience. If only web components are available to you, offer a quick link to your website instead of an App Clip. + +**Design a linear, easy-to-use, and focused user interface.** App Clips donโ€™t need tab bars, complex navigation, or settings. Keep the number of screens and entry forms to a minimum. Remove extraneous information and reduce complexity in the user interface wherever possible. + +**On launch, show the most relevant part of your App Clip.** Skip unnecessary steps and take people immediately to the part of the App Clip that best fits their context. + +**Ensure people can use your App Clip immediately.** App Clips need to include all required assets, omit splash screens, and never make people wait on launch. + +**Ensure your App Clip is small.** The smaller your App Clip, the faster it will launch on a personโ€™s device. Keeping your App Clip small is especially important when bandwidth is limited. As much as possible, reduce unnecessary code and remove unused assets. Avoid downloading additional data, which can take away the feeling of immediacy. + +**Make the App Clip shareable.** When someone shares a link to an App Clip in the Messages app, recipients can launch the App Clip from within the Messages app. Offer the ability to share links to specific points in your App Clip, and encourage people to share the App Clip with others. + +**Make it easy to pay for a service or product.** Entering payment information can be a long and error-prone task. Consider supporting [Apple Pay](https://developer.apple.com/design/human-interface-guidelines/apple-pay) to offer express checkout and let people enter shipping information with no typing. + +**Avoid requiring people to create an account before they can benefit from your App Clip.** Creating an account is a complex task that takes time and effort. Consider not requiring an account, or think about asking people to create an account after they finish a task. If your App Clip requires an account to provide value, limit the amount of information people need to provide โ€” for example, by offering [Sign in with Apple](https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple). + +**Provide a familiar, focused experience in your app.** When people install the full app, it replaces the App Clip on their device. From this moment, invocations that would have launched the App Clip launch the full app instead. Ensure your app provides a focused, familiar experience to people who previously used the App Clip. Donโ€™t require additional steps that slow people down; for example, donโ€™t require people to log in again when they transition from the App Clip to the app. + +### [Preserving privacy](https://developer.apple.com/design/human-interface-guidelines/app-clips#Preserving-privacy) + +The system imposes limits on App Clips to ensure peopleโ€™s privacy. For example, App Clips canโ€™t perform background operations. For developer guidance, see [Choosing the right functionality for your App Clip](https://developer.apple.com/documentation/AppClip/choosing-the-right-functionality-for-your-app-clip). + +**Limit the amount of data you store and handle yourself.** If you need to store peopleโ€™s data โ€” for example, login information โ€” store it securely. In addition, donโ€™t rely on the availability of data you previously stored on the device โ€” the system may have removed the App Clip from the device between launches and deleted all of its data. If you store login information, securely store it off the device. + +**Consider offering Sign in with Apple.** Sign in with Apple securely retains login information off peopleโ€™s devices and preserves their privacy. For guidance, see [Sign in with Apple](https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple). + +**Offer a secure way to pay for services or goods that also respects peopleโ€™s privacy.** For example, consider offering [Apple Pay](https://developer.apple.com/design/human-interface-guidelines/apple-pay). + +### [Showcasing your app](https://developer.apple.com/design/human-interface-guidelines/app-clips#Showcasing-your-app) + +People donโ€™t manage App Clips themselves, and App Clips donโ€™t appear on the Home screen. Instead, the system removes an App Clip after a period of inactivity. + +Because apps remain the best way to keep people engaged over time, the system helps them discover and install the full app: + + * On the App Clip card, people can either launch the App Clip or visit the full appโ€™s page on the App Store. + + * When people first launch the App Clip, the system displays an app banner at the top of the screen. Like the App Clip card, the banner allows people to visit the appโ€™s page on the App Store. + + + + +In addition, you can display an overlay in your App Clip that allows people to download the full app from within the App Clip. + +**Donโ€™t compromise the user experience by asking people to install the full app.** If your App Clip offers an on-the-go experience, consider whether the App Clip card and the system-provided app banner provide enough incentive for people to download the full app. If your App Clip offers a demo experience, let people fully experience the demo before asking them to install the full app. + +**Pick the right time to recommend your app.** When someone completes a task or reaches a natural pause, display an [`SKOverlay`](https://developer.apple.com/documentation/StoreKit/SKOverlay) that allows people to initiate a download of your full app or game from the context of the App Clip. + +**Recommend your app in a nonintrusive, polite way.** Donโ€™t ask people to install the full app repeatedly or interrupt them during a task. Push notifications arenโ€™t a good way to ask people to install the app either. Clearly communicate your appโ€™s additional features. + +For developer guidance, see [Recommending your app to App Clip users](https://developer.apple.com/documentation/AppClip/recommending-your-app-to-app-clip-users). + +### [Limiting notifications](https://developer.apple.com/design/human-interface-guidelines/app-clips#Limiting-notifications) + +App Clips provide the option to schedule and receive notifications for up to 8 hours after launch, enough time to follow up and complete most common tasks. + +**Only ask for permission to use notifications for an extended period of time if itโ€™s really needed.** If your App Clipโ€™s functionality spans more than a day, explicitly request permission to schedule and receive notifications. For example, a car rental companyโ€™s App Clip can ask for permission to send a notification that reminds people that they need to return a rented car soon. + +**Keep notifications focused.** Donโ€™t send purely promotional notifications, and only use notifications in response to an explicit user action. If a person completes their task without leaving the App Clip, you might not need to send any notifications at all. + +**Use notifications to help people complete a task.** Notifications for an App Clip relate directly to the task the App Clip helps to accomplish. For example, an App Clip that helps people order food could send notifications related to a scheduled delivery. + +For developer guidance, see [Enabling notifications in App Clips](https://developer.apple.com/documentation/AppClip/enabling-notifications-in-app-clips). + +### [Creating App Clips for businesses](https://developer.apple.com/design/human-interface-guidelines/app-clips#Creating-App-Clips-for-businesses) + +If youโ€™re a platform provider who services businesses, you may create several App Clip experiences in [App Store Connect](https://appstoreconnect.apple.com) and use a single App Clip to power them all. To people using the App Clip, it appears with the branding of an individual business or location instead of your own branding. + +**Use consistent branding.** When people see the App Clip card for a business, the brand for that business is front and center. Tone down your own branding and make sure the branding for the business is clearly visible to avoid confusing people when they enter the App Clip experience. + +**Consider multiple businesses.** An App Clip may power many different businesses or a business that has multiple locations. In both scenarios, people may end up using the App Clip for more than one business or location at a time. The App Clip must handle this use case and update its user interface accordingly. For example, consider a way to switch between recent businesses or locations within your App Clip, and verify a personโ€™s location when they launch it. + +For developer guidance, see [Configuring App Clip experiences](https://developer.apple.com/documentation/AppClip/configuring-the-launch-experience-of-your-app-clip). + +## [Creating content for an App Clip card](https://developer.apple.com/design/human-interface-guidelines/app-clips#Creating-content-for-an-App-Clip-card) + +The system-provided App Clip card is peopleโ€™s first interaction with your App Clip, so give careful consideration to its images and copy. + +**Be informative.** Make sure the image on the App Clip card clearly communicates the features offered by your App Clip, supported tasks, or content. + +**Prefer photography and graphics.** Avoid using a screenshot of your appโ€™s user interface because itโ€™s unlikely to communicate the purpose of your App Clip. Instead, use an image that helps people understand the App Clipโ€™s value, or a photo of the location of its associated business or point of interest. + +**Avoid using text.** Text in the header image isnโ€™t localizable, can be difficult to read, and can make a card image less aesthetically pleasing. + +**Adhere to image requirements.** Use a 1800x1200 px PNG or JPEG image without transparency. + +**Use concise copy.** An App Clip card requires both a title and a subtitle. Clearly express the purpose of your App Clip within the available space so people can read and understand it at a glance. Create a title that has no more than 30 characters and a subtitle that has no more than 56 characters. + +**Pick a verb for the action button that best fits your App Clip.** Possible verbs are _View_ , _Play_ , or _Open_. Pick _View_ for media, or if your App Clip provides informational or educational content. Pick _Play_ for games. Choose _Open_ for all other App Clips. + +![A horizontal row of two App Clip cards. The left App Clip card is for a game and uses Play as the verb for the action button. The right App Clip card is for an app and uses Open as the verb for the action button.](https://docs-assets.developer.apple.com/published/d23129c13777717e7b27c9a1e2b2f8b0/app-clips-card%402x.png) + +## [App Clip Codes](https://developer.apple.com/design/human-interface-guidelines/app-clips#App-Clip-Codes) + +App Clip Codes are the best way for people to discover your App Clip. Their distinct design is immediately recognizable, and they offer a fast, secure way to launch your App Clip. + +![An App Clip Code that uses a badge design with the App Clip logo.](https://docs-assets.developer.apple.com/published/a5b0ac2b9f76d76391473c86a4104ef9/with-appclip-logo%402x.png)App Clip Code with the App Clip logo + +![An App Clip Code that uses a design without the App Clip logo.](https://docs-assets.developer.apple.com/published/2441534012373af30bf4e6ac94bbcc20/without-appclip-logo%402x.png)App Clip Code without the App Clip logo + +App Clip Codes always use the designs Apple provides and follow size, placement, and printing guidelines. Choose between the badge design that uses the App Clip logo (๏ฃฟ App Clip) or, when space is at a premium, a design without it. Create App Clip Codes that use a default color pair, or choose custom foreground and background colors. For developer guidance, see [Creating App Clip Codes](https://developer.apple.com/documentation/AppClip/creating-app-clip-codes). + +### [Interacting with App Clip Codes](https://developer.apple.com/design/human-interface-guidelines/app-clips#Interacting-with-App-Clip-Codes) + +App Clip Codes come in two variants: _scan-only_ or with an embedded NFC tag (_NFC-integrated_). + +![A scan-only App Clip Code with callouts for the center icon, visual code, and the App Clip logo.](https://docs-assets.developer.apple.com/published/3e01f3350c0634f35ba0cb54c46b4227/scan-only%402x.png) + +The scan-only variant uses a camera icon in its center to let people know to use the Camera app or the Code Scanner in Control Center to scan the App Clip Code. The NFC-integrated variant uses an iPhone icon at its center that guides people to hold their device close to the App Clip Code or to scan it using the NFC Tag Reader in Control Center. People can also scan an NFC-integrated App Clip Code with the Camera app or the Code Scanner in Control Center. For example: + + * A coffee shop could place an App Clip Code on their menu. A guest could hold their device close to the App Clip Code and instantly launch the shopโ€™s App Clip to order a drink. + + * A gas station could have an NFC-integrated App Clip Code attached to each pump. A traveler could hold their device close to it to launch the gas stationโ€™s App Clip and use it to pay for their refill. + + * A video game creator could hand out marketing material at an industry event that includes an App Clip Code. An event attendee could scan the code to launch an App Clip thatโ€™s a playable demo of their latest video game. + + + + +![An illustration that shows how a person uses an App Clip Code on a table at a coffee shop. The left side of the illustration shows two people sitting at a table. A placard in the middle of the table contains an App Clip Code. The person on the left is using their camera to scan the App Clip Code. The right side of the illustration shows a zoomed-in version of the person's phone screen and the placard on the table.](https://docs-assets.developer.apple.com/published/331753285f06bb59ab3bae929756a505/interacting-coffee-shop-example%402x.png) + +### [Displaying App Clip Codes](https://developer.apple.com/design/human-interface-guidelines/app-clips#Displaying-App-Clip-Codes) + +When you start designing your App Clip Codes, choose the variant that works best for the way people use your App Clip. If people can physically access the App Clip Code, use the NFC-integrated variant. For example: + + * On a tabletop at a restaurant + + * Near a register at a retail store + + * In a storefront window + + * On signage + + * On a gift card or coupon + + + + +If you need to place your App Clip Code in an area thatโ€™s physically inaccessible or you need to display it digitally, use a scan-only App Clip Code. For example: + + * On posters or printed advertising + + * On signage behind a counter or unreachable in a storefront + + * On digital materials such as digital displays, in emails, or on images you post to social media + + + + +No matter which of the two variants you use, itโ€™s important you carefully consider where you place your App Clip Code to ensure a reliable scanning experience. + +**Include the App Clip logo when space allows.** The logo helps make it clear that the code launches an App Clip; however, if you canโ€™t meet the clear space requirements, use the App Clip Code design without the App Clip logo. Also, use the design without the App Clip logo if you place the App Clip Code on disposable paper or plastic items, or on items associated with gambling or drinking. For example, use the App Clip Code without the App Clip logo on playing cards, poker chips, or bar coasters. The App Clip logo is always part of the badge design where it appears below the App Clip Code; never use the App Clip logo on its own. + +**Place your App Clip Code on a flat or cylindrical surface only.** If you place your App Clip Code on a cylindrical surface โ€” for example, on a scooterโ€™s handlebar โ€” make sure the width of the App Clip Code doesnโ€™t exceed one-sixth of the cylinderโ€™s circumference. + +![An illustration that shows a circle that represents a cylindrical surface. Lines divide the circle into six segments of equal size. One segment represents an App Clip Code and shows how the code doesnโ€™t cover more than one-sixth, or 60 degrees, of the surfaceโ€™s circumference.](https://docs-assets.developer.apple.com/published/5999e7c0f514a877839b74e365c8a7e2/app-clips-slice%402x.png) + +**Help your App Clip Code remain as flat as possible so itโ€™s easy for people to scan.** To provide the best scanning experience, avoid displaying App Clip Codes on deformable materials that readily fold or crumple, such as paper, plastic, or fabric. If you need to make your App Clip Code available on a bag, flexible box, or other deformable object, display it on something rigid โ€” like a card โ€” that you attach to the object. If you create an App Clip Code sticker, make sure it adheres well to flat surfaces. + +**Place your App Clip Code in a location that helps ensure reliable scanning.** For example, place a scan-only App Clip Code in a location that offers enough light to ensure reliable scanning, and donโ€™t require people to scan from a wide angle. + +**Make sure the App Clip Code is unobstructed.** Donโ€™t overlay the App Clip Code with text, logos, or images. Never animate the App Clip Code or dim it. + +**Display the App Clip Code in an upright position.** Donโ€™t rotate the generated App Clip Code or display the center glyph at an angle. + +![A correctly placed App Clip Code in the upright position.](https://docs-assets.developer.apple.com/published/a6eaaa833a98678b2b93f910f149bb6e/upright-display-right%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +![An incorrectly placed App Clip Code that's rotated 90 degrees to the left.](https://docs-assets.developer.apple.com/published/5897608f22eb0d296f1a8189c1f2e38b/upright-display-wrong-1%402x.png) + +![An X in a circle to indicate an invalid App Clip Code.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An incorrectly placed App Clip Code that's rotated 135 degrees to the right.](https://docs-assets.developer.apple.com/published/d4b3a7a3d5685fbec4194deb55bbb0c9/upright-display-wrong-2%402x.png) + +![An X in a circle to indicate an invalid App Clip Code.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +**Donโ€™t create App Clip Codes that are too small.** App Clip Codes must adhere to the following specifications. + +Type| Minimum size +---|--- +Printed communications| Minimum diameter of 3/4 inch (1.9 cm). +Digital communications| Minimum size of 256ร—256 px. Use a PNG or SVG file. +NFC-integrated App Clip Code| The embedded NFC tag needs to be at least 35 mm in diameter or of equivalent size. For example, if your embedded NFC tag is 35 mm in diameter, your printed App Clip Code needs to be at least 1.37 inches (3.48 cm) in diameter. + +![An App Clip Code that uses the badge design and has a minimum diameter of 3/4 inch \(1.9 cm\).](https://docs-assets.developer.apple.com/published/604637e18752ed4948becd174afbc361/sizing-minimum-rectangle%402x.png) + +![An App Clip Code that uses the design without the App Clip logo and has a minimum diameter of 3/4 inch \(1.9 cm\).](https://docs-assets.developer.apple.com/published/0aa05fa8f03c37459ea683c058a35a2f/sizing-minimum-circular%402x.png) + +When determining the dimensions of your App Clip Codes, consider a distance to code size ratio of no more than 20:1. If possible, use a ratio of 10:1 to ensure reliable scanning. For example, an App Clip that people scan from 40 inches (101 cm) away needs to be at least 4 inches (10.16 cm) in diameter. + +If you display an App Clip Code near a QR Code or other scannable item, choose a size for the App Clip Code thatโ€™s at least the other codeโ€™s or itemโ€™s size. + +![An illustration of an App Clip Code next to a QR code. Red guides denote that both are the same size.](https://docs-assets.developer.apple.com/published/b693a5616cb7a4b58895496c6b834b2d/app-clip-with-qr-code%402x.png) + +**Provide enough space between an App Clip Code and adjacent App Clip Codes, graphics, or materials.** The minimum clear space around an App Clip Code is equal to the space between the center glyph and the circular code. If you place your App Clip Code next to another App Clip Code or other machine-readable code, leave enough clear space to allow for reliable scanning of each code. + +![An illustration that shows an App Clip Code with the badge design to the left of an App Clip Code without the App Clip logo. A red guide surrounds each App Clip Code, illustrating the clear space requirements.](https://docs-assets.developer.apple.com/published/60ce57295e138b8c399d9c229238f40d/app-clip-spacing%402x.png) + +### [Using clear messaging](https://developer.apple.com/design/human-interface-guidelines/app-clips#Using-clear-messaging) + +Add clear messaging that informs people how they can use the App Clip Code to launch your App Clip, especially if you use the design without the App Clip logo. For example, add a call to action next to an App Clip Code you display in an email or on a poster. Use the suggested call-to-action messaging or your own copy. Always use a simple, clear call to action. + +![An illustration that shows two people sitting at a table at a coffee shop. A placard in the middle of the table contains an App Clip Code. The right side of the illustration shows a zoomed-in version of the placard, which contains an App Clip Code and surrounding text that reads 'Place your order. Hold your iPhone near the menu to place your food order.'.](https://docs-assets.developer.apple.com/published/f168bf0bd2558212caf8059de39205e5/clear-messaging%402x.png) + +For a scan-only App Clip Code, you can use the following call to action: + + * Scan to [_describe what people can do with your App Clip_]. + + * Scan using the camera on your iPhone or iPad to [describe what people can do with your App Clip]. + + + + +For an NFC-integrated App Clip Code, you can use the following call to action: + + * Scan to [_describe what people can do with your App Clip_]. + + * Hold your iPhone near the [_object name_] to launch an App Clip that [_describe what a person can do with your App Clip_]. + + + + +For more information, see [NFC](https://developer.apple.com/design/human-interface-guidelines/nfc). + +**Adhere to[Guidelines for Using Apple Trademarks](https://www.apple.com/legal/intellectual-property/guidelinesfor3rdparties.html) when referring to your App Clip and App Clip Codes.** For example, Apple trademarks canโ€™t appear in your app name or images, always use title case when using the terms App Clips or App Clip Code, and so on. For additional information, see [Legal requirements](https://developer.apple.com/design/human-interface-guidelines/app-clips#Legal-requirements). + +### [Customizing your App Clip Code](https://developer.apple.com/design/human-interface-guidelines/app-clips#Customizing-your-App-Clip-Code) + +Use [App Store Connect](https://appstoreconnect.apple.com) or the [App Clip Code Generator](https://developer.apple.com/app-clips/resources/) command-line tool to create App Clip Codes, and follow best practices to ensure a reliable scanning experience. + +![Four App Clip badges, each using different colors. The two on the left use the badge design, and the two on the right use the design without the App Clip logo.](https://docs-assets.developer.apple.com/published/02d96548534ce28b92754477aadbf9bb/app-clips-customizing%402x.png) + +**Always use the generated App Clip Code.** Donโ€™t create your own App Clip Code design or modify a generated App Clip Code in any way. Donโ€™t apply filters, augment its colors, or add glows, shadows, gradients, or reflections. They negatively impact peopleโ€™s scanning experience. When scaling a generated App Clip Code, donโ€™t change the generated codeโ€™s aspect ratio, and be sure to scale all attributes of the App Clip Code โ€” for example the stroke widths. + +![An illustration of an invalid App Clip Code with a changed aspect ratio.](https://docs-assets.developer.apple.com/published/2243d4c0011526cae9bd3d69e62907ae/customizing-wrong-1%402x.png) + +![An X in a circle to indicate an invalid App Clip Code.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration of an invalid App Clip Code with a color gradient instead of a solid background color.](https://docs-assets.developer.apple.com/published/6578f646c8aae8d6ac64ef965b783175/customizing-wrong-2%402x.png) + +![An X in a circle to indicate an invalid App Clip Code.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration of an invalid App Clip Code with a drop shadow effect.](https://docs-assets.developer.apple.com/published/658d3fafbc3dfdd11dc26f1b5a7aee48/customizing-wrong-3%402x.png) + +![An X in a circle to indicate an invalid App Clip Code.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +**Choose colors with enough contrast that ensure accurate scanning.** Each App Clip Code uses three colors: a foreground color, a background color, and a third color thatโ€™s generated for you based on the foreground and background colors. Both [App Store Connect](https://appstoreconnect.apple.com) and the [App Clip Code Generator](https://developer.apple.com/app-clips/resources/) command-line tool offer a selection of default color pairs. Alternatively, you can choose custom foreground and background colors. Note that you canโ€™t choose custom colors that will lead to a suboptimal scanning experience. If your color selection doesnโ€™t work well, neither App Store Connect nor the command-line tool will generate an App Clip Code. To help you choose a color combination that works well, both tools contain functionality to suggest a different foreground color based on your custom background color. For more information, see [Creating App Clip Codes with the App Clip Code Generator](https://developer.apple.com/documentation/AppClip/creating-app-clip-codes-with-the-app-clip-code-generator) and [Creating App Clip Codes with App Store Connect](https://developer.apple.com/documentation/AppClip/creating-app-clip-codes-with-app-store-connect). + +![An illustration of an App Clip Code that uses the badge design and has callouts for the background, foreground, and generated colors.](https://docs-assets.developer.apple.com/published/aae3a773b90a26b4870f4db43a0c3f94/app-clip-colors%402x.png) + +## [Printing guidelines](https://developer.apple.com/design/human-interface-guidelines/app-clips#Printing-guidelines) + +App Clip Codes offer the best experience to launch App Clips. As a result, itโ€™s important to manufacture and display App Clip Codes that offer a reliable scanning experience for a long time. You can print App Clip Codes yourself, or work with a professional printing service โ€” for example, [RR Donnelley](https://touchless.acc.rrd.com/). + +Always test printed App Clip Codes before you distribute them to be sure theyโ€™re scannable from a variety of angles. + +**Use high-quality, non-textured print materials.** Print App Clip Codes on matte finishes. Avoid shine, gloss, reflective or holographic overlays, as well as thin laminate finishes or materials. In case you need to laminate print material with an App Clip Code on it, use a matte laminate to avoid shine and reflections. If you place your App Clip Code outdoors, use UV-resistant materials or coatings to prevent fading from exposure to sunlight, rain, and other weather conditions. If you work with a professional printing service, use flexographic printing for best results. If you print the App Clip Codes yourself using a desktop printer, use an inkjet printer for best results. + +**Use high-resolution images and printer settings.** When rasterizing the SVG file, set the image resolution to at least 600 ppi, and print your App Clip Codes with a minimum resolution of 300 dpi. Consider leveling and calibrating your printer before printing to ensure a high print quality, and avoid poor color channel alignment, inaccurate gamma values, artifacts, or printing elliptical or otherwise distorted App Clip Codes. When using receipt printers, print App Clip Codes as close to the paperโ€™s maximum bounds as possible. + +**Use correct color settings when you convert the generated SVG file to a CMYK image.** Both the [App Clip Code Generator](https://developer.apple.com/app-clips/resources/) command-line tool and [App Store Connect](https://appstoreconnect.apple.com) generate App Clip Codes as SVG files in the sRGB color space. To print colors that match the SVG file, convert the sRGB image to a CMYK image. Use a relative calorimetric (media-relative) intent when performing the conversion. Use โ€œGeneric CMYK ICC profileโ€ on CMYK printers or โ€œGracol 2013 ICC profileโ€ on CMYKOV printers and allow for a color tolerance CIELab Delta E of 2.5. + +**If youโ€™re using a printer that only prints in grayscale, only generate grayscale App Clip Codes.** Codes generated in color and then printed in grayscale may work less reliably. + +**For NFC-integrated App Clip Codes, choose Type 5 NFC tags.** The embedded NFC tag needs to be at least 35 mm in diameter or of equivalent size. + +**If you create large batches of App Clip Codes, thoroughly test your printing workflow, and verify printed App Clip Codes.** For example, conduct small, inexpensive print runs using a subset of codes. Print your App Clip Codes on print templates with additional padded regions that allow you to display the encoded invocation URL and the SVG filename alongside each code for validation at the time of print. + +If you create many App Clip Codes with the [App Clip Code Generator](https://developer.apple.com/app-clips/resources/) tool or [App Store Connect](https://appstoreconnect.apple.com), youโ€™ll likely work with a professional printing service. If this is the case, you need to handle a lot of SVG files. Because you have no way of knowing which App Clip Code encodes which URL by looking at an App Clip Code, you need to use a file that contains information about which SVG file maps to which invocation URL. Under any circumstance, careful file management, versioning, and change tracking are key to avoiding faulty print runs. For more information, see [Preparing multiple App Clip Codes for production](https://developer.apple.com/documentation/AppClip/preparing-multiple-app-clip-codes-for-production). + +### [Verifying your printerโ€™s calibration](https://developer.apple.com/design/human-interface-guidelines/app-clips#Verifying-your-printers-calibration) + +A reliable scanning experience depends on the quality of your printed App Clip Codes. To ensure printing App Clip Codes results in a reliable scanning experience and to avoid using a printer that canโ€™t print high-quality App Clip Codes, Apple offers [printer calibration test sheets](https://developer.apple.com/app-clips/resources/printer-calibration-test-sheets.zip) you can use to verify your printerโ€™s settings and print quality. + +**Verify print quality of your chosen color pair with the printer calibration test sheet that shows text boxes for each default color pair.** Follow the instructions on the sheet to print it at the right scale and to verify that your printer can create high-quality App Clip Codes. + +**Verify your printerโ€™s grayscale settings by printing the printer calibration test sheet that shows two grayscale bars.** If any of the specific gray colors are light or entirely missing, the printer may need calibration or may not be suitable for printing an App Clip Code that allows for reliable scanning. + +## [Legal requirements](https://developer.apple.com/design/human-interface-guidelines/app-clips#Legal-requirements) + +Only the Apple-provided App Clip Codes created in App Store Connect or with the App Clip Code Generator command-line tool and that follow these guidelines are approved for use. + +App Clip Codes are approved for use to indicate availability of an App Clip. Apple may update the App Clip Code design from time to time at Appleโ€™s discretion. + +In the event your App Clip is no longer active, also stop displaying the App Clip Code associated with that inactive App Clip. + +You may not use the App Clip Code (including, without limitation, the Apple Logo, the App Clip mark, and the App Clip Code designs) as part of your own company name or as part of your product name. You may not seek copyright or trademark registration for the App Clip Codes or any elements contained therein. + +The App Clip Codes described in these guidelines must not be used in any manner that is likely to reduce, diminish, or damage the goodwill, value, or reputation associated with Apple or App Clips; or that infringes or violates the trademarks or other proprietary rights of any third party; or that is likely to cause confusion as to the source of products or services. + +Apple retains all rights to its trademarks, copyrights, or other intellectual property rights contained in the materials provided for use hereunder, including, without limitation, the App Clip Codes and any elements contained therein. + +Donโ€™t add a symbol to App Clip Codes created in App Store Connect or with the App Clip Code Generator command-line tool. + +Donโ€™t translate any Apple trademark. Apple trademarks must remain in English even when they appear within text in a language other than English. With Appleโ€™s approval, a translation of the legal notice and credit lines (but not the trademarks) can be used in materials distributed outside the U.S. + +For more information about using Apple trademarks, see [Guidelines for Using Apple Trademarks](https://www.apple.com/legal/intellectual-property/guidelinesfor3rdparties.html). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/app-clips#Platform-considerations) + + _No additional considerations for iOS or iPadOS. Not supported in macOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/app-clips#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/app-clips#Related) + +[Apple Pay](https://developer.apple.com/design/human-interface-guidelines/apple-pay) + +[Sign in with Apple](https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple) + +[Guidelines for Using Apple Trademarks and Copyrights](https://www.apple.com/legal/intellectual-property/guidelinesfor3rdparties.html) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/app-clips#Developer-documentation) + +[App Clips](https://developer.apple.com/documentation/AppClip) + +[App Store Connect](https://appstoreconnect.apple.com/) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/app-clips#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/95357E8D-01E6-476E-9516-8AF54EC9794A/4878_wide_250x141_1x.jpg) What's new in App Clips ](https://developer.apple.com/videos/play/wwdc2021/10012) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/7120E473-4A84-447D-8B55-0F1614324E59/4879_wide_250x141_1x.jpg) Build light and fast App Clips ](https://developer.apple.com/videos/play/wwdc2021/10013) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/app-clips#Change-log) + +Date| Changes +---|--- +June 9, 2025| Updated guidance to include demo App Clips. +May 2, 2023| Consolidated guidance into one page. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/app-shortcuts.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/app-shortcuts.md new file mode 100644 index 00000000..6ff2c0ce --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/app-shortcuts.md @@ -0,0 +1,114 @@ +--- +title: "App Shortcuts | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/app-shortcuts + +# App Shortcuts + +An App Shortcut gives people access to your appโ€™s key functions or content throughout the system. + +![A stylized representation of the Notes app appearing as the result in the Top Hit area of Spotlight, along with App Shortcuts for creating a new note and opening two other recent notes. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/183dda77d62e2ab7dae5994f2e754a26/components-app-shortcuts-intro%402x.png) + +People can initiate App Shortcuts using features like [Siri](https://developer.apple.com/design/human-interface-guidelines/siri), Spotlight, and the Shortcuts app; using hardware features like the [Action button](https://developer.apple.com/design/human-interface-guidelines/action-button) on iPhone or Apple Watch; or by [squeezing](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Squeeze) Apple Pencil. + +Because App Shortcuts are part of your app, they are available immediately when installation finishes. For example, a journaling app could offer an App Shortcut for making a new journal entry thatโ€™s available before a person opens the app for the first time. Once someone starts using your app, its App Shortcuts can reflect their choices, like those from FaceTime for calling recent contacts. + +![A partial screenshot of the Shortcuts app on iPhone showing App Shortcuts for FaceTime listed in a grid view. The App Shortcuts are in a group labeled Call Recents, and are each titled with the name of a recent FaceTime contact.](https://docs-assets.developer.apple.com/published/c5f6fb621f6aadfac85015446ec31542/app-shortcuts-personalized-choices%402x.png) + +App Shortcuts use [App Intents](https://developer.apple.com/documentation/AppIntents) to define actions within your app to make available to the system. Each App Shortcut includes one or more actions that represent a set of steps people might want to perform to accomplish a task. For example, a home security app might combine the two common actions of turning off the lights and locking exterior doors when a person goes to sleep at night into a single App Shortcut. Each app can include up to 10 App Shortcuts. + +Note + +When you use App Intents to make your appโ€™s actions available to the system, in addition to the App Shortcuts that your app provides, people can also make their own custom shortcuts by combining actions in the Shortcuts app. Custom shortcuts give people flexibility to configure the behavior of actions, and enable workflows that perform tasks across multiple apps. For additional guidance, see the [Shortcuts User Guide](https://support.apple.com/guide/shortcuts/welcome/ios). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/app-shortcuts#Best-practices) + +**Offer App Shortcuts for your appโ€™s most common and important tasks.** Straightforward tasks that people can complete without leaving their current context work best, but you can also open your app if it helps people complete multistep tasks more easily. + +**Add flexibility by letting people choose from a set of options.** An App Shortcut can include a single optional value, or parameter, if it makes sense. For example, a meditation app could offer an App Shortcut that lets someone begin a specific type of meditation: โ€œStart [morning, daily, sleep] meditation.โ€ Include predictable and familiar values as options, because people wonโ€™t have the list in front of them for reference. For developer guidance, see [Adding parameters to an app intent](https://developer.apple.com/documentation/AppIntents/Adding-parameters-to-an-app-intent). + +![A diagram of the activation phrase of a shortcut for ordering a drink from a coffee app. The activation phrase contains an optional value for the name of the drink, which is underlined and called out as the shortcut's parameter.](https://docs-assets.developer.apple.com/published/30601265724ed100cf9fbbe64c8b9c9c/app-intents-parameter-diagram%402x.png) + +**Ask for clarification in response to a request thatโ€™s missing optional information.** For example, someone might say โ€œStart meditationโ€ without specifying the type (morning, daily, or sleep); you could follow up by suggesting the one they used most recently, or one based on the current time of day. If one option is most likely, consider presenting it as the default, and provide a short list of alternatives to choose from if a person doesnโ€™t want the default choice. + +**Keep voice interactions simple.** If your phrase feels too complicated when you say it aloud, itโ€™s probably too difficult to remember or say correctly. For example, โ€œStart [sleep] meditation with nature soundsโ€ appears to have two possible parameters: the meditation type, and the accompanying sound. If additional information is absolutely required, ask for it in a subsequent step. For additional guidance on writing dialogue text for voice interactions, see [Siri](https://developer.apple.com/design/human-interface-guidelines/siri). + +**Make App Shortcuts discoverable in your app.** People are most likely to remember and use App Shortcuts for tasks they do often, once they know the shortcut is available. Consider showing occasional tips in your app when people perform common actions to let them know an App Shortcut exists. For developer guidance, see [`SiriTipUIView`](https://developer.apple.com/documentation/AppIntents/SiriTipUIView). + +### [Responding to App Shortcuts](https://developer.apple.com/design/human-interface-guidelines/app-shortcuts#Responding-to-App-Shortcuts) + +As a person engages with an App Shortcut, your app can respond in a variety of ways, including with dialogue that Siri speaks aloud and custom visuals like snippets and Live Activities. + + * Snippets are great for custom views that display static information or dialog options, like showing the weather at a personโ€™s location or confirming an order. For developer guidance, see [`ShowsSnippetView`](https://developer.apple.com/documentation/AppIntents/ShowsSnippetView). + + * [Live Activities](https://developer.apple.com/design/human-interface-guidelines/live-activities) offer continuous access to information thatโ€™s likely to remain relevant and change over a period of time, and are great for timers and countdowns that appear until an event is complete. For developer guidance, see [`LiveActivityIntent`](https://developer.apple.com/documentation/AppIntents/LiveActivityIntent). + + + + +![A screenshot of the iPhone Home Screen with a custom snippet occupying the top half of the screen. The snippet includes buttons to confirm or cancel a delivery order from a coffee shop, along with the items in the order and the total price.](https://docs-assets.developer.apple.com/published/05c70d4c5b6a1d65a1ea0a1662b5aa83/app-shortcuts-siri-dialogue%402x.png) + +![A screenshot of the iPhone Home Screen with a Live Activity occupying the top quarter of the screen. The Live Activity shows the estimated time for the arrival of a delivery of an order from a coffee shop, along with the number of items in the order and a button to contact the delivery driver.](https://docs-assets.developer.apple.com/published/62d4ff80b64dba56a083468c467fdb64/app-shortcuts-live-activity%402x.png) + +**Provide enough detail for interaction on audio-only devices.** People can receive responses on audio-only devices such as AirPods and HomePod too, and may not always be able to see content onscreen. Include all critical information in the full dialogue text of your App Shortcuts. For developer guidance, see [`init(full:supporting:systemImageName:)`](https://developer.apple.com/documentation/AppIntents/IntentDialog/init\(full:supporting:systemImageName:\)). + +## [Editorial guidelines](https://developer.apple.com/design/human-interface-guidelines/app-shortcuts#Editorial-guidelines) + +**Provide brief, memorable activation phrases and natural variants.** Because an App Shortcut phrase (or a variant you define) is what people say to run an App Shortcut with Siri, itโ€™s important to keep it brief to make it easier to remember. You have to include your app name, but you can be creative with it. For example, Keynote accepts both โ€œCreate a Keynoteโ€ and โ€œAdd a new presentation in Keynoteโ€ as App Shortcut phrases for creating a new document. For developer guidance, see [`AppShortcutPhrase`](https://developer.apple.com/documentation/AppIntents/AppShortcutPhrase). + +**When referring to App Shortcuts or the Shortcuts app, always use title case and make sure that _Shortcuts_ is plural.** For example, _MyApp integrates with Shortcuts to provide a quick way to get things done with just a tap or by asking Siri, and offers App Shortcuts you can place on the Action button._ + +**When referring to individual shortcuts (not App Shortcuts or the Shortcuts app), use lowercase.** For example, _Run a shortcut by asking Siri or tapping a suggestion on the Lock Screen._ + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/app-shortcuts#Platform-considerations) + + _No additional considerations for visionOS or watchOS. Not supported in tvOS._ + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/app-shortcuts#iOS-iPadOS) + +App Shortcuts can appear in the Top Hit area of Spotlight when people search for your app, or in the Shortcuts area below. Each App Shortcut includes a symbol from [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) that you choose to represent its functionality, or a preview image of an item that the shortcut links to directly. + +![A partial screenshot showing search results in Spotlight on iPhone, including the Top Hit area at the top of the screen with the Suggestions area beneath it. The Notes app appears as the Top Hit, with App Shortcuts appearing in a row to the right of the app icon: one titled New Note with a symbol of a pencil diagonally over a square, one titled Quick Note with a symbol of a scribbled line on a canvas, and one with a thumbnail of an embedded image for a recent note titled Nature. The Suggestions area includes a link to a web search for 'not,' and suggested autocomplete terms 'noteworthy' and 'notes'.](https://docs-assets.developer.apple.com/published/11e4814bf124943b889fc4f56a025431/app-shortcuts-spotlight-search-top-hit%402x.png) + +**Order shortcuts based on importance.** The order you choose determines how App Shortcuts initially appear in both Spotlight and the Shortcuts app, so itโ€™s helpful to include the most generally useful ones first. Once people start using your App Shortcuts, the system updates to prioritize the ones they use most frequently. + +**Offer an App Shortcut that starts a Live Activity.** Live Activities allow people to track an event or the progress of a task in glanceable locations across their devices. For example, a cooking app could offer a Live Activity to show the time left until a dish is ready to take out of the oven. To make it easy for people to start a cooking timer, the app offers an App Shortcut that people can place on the Action button. For more information about Live Activities, see [Live Activities](https://developer.apple.com/design/human-interface-guidelines/live-activities). + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/app-shortcuts#macOS) + +App Shortcuts arenโ€™t supported in macOS. However, actions you create for your app using App Intents are supported, and people can build custom shortcuts using them with the Shortcuts app on Mac. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/app-shortcuts#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/app-shortcuts#Related) + +[Siri](https://developer.apple.com/design/human-interface-guidelines/siri) + +[Siri Style Guide](https://developer.apple.com/siri/style-guide/) + +[Shortcuts User Guide](https://support.apple.com/guide/shortcuts/welcome/ios) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/app-shortcuts#Developer-documentation) + +[App Intents](https://developer.apple.com/documentation/AppIntents) + +[SiriKit](https://developer.apple.com/documentation/SiriKit) + +[Making actions and content discoverable and widely available](https://developer.apple.com/documentation/AppIntents/Making-actions-and-content-discoverable-and-widely-available) โ€” App Intents + +[Integrating custom data types into your intents](https://developer.apple.com/documentation/AppIntents/Integrating-custom-types-into-your-intents) โ€” App Intents + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/app-shortcuts#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/4D88FD13-E491-4499-AA3F-8A84CF4BA607/9999_wide_250x141_1x.jpg) Design interactive snippets ](https://developer.apple.com/videos/play/wwdc2025/281) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/05A21C8D-2A52-47BF-85A6-EB13E720EC85/9971_wide_250x141_1x.jpg) Get to know App Intents ](https://developer.apple.com/videos/play/wwdc2025/244) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/CD2BF2ED-403B-4831-9B5C-8A774D0EB7C7/9344_wide_250x141_1x.jpg) Bring your appโ€™s core features to users with App Intents ](https://developer.apple.com/videos/play/wwdc2024/10210) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/app-shortcuts#Change-log) + +Date| Changes +---|--- +January 17, 2025| Updated and streamlined guidance. +June 5, 2023| New page. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/complications.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/complications.md new file mode 100644 index 00000000..2e833d51 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/complications.md @@ -0,0 +1,425 @@ +--- +title: "Complications | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/complications + +# Complications + +A complication displays timely, relevant information on the watch face, where people can view it each time they raise their wrist. + +![A stylized representation of an Apple Watch face that includes the time and a set of differently sized complications with labels. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/0da5c74b1de3ab3c76a9f7ff332dec5d/components-complications-intro%402x.png) + +People often prefer apps that provide multiple, powerful complications, because it gives them quick ways to view the data they care about, even when they donโ€™t open the app. + +Most watch faces can display at least one complication; some can display four or more. + +Starting in watchOS 9, the system organizes complications (also known as _accessories_) into several families โ€” like [circular](https://developer.apple.com/design/human-interface-guidelines/complications#Circular) and [inline](https://developer.apple.com/design/human-interface-guidelines/complications#Inline) โ€” and defines some recommended layouts you can use to display your complication data. A watch face can specify the family it supports in each complication slot. Complications that work in earlier versions of watchOS can use the [legacy templates](https://developer.apple.com/design/human-interface-guidelines/complications#Legacy-templates), which define nongraphic complication styles that donโ€™t take on a wearerโ€™s selected color. + +Developer note + +Prefer using [WidgetKit](https://developer.apple.com/documentation/WidgetKit) to develop complications for watchOS 9 and later. For guidance, see [Migrating ClockKit complications to WidgetKit](https://developer.apple.com/documentation/WidgetKit/Converting-A-ClockKit-App). To support earlier versions of watchOS, continue to implement the ClockKit complication data source protocol (see [`CLKComplicationDataSource`](https://developer.apple.com/documentation/ClockKit/CLKComplicationDataSource)). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/complications#Best-practices) + +**Identify essential, dynamic content that people want to view at a glance.** Although people can use a complication to quickly launch an app, the complication behavior they appreciate more is the display of relevant information that always feels up to date. A static complication that doesnโ€™t display meaningful data may be less likely to remain in a prominent position on the watch face. + +**Support all complication families when possible.** Supporting more families means that your complications are available on more watch faces. If you canโ€™t display useful information for a particular complication family, provide an image that represents your app โ€” like your app icon โ€” that still lets people launch your app from the watch face. + +**Consider creating multiple complications for each family.** Supporting multiple complications helps you take advantage of shareable watch faces and lets people configure a watch face thatโ€™s centered on an app they love. For example, an app that helps people train for triathlons could offer three circular complications โ€” one for each segment of the race โ€” each of which deep-links to the segment-specific area in the app. This app could also offer a shareable watch face thatโ€™s preconfigured to include its swimming, biking, and running complications and to use its custom images and colors. When people choose this watch face, they donโ€™t have to do any configuration before they can start using it. For guidance, see [Watch faces](https://developer.apple.com/design/human-interface-guidelines/watch-faces). + +**Define a different deep link for each complication you support.** It works well when each complication opens your app to the most relevant area. If all the complications you support open the same area in your app, they can seem less useful. + +**Keep privacy in mind.** With the Always-On Retina display, information on the watch face might be visible to people other than the wearer. Make sure you help people prevent potentially sensitive information from being visible to others. For guidance, see [Always On](https://developer.apple.com/design/human-interface-guidelines/always-on). + +**Carefully consider when to update data.** You provide a complicationโ€™s data in the form of a timeline where each entry has a value that specifies the time at which to display your data on the watch face. Different data sets might require different time values. For example, a meeting app might display information about an upcoming meeting an hour before the meeting starts, but a weather app might display forecast information at the time those conditions are expected to occur. You can update the timeline a limited number of times each day, and the system stores a limited number of timeline entries for each app, so you need to choose times that enhance the usefulness of your data. For developer guidance, see [Migrating ClockKit complications to WidgetKit](https://developer.apple.com/documentation/WidgetKit/Converting-A-ClockKit-App#Configure-your-timeline-provider). + +## [Visual design](https://developer.apple.com/design/human-interface-guidelines/complications#Visual-design) + +**Choose a ring or gauge style based on the data you need to display.** Many families support a ring or gauge layout that provides consistent ways to represent numerical values that can change over time. For example: + + * The closed style can convey a value thatโ€™s a percentage of a whole, such as for a battery gauge. + + * The open style works well when the minimum and maximum values are arbitrary โ€” or donโ€™t represent a percentage of the whole โ€” like for a speed indicator. + + * Similar to the open style, the segmented style also displays values within an app-defined range, and can convey rapid value changes, such as in the Noise complication. + + + + +**Make sure images look good in tinted mode.** In tinted mode, the system applies a solid color to a complicationโ€™s text, gauges, and images, and desaturates full-color images unless you provide tinted versions of them. For developer guidance, see [`WidgetRenderingMode`](https://developer.apple.com/documentation/WidgetKit/WidgetRenderingMode). (If youโ€™re using legacy templates, tinted mode applies only to graphic complications.) To help your complications perform well in tinted mode: + + * Avoid using color as the only way to communicate important information. You want people to get the same information in tinted mode as they do in nontinted mode. + + * When necessary, provide an alternative tinted-mode version of a full-color image. If your full-color image doesnโ€™t look good when itโ€™s desaturated, you can supply a different version of the image for the system to use in tinted mode. + + + + +**Recognize that people might prefer to use tinted mode for complications, instead of viewing them in full color.** When people choose tinted mode, the system automatically desaturates your complication, converting it to grayscale and tinting its images, gauges, and text using a single color thatโ€™s based on the wearerโ€™s selected color. + +**When creating complication content, generally use line widths of two points or greater.** Thinner lines can be difficult to see at a glance, especially when the wearer is in motion. Use line weights that suit the size and complexity of the image. + +**Provide a set of static placeholder images for each complication you support.** The system uses placeholder images when thereโ€™s no other content to display for your complicationโ€™s data. For example, when people first install your app, the system can display a static placeholder while it checks to see if your app can generate a localized placeholder to use instead. Placeholder images can also appear in the carousel from which people select complications. Note that complication image sizes vary per layout (and per legacy template) and the size of a placeholder image may not match the size of the actual image you supply for that complication. For developer guidance, see [`placeholder(in:)`](https://developer.apple.com/documentation/WidgetKit/TimelineProvider/placeholder\(in:\)). + +## [Circular](https://developer.apple.com/design/human-interface-guidelines/complications#Circular) + +Circular layouts can include text, gauges, and full-color images in circular areas on the Infograph and Infograph Modular watch faces. The circular family also defines extra-large layouts for displaying content on the X-Large watch face. + +![A white musical notes icon displayed within a red circle. The circleโ€™s outline is bright red for about ninety percent of the circumference and dull red for about ten percent, showing current progress.](https://docs-assets.developer.apple.com/published/894b82d3c9a300e9ee13ccb6b5db1b18/circular-closed-gauge-image%402x.png)Closed gauge image + +![The number one hundred in white text displayed within a green circle. The circleโ€™s outline appears to overlap the starting point on the circumference by about five percent, showing current progress.](https://docs-assets.developer.apple.com/published/4c708bff423e1259099caeb43d49671e/circular-closed-gauge-text%402x.png)Closed gauge text + +![The number one point zero in white text, surrounded by a partial circle that begins at about the 8:00 position and ends at about the 4:00 position. The partial circleโ€™s outline shades from green at the 8:00 position to violet the 4:00 position. A small green sun icon appears at about the 6:00 position.](https://docs-assets.developer.apple.com/published/654cf781ef7b3aae9d4849238bbc4518/circular-open-gauge-image%402x.png)Open gauge image + +![The number forty-two in white text, surrounded by a partial circle that begins at about the 8:00 position and ends at about the 4:00 position. The partial circleโ€™s outline shades from blue at the 8:00 position to violet the 4:00 position. The letters A, Q, I appear in green text at about the 6:00 position.](https://docs-assets.developer.apple.com/published/e03f6469e57138b5b81c415a14822592/circular-open-gauge-simple-text%402x.png)Open gauge text + +![The number seventy-two in white text, surrounded by a partial circle that begins at about the 8:00 position and ends at about the 4:00 position. The partial circleโ€™s outline shades from green at the 8:00 position to yellow the 4:00 position. Two numbers appear side by side at about the 6:00 position. Fifty-five appears in green text on the left and seventy-six appears in orange text on the right.](https://docs-assets.developer.apple.com/published/07a43e66647416847d4c126f2cc9a3a1/circular-open-gauge-range-text%402x.png)Open gauge range + +![An image of the breathe app icon.](https://docs-assets.developer.apple.com/published/f89731b0962fff02483c177a06a158e1/graphic-circular-image%402x.png)Image + +![A sunset icon displayed above the time seven twenty-four PM, centered within a circular area.](https://docs-assets.developer.apple.com/published/77444d60c23fb81c91b3cca54fdd0bc3/complication-graphic-circular-stack%402x.png)Stack image + +![Two lines of text centered within a circular area. The top line is the Apple stock symbol A A P L in white and the second line is the number 121.96 in green.](https://docs-assets.developer.apple.com/published/d91868e8b2928ef9ae237d15eb98fed0/complication-graphic-circular-stack-text%402x.png)Stack text + +You can also add text to accompany a regular-size circular image, using a design that curves the text along the bezel of some watch faces, like Infograph. The text can fill nearly 180 degrees of the bezel before truncating. + +![A line of white text that appears to follow the curve of the upper third of a circle. The text reads 8:00 AM yoga, flow studio. Centered below the text is the calendar date friday twenty-three displayed in a circular area.](https://docs-assets.developer.apple.com/published/1d10fccea806be8f043ccc0f39ea4caf/bezel-circular-text%402x.png)Closed gauge image + +As you design images for a regular-size circular complication, use the following values for guidance. + +Image| 40mm| 41mm| 44mm| 45mm/49mm +---|---|---|---|--- +Image| 42x42 pt (84x84 px @2x)| 44.5x44.5 pt (89x89 px @2x)| 47x47 pt (94x94 px @2x)| 50x50 pt (100x100 px @2x) +Closed gauge| 27x27 pt (54x54 px @2x)| 28.5x28.5 pt (57x57 px @2x)| 31x31 pt (62x62 px @2x)| 32x32 pt (64x64 px @2x) +Open gauge| 11x11 pt (22x22 px @2x)| 11.5x11.5 pt (23x23 px @2x)| 12x12 pt (24x24 px @2x)| 13x13 pt (26x26 px @2x) +Stack (not text)| 28x14 pt (56x28 px @2x)| 29.5x15 pt (59X30 px @2x)| 31x16 pt (62x32px @ 2x)| 33.5x16.5 pt (67x33 px @2x) + +Note + +The system applies a circular mask to each image. + +A SwiftUI view that implements a regular-size circular complication uses the following default text values: + + * Style: Rounded + + * Weight: Medium + + * Text size: 12 pt (40mm), 12.5 pt (41mm), 13 pt (44mm), 14.5 pt (45mm/49mm) + + + + +If you want to design an oversized treatment of important information that can appear on the X-Large watch face โ€” for example, the Contacts complication, which features a contact photo โ€” use the extra-large versions of the circular familyโ€™s layouts. The following layouts let you display full-color images, text, and gauges in a large circular region that fills most of the X-Large watch face. Some of the text fields can support multicolor text. + +![A white musical notes icon displayed within a red circle. The circleโ€™s outline is bright red for about sixty-six percent of the circumference and dull red for about ten percent, showing current progress.](https://docs-assets.developer.apple.com/published/6e62e84e78d7206a561aaadff4f7bf9d/complication-graphic-xl-circular-closed-gauge-image%402x.png)Closed gauge image + +![The number one eighty-five in blue text displayed within a blue circle. The circleโ€™s outline is bright blue for eighty-five percent of the circumference and dull blue for fifteen percent, showing current progress.](https://docs-assets.developer.apple.com/published/88e7205fd0f8faa2b99412ab707b02d4/complication-graphic-xl-circular-closed-gauge-text%402x.png)Closed gauge text + +![The number fifty in light-blue text, surrounded by a partial light-blue circle that includes a teardrop shape at the bottom.](https://docs-assets.developer.apple.com/published/0b73bd54f4052f97317b84828b8ff150/complication-graphic-xl-circular-open-gauge-image%402x.png)Open gauge image + +![The number twenty-nine in green text, surrounded by a partial white circle that includes the letters A, Q, I in green text at the bottom.](https://docs-assets.developer.apple.com/published/8a6e7a02ffe907d8c0dd63ffb66e199a/complication-graphic-xl-circular-open-gauge-simple-text%402x.png)Open gauge text + +![The number fifty-six in white text, surrounded by a partial circle that shades from green at the 8:00 position to red at the 4:00 position. Two numbers appear side by side at the bottom of the partial circle. Fifty-two appears in green text on the left and eighty-nine appears in red text on the right.](https://docs-assets.developer.apple.com/published/80ec1e90fe34971189a61fc5a3fe04ab/complication-graphic-xl-circular-open-gauge-range-text%402x.png)Open gauge range + +![An image of the Breathe app icon.](https://docs-assets.developer.apple.com/published/78a8e3548c6f994f9565cd2e4b4e103b/complication-graphic-xl-circular-image%402x.png)Image + +![A red sunset icon displayed above the time seven twenty-four PM, centered within a circular area.](https://docs-assets.developer.apple.com/published/77444d60c23fb81c91b3cca54fdd0bc3/complication-graphic-xl-circular-stack-image%402x.png)Stack image + +![Two lines of text centered within a circular area. The top line is the Apple stock symbol A A P L in white and the second line is the number 121.96 in green.](https://docs-assets.developer.apple.com/published/d91868e8b2928ef9ae237d15eb98fed0/complication-graphic-xl-circular-stack-text%402x.png)Stack text + +Use the following values for guidance as you create images for an extra-large circular complication. + +Image| 40mm| 41mm| 44mm| 45mm/49mm +---|---|---|---|--- +Image| 120x120 pt (240x240 px @2x)| 127x127 pt (254x254 px @2x)| 132x132 pt (264x264 px @2x)| 143x143 pt (286x286 px @2x) +Open gauge| 31x31 pt (62x62 px @2x)| 33x33 pt (66x66 px @2x)| 33x33 pt (66x66 px @2x)| 37x37 pt (74x74 px @2x) +Closed gauge| 77x77 pt (154x154 px @2x)| 81.5x81.5 (163x163 px @2x)| 87x87 pt (174x174 px @2x)| 91.5x91.5 (183x183 px @2x) +Stack| 80x40 pt (160x80 px @2x)| 85x42 (170x84 px @2x)| 87x44 pt (174x88 px @2x)| 95x48 pt (190x96 px @2x ) + +Note + +The system applies a circular mask to the circular, open-gauge, and closed-gauge images. + +Use the following values to create no-content placeholder images for your circular-family complications. + +Layout| 38mm| 40mm/42mm| 41mm| 44mm| 45mm/49mm +---|---|---|---|---|--- +Circular| โ€“| 42x42 pt (84x84 px @2x)| 44.5x44.5 pt (89x89 px @2x)| 47x47 pt (94x94 px @2x)| 50x50 pt (100x100 px @2x) +Bezel| โ€“| 42x42 pt (84x84 px @2x)| 44.5x44.5 pt (89x89 px @2x)| 47x47 pt (94x94 px @2x)| 50x50 pt (100x100 px @2x) +Extra Large| โ€“| 120x120 pt (240x240 px @2x)| 127x127 pt (254x254 px @2x)| 132x132 pt (264x264 px @2x)| 143x143 pt (286x286 px @2x) + +A SwiftUI view that implements an extra-large circular layout uses the following default text values: + + * Style: Rounded + + * Weight: Medium + + * Text size: 34.5 pt (40mm), 36.5 pt (41mm), 36.5 pt (44mm), 41 pt (45mm/49mm) + + + + +## [Corner](https://developer.apple.com/design/human-interface-guidelines/complications#Corner) + +Corner layouts let you display full-color images, text, and gauges in the corners of the watch face, like Infograph. Some of the templates also support multicolor text. + +![An icon showing a yellow sun partially obscured by a white cloud within a circular area.](https://docs-assets.developer.apple.com/published/d2505fe8bcd6129c02eee89133fae163/corner-circular-image%402x.png)Circular image + +![The value fourteen minutes and fifty-nine seconds displayed next to a thin solid bar. The text and the bar appear to follow the curve of the bottom-right quadrant of a circle. The timer app icon appears below the time value.](https://docs-assets.developer.apple.com/published/a21715e44b62556c939eb1031d4d7f55/corner-gauge-image%402x.png)Gauge image + +![The weather values fifty-five, shown in green, and seventy-six, shown in orange, displayed with a shaded solid bar between them. The bar shades from green to orange to match the values. The text and the bar appear to follow the curve of the top-right quadrant of a circle. The value seventy-two degrees appears in large white text above the temperature range.](https://docs-assets.developer.apple.com/published/9820a8d14b8494f9bc3992c504baf134/corner-gauge-text%402x.png)Gauge text + +![Two lines of text, both of which appear to follow the curve of the top-left quadrant of a circle. The top line contains the word cup in large white text. The bottom line contains the time ten oh nine AM followed by a plus sign and zero hours, all in orange text.](https://docs-assets.developer.apple.com/published/6a3616ce205f69ee7c3deebb63f24c82/corner-stack-text%402x.png)Stack text + +![A line displaying zero hours, zero minutes, and zero seconds in orange text. The line appears to follow the curve of the bottom-left quadrant of a circle. The stopwatch app icon appears below the line of text.](https://docs-assets.developer.apple.com/published/c829cc0f1c923486c897b4328e11559a/corner-text-image%402x.png)Text image + +As you design images for a corner complication, use the following values for guidance. + +Image| 40mm| 41mm| 44mm| 45mm/49mm +---|---|---|---|--- +Circular| 32x32 pt (64x64 px @2x)| 34x34 pt (68x68 px @2x)| 36x36 pt (72x72 px @2x)| 38x38 pt (76x76 px @2x ) +Gauge| 20x20 pt (40x40 px @2x)| 21x21 pt (42x42 px @2x)| 22x22 pt (44x44 px @2x)| 24x24 pt (48x48 px @2x) +Text| 20x20 pt (40x40 px @2x)| 21x21 pt (42x42 px @2x)| 22x22 pt (44x44 px @2x)| 24x24 pt (48x48 px @2x) + +Note + +The system applies a circular mask to each image. + +Use the following values to create no-content placeholder images for your corner-family complications. + +38mm| 40mm/42mm| 41mm| 44mm| 45mm/49mm +---|---|---|---|--- +โ€“| 20x20 pt (40x40 px @2x)| 21x21 pt (42x42 px @2x)| 22x22 pt (44x44 px @2x)| 24x24 pt (48x48 px @2x) + +A SwiftUI view that implements a corner layout uses the following default text values: + + * Style: Rounded + + * Weight: Semibold + + * Text size: 10 pt (40mm), 10.5 pt (41mm), 11 pt (44mm), 12 pt (45mm/49mm) + + + + +## [Inline](https://developer.apple.com/design/human-interface-guidelines/complications#Inline) + +Inline layouts include utilitarian small and large layouts. + +Utilitarian small layouts are intended to occupy a rectangular area in the corner of a watch face, such as the Chronograph and Simple watch faces. The content can include an image, interface icon, or a circular graph. + +![The letters L, O, N displayed above the time six oh nine.](https://docs-assets.developer.apple.com/published/269280772375293a9bba7c48384bbc81/complication-utility-small-flat%402x.png)Flat + +![Two tear drop icons, each centered within a partial ring.](https://docs-assets.developer.apple.com/published/3321ec0b2fcd3a1aa7d7e50b082a82e2/complication-utility-small-ring-image%402x.png)Ring image + +![Two partial rings, each displaying the number sixty-three centered within them.](https://docs-assets.developer.apple.com/published/2fc1e12d51df2d6c708385b7d33ae3ad/complication-utility-small-ring-text%402x.png)Ring text + +![An image of the moon.](https://docs-assets.developer.apple.com/published/2316eb7e29bc98cbe606a332543e41ac/complication-utility-small-square%402x.png)Square + +As you design images for a utilitarian small layout, use the following values for guidance. + +Content| 38mm| 40mm/42mm| 41mm| 44mm| 45mm/49mm +---|---|---|---|---|--- +Flat| 9-21x9 pt (18-42x18 px @2x)| 10-22x10 pt (20-44x20 px @2x)| 10.5-23.5x21 pt (21-47x21 @2x)| N/A| 12-26x12 pt (24-52x24 px @2x) +Ring| 14x14 pt (28x28 px @2x)| 14x14 pt (28x28 px @2x)| 15x15 pt (30x30 px @2x)| 16x16 pt (32x32 px @2x)| 16.5x16.5 pt (33x33 px @2x) +Square| 20x20 pt (40x40 px @2x)| 22x22 pt (44x44 px @2x)| 23.5x23.5 pt (47x47 px @2x)| 25x25 pt (50x50 px @2x)| 26x26 pt (52x52 px @2x) + +The utilitarian large layout is primarily text-based, but also supports an interface icon placed on the leading side of the text. This layout spans the bottom of a watch face, like the Utility or Motion watch faces. + +![The text eleven AM photo shoot displayed on one line in a large text size.](https://docs-assets.developer.apple.com/published/71cce6286d76ea16f2821b0cfdcb453e/complication-utility-large-flat%402x.png)Large flat + +As you design images for a utilitarian large layout, use the following values for guidance. + +Content| 38mm| 40mm/42mm| 41mm| 44mm| 45mm/49mm +---|---|---|---|---|--- +Flat| 9-21x9 pt (18-42x18 px @2x)| 10-22x10 pt (20-44x20 px @2x)| 10.5-23.5x10.5 pt (21-47x21 px @2x)| N/A| 12-26x12 pt (24-52x24 px @2x) + +## [Rectangular](https://developer.apple.com/design/human-interface-guidelines/complications#Rectangular) + +Rectangular layouts can display full-color images, text, a gauge, and an optional title in a large rectangular region. Some of the text fields can support multicolor text. + +The large rectangular region works well for showing details about a value or process that changes over time, because it provides room for information-rich charts, graphs, and diagrams. For example, the Heart Rate complication displays a graph of heart-rate values within a 24-hour period. The graph uses high-contrast white and red for the primary content and a lower-contrast gray for the graph lines and labels, making the data easy to understand at a glance. + +Starting with watchOS 10, if you have created a rectangular layout for your watchOS app, the system may display it in the Smart Stack. You can optimize this presentation in a few ways: + + * By supplying background color or content that communicates information or aids in recognition + + * By using [intents](https://developer.apple.com/documentation/appintents/app-intents) to specify relevancy, and help ensure that your widget is displayed in the Smart Stack at times that are most appropriate and useful to people + + * By creating a custom layout of your information that is optimized for the Smart Stack + + + + +For developer guidance, see [`WidgetFamily.accessoryRectangular`](https://developer.apple.com/documentation/WidgetKit/WidgetFamily/accessoryRectangular). See [Widgets](https://developer.apple.com/design/human-interface-guidelines/widgets) for additional guidance on designing widgets for the Smart Stack. + +![Three lines of left-aligned text. The first line uses blue text to display the words water reminders. The second line uses white text to display the words thirty-two ounces consumed. The third line uses gray text to display the words four day streak, woo hoo.](https://docs-assets.developer.apple.com/published/eacc51c96f809a72dc4e293e1ce12231/rectangular-standard-body%402x.png)Standard body + +![Two lines of text displayed above a bar that can fill with color to indicate progress. The first line uses blue text to display a tear drop icon followed by the words water reminder. The second line uses white text to display the words thirty-two ounces consumed. The bar uses the same blue color as used in the first line of text to fill the bar from the left to about seventy percent of the total length.](https://docs-assets.developer.apple.com/published/cf5c53f181d423b5e950c27b3a8056d6/rectangular-text-gauge%402x.png)Text gauge + +![A line of text displayed above a graph. The text displays in white the words sixty-eight B, P, M, followed by the words two minutes ago, in red text. The graph shows many heart rate values over time.](https://docs-assets.developer.apple.com/published/f1b05dfd5648f270edc50eae0cbc2834/rectangular-large-image%402x.png)Large image + +Use the following values for guidance as you create images for a rectangular layout. + +Content| 40mm| 41mm| 44mm| 45mm/49mm +---|---|---|---|--- +Large image with title *| 150x47 pt (300x94 px @2x)| 159x50 pt (318x100 px @2x)| 171x54 pt (342x108 px @2x)| 178.5x56 pt (357x112 px @2x) +Large image without title *| 162x69 pt (324x138 px @2x)| 171.5x73 pt (343x146 px @2x)| 184x78 pt (368x156 px @2x)| 193x82 pt (386x164 px @2x) +Standard body| 12x12 pt (24x24 px @2x)| 12.5x12.5 pt (25x25 px @2x)| 13.5x13.5 pt (27x27 px @2x)| 14.5x14.5 pt (29x29 px @2x) +Text gauge| 12x12 pt (24x24 px @2x)| 12.5x12.5 pt (25x25 px @2x)| 13.5x13.5 pt (27x27 px @2x)| 14.5x14.5 pt (29x29 px @2x) + +Note + +Both large-image layouts automatically include a four-point corner radius. + +A SwiftUI view that implements a rectangular layout uses the following default text values: + + * Style: Rounded + + * Weight: Medium + + * Text size: 16.5 pt (40mm), 17.5 pt (41mm), 18 pt (44mm), 19.5 pt (45mm/49mm) + + + + +## [Legacy templates](https://developer.apple.com/design/human-interface-guidelines/complications#Legacy-templates) + +### [Circular small](https://developer.apple.com/design/human-interface-guidelines/complications#Circular-small) + +Circular small templates display a small image or a few characters of text. They appear in the corner of the watch face (for example, in the Color watch face). + +![A tear drop icon centered within a partial ring.](https://docs-assets.developer.apple.com/published/099b811bae2a48a89ccd01a8a526dc78/complication-circular-small-ring-image%402x.png)Ring image + +![The number sixty-three centered within a partial ring.](https://docs-assets.developer.apple.com/published/1e800aa127d18b31519bf181383bf13f/complication-circular-small-ring-text%402x.png)Ring text + +![A stopwatch icon centered within a circular area.](https://docs-assets.developer.apple.com/published/ea77fbe94ab1c99437b6f05635904912/complication-circular-small-simple-image%402x.png)Simple image + +![The number sixty-eight and the degree symbol centered within a circular area.](https://docs-assets.developer.apple.com/published/a5f5c73d96c14ed8b45d4ed14cf5d3fe/complication-circular-small-simple-text%402x.png)Simple text + +![A sunset icon displayed above the time seven twenty-four PM, centered within a circular area.](https://docs-assets.developer.apple.com/published/bdbfa5a07a29ae3c91413454c0d45f5c/complication-circular-small-stack-image%402x.png)Stack image + +![The letters L, O, N displayed above the time six oh nine.](https://docs-assets.developer.apple.com/published/0bc5cc4c6505b400a2b3ed317c47293c/complication-circular-small-stack-text%402x.png)Stack text + +As you design images for a circular small complication, use the following values for guidance. + +Image| 38mm| 40mm/42mm| 41mm| 44mm| 45mm/49mm +---|---|---|---|---|--- +Ring| 20x20 pt (40x40 px @2x)| 22x22 pt (44x44 px @2x)| 23.5x23.5 pt (47x47 px @2x)| 24x24 pt (48x48 px @2x)| 26x26 pt (52x52 px @2x) +Simple| 16x16 pt (32x32 px @2x)| 18x18 pt (36x36 px @2x)| 19x19 pt (38x38 px @2x)| 20x20 pt (40x40 px @2x)| 21.5x21.5 pt (43x43 px @2x) +Stack| 16x7 pt (32x14 px @2x)| 17x8 pt (34x16 px @2x)| 18x8.5 pt (36x17 px @2x)| 19x9 pt (38x18 px @2x)| 19x9.5 pt (38x19 px @2x) +Placeholder| 16x16 pt (32x32 px @2x)| 18x18x pt (36x36 px @2x)| 19x19 pt (38x38 px @2x)| 20x20 pt (40x40 px @2x)| 21.5x21.5 pt (43x43 px @2x) + +Note + +In each stack measurement, the width value represents the maximum size. + +### [Modular small](https://developer.apple.com/design/human-interface-guidelines/complications#Modular-small) + +Modular small templates display two stacked rows consisting of an icon and content, a circular graph, or a single larger item (for example, the bottom row of complications on the Modular watch face). + +![Text and numbers arranged in a two-row column. The top row displays the letters C and P and the number fourteen. The bottom row displays the letters M and H and the number twenty-eight.](https://docs-assets.developer.apple.com/published/d4768440b750b0614aca0bab1754c1bd/complication-modular-small-columns-text%402x.png)Columns text + +![A tear drop icon centered within a partial ring.](https://docs-assets.developer.apple.com/published/f89c9c4226b921580000237320197983/complication-modular-small-ring-image%402x.png)Ring image + +![The number sixty-three centered within a partial ring.](https://docs-assets.developer.apple.com/published/e9e15b071c1e272ef99ee09a583d6214/complication-modular-small-ring-text%402x.png)Ring text + +![An image of the moon.](https://docs-assets.developer.apple.com/published/d5a7bd5314f4a682df263d9f32224665/complication-modular-small-simple-image%402x.png)Simple image + +![The number sixty-eight and the degree symbol.](https://docs-assets.developer.apple.com/published/ea5c2c9f8f7d19da47f656816d13ccc4/complication-modular-small-simple-text%402x.png)Simple text + +![A sunset icon displayed above the time seven twenty-four PM.](https://docs-assets.developer.apple.com/published/74d96206654ce136baea6153f8f5916e/complication-modular-small-stack-image%402x.png)Stack image + +![The letters L, O, N displayed above the time six oh nine.](https://docs-assets.developer.apple.com/published/82cbeeb9536e51537483b6eb9294955d/complication-modular-small-stack-text%402x.png)Stack text + +As you design icons and images for a modular small complication, use the following values for guidance. + +Image| 38mm| 40mm/42mm| 41mm| 44mm| 45mm/49mm +---|---|---|---|---|--- +Ring| 18x18 pt (36x36 px @2x)| 19x19 pt (38x38 px @2x)| 20x20 pt (40x40 px @2x)| 21x21 pt (42x42 px @2x)| 22.5x22.5 pt (45x45 px @2x) +Simple| 26x26 pt (52x52 px @2x)| 29x29 pt (58x58 px @2x)| 30.5x30.5 pt (61x61 px @2x)| 32x32 pt (64x64 px @2x)| 34.5x34.5 pt (69x69 px @2x) +Stack| 26x14 pt (52x28 px @2x)| 29x15 pt (58x30 px @2x)| 30.5x16 pt (61x32 px @2x)| 32x17 pt (64x34 px @2x)| 34.5x18 pt (69x36 px @2x) +Placeholder| 26x26 pt (52x52 px @2x)| 29x29 pt (58x58 px @2x)| 30.5x30.5 pt (61x61 px @2x)| 32x32 pt (64x64 px @2x)| 34.5x34.5 pt (69x69 px @2x) + +Note + +In each stack measurement, the width value represents the maximum size. + +### [Modular large](https://developer.apple.com/design/human-interface-guidelines/complications#Modular-large) + +Modular large templates offer a large canvas for displaying up to three rows of content (for example, in the center of the Modular watch face). + +![Activity-related information displayed in a three-row column. The top row displays a calorie count of 396 out of 660. The middle row displays a minute count of 13 out of 30. The bottom row displays an hour count of 3 out of 12.](https://docs-assets.developer.apple.com/published/9e6f3cc81365a53a8509935db81ab4f0/complication-modular-large-columns%402x.png)Columns + +![Weather-related information displayed in three left-aligned lines of text. The top row displays the location Cupertino California. The middle row displays sixty-eight degrees and cloudy. The bottom row displays a forecast high of seventy-two degrees and low of sixty-two degrees.](https://docs-assets.developer.apple.com/published/97d75fa71e7d4dcac61d89286cd9e415/complication-modular-large-standard-body%402x.png)Standard body + +![Sports-related information displayed in a two-column, two-row table with a title. The table title is Final Score. The first table row contains the number 14 followed by the text Central Prep. The second table row contains the number 28 followed by the text Mission High.](https://docs-assets.developer.apple.com/published/ab811ad4c90cc2c030da750ebd0be495/complication-modular-large-table%402x.png)Table + +![Calendar-related information displayed in two lines of fully justified text. The first line displays the word wednesday. The second line displays the abbreviation mar and the number nine in text that is about twice as tall as the text in the first line.](https://docs-assets.developer.apple.com/published/877462cb5be4b9764cf101897b0acc2a/complication-modular-large-tall-body%402x.png)Tall body + +As you design icons and images for a modular large complication, use the following values for guidance. + +Content| 38mm| 40mm/42mm| 41mm| 44mm| 45mm/49mm +---|---|---|---|---|--- +Columns| 11-32x11 pt (22-64x22 px @2x)| 12-37x12 pt (24-74x24 px @2x)| 12.5-39x12.5 pt (25-78x25 px @2x)| 14-42x14 pt (28-84x28 px @2x)| 14.5-44x14.5 pt (29-88x29 px @2x) +Standard body| 11-32x11 pt (22-64x22 px @2x)| 12-37x12 pt (24-74x24 px @2x)| 12.5-39x12.5 pt (25-78x25 px @2x)| 14-42x14 pt (28-84x28 px @2x)| 14.5-44x14.5 pt (29-88x29 px @2x) +Table| 11-32x11 pt (22-64x22 px @2x)| 12-37x12 pt (24-74x24 px @2x)| 12.5-39x12.5 pt (25-78x25 px @2x)| 14-42x14 pt (28-84x28 px @2x)| 14.5-44x14.5 pt (29-88x29 px @2x) + +### [Extra large](https://developer.apple.com/design/human-interface-guidelines/complications#Extra-large) + +Extra large templates display larger text and images (for example, on the X-Large watch faces). + +![A tear drop icon centered within a partial ring.](https://docs-assets.developer.apple.com/published/f89c9c4226b921580000237320197983/complication-extralarge-ring-image%402x.png)Ring image + +![The number sixty-three centered within a partial ring.](https://docs-assets.developer.apple.com/published/e9e15b071c1e272ef99ee09a583d6214/complication-extralarge-ring-text%402x.png)Ring text + +![An image of the moon.](https://docs-assets.developer.apple.com/published/d5a7bd5314f4a682df263d9f32224665/complication-extralarge-simple-image%402x.png)Simple image + +![The number sixty-eight and the degree symbol.](https://docs-assets.developer.apple.com/published/ea5c2c9f8f7d19da47f656816d13ccc4/complication-extralarge-simple-text%402x.png)Simple text + +![A sunset icon displayed above the time seven twenty-four PM.](https://docs-assets.developer.apple.com/published/74d96206654ce136baea6153f8f5916e/complication-extralarge-stack-image%402x.png)Stack image + +![The letters L, O, N displayed above the time six oh nine.](https://docs-assets.developer.apple.com/published/82cbeeb9536e51537483b6eb9294955d/complication-extralarge-stack-text%402x.png)Stack text + +As you design icons and images for an extra large complication, use the following values for guidance. + +Image| 38mm| 40mm/42mm| 41mm| 44mm| 45mm/49mm +---|---|---|---|---|--- +Ring| 63x63 pt (126x126 px @2x)| 66.5x66.5 pt (133x133 px @2x)| 70.5x70.5 pt (141x141 px @2x)| 73x73 pt (146x146 px @2x)| 79x79 pt (158x158 px @2x) +Simple| 91x91 pt (182x182 px @2x)| 101.5x101.5 pt (203x203 px @2x)| 107.5x107.5 pt (215x215 px @2x)| 112x112 pt (224x224 px @2x)| 121x121 pt (242x242 px @2x ) +Stack| 78x42 pt (156x84 px @2x)| 87x45 pt (174x90 px @2x)| 92x47.5 pt (184x95 px @2x)| 96x51 pt (192x102 px @2x)| 103.5x53.5 pt (207x107 px @2x) +Placeholder| 91x91 pt (182x182 px @2x)| 101.5x101.5 pt (203x203 px @2x)| 107.5x107.5 pt (215x215 px @2x)| 112x112 pt (224x224 px @2x)| 121x121 pt (242x242 px @2x) + +Note + +In each stack measurement, the width value represents the maximum size. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/complications#Platform-considerations) + + _Not supported in iOS, iPadOS, macOS, tvOS, or visionOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/complications#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/complications#Related) + +[Watch faces](https://developer.apple.com/design/human-interface-guidelines/watch-faces) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/complications#Developer-documentation) + +[WidgetKit](https://developer.apple.com/documentation/WidgetKit) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/complications#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/56D03FE8-0566-429A-81D2-2F6566031498/8390_wide_250x141_1x.jpg) Design widgets for the Smart Stack on Apple Watch ](https://developer.apple.com/videos/play/wwdc2023/10309) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/124/34240770-1E20-456E-907F-3D06D6C87AFE/6544_wide_250x141_1x.jpg) Go further with Complications in WidgetKit ](https://developer.apple.com/videos/play/wwdc2022/10051) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/124/A37E04FC-826F-44B5-A5D8-36B41E5F73E5/6543_wide_250x141_1x.jpg) Complications and widgets: Reloaded ](https://developer.apple.com/videos/play/wwdc2022/10050) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/complications#Change-log) + +Date| Changes +---|--- +October 24, 2023| Replaced links to deprecated ClockKit documentation with links to WidgetKit documentation. +June 5, 2023| Updated guidance for rectangular complications to support them as widgets in the Smart Stack. +September 14, 2022| Added specifications for Apple Watch Ultra. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/home-screen-quick-actions.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/home-screen-quick-actions.md new file mode 100644 index 00000000..ca4ca04d --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/home-screen-quick-actions.md @@ -0,0 +1,42 @@ +--- +title: "Home Screen quick actions | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/home-screen-quick-actions + +# Home Screen quick actions + +Home Screen quick actions give people a way to perform app-specific actions from the Home Screen. + +![A stylized representation of a set of menu items extending up from an app icon. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/2dfd04b8dd39ab0d828c4ab2a8e46ef2/components-home-screen-quick-actions-intro%402x.png) + +People can get a menu of available quick actions when they touch and hold an app icon (on a 3D Touch device, people can press on the icon with increased pressure to see the menu). For example, Mail includes quick actions that open the Inbox or the VIP mailbox, initiate a search, and create a new message. In addition to app-specific actions, a Home Screen quick action menu also lists items for removing the app and editing the Home Screen. + +Each Home Screen quick action includes a title, an interface icon on the left or right (depending on your appโ€™s position on the Home Screen), and an optional subtitle. The title and subtitle are always left-aligned in left-to-right languages. Your app can even dynamically update its quick actions when new information is available. For example, Messages provides quick actions for opening your most recent conversations. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/home-screen-quick-actions#Best-practices) + +**Create quick actions for compelling, high-value tasks.** For example, Maps lets people search near their current location or get directions home without first opening the Maps app. People tend to expect every app to provide at least one useful quick action; you can provide a total of four. + +**Avoid making unpredictable changes to quick actions.** Dynamic quick actions are a great way to keep actions relevant. For example, it may make sense to update quick actions based on the current location or recent activities in your app, time of day, or changes in settings. Make sure that actions change in ways that people can predict. + +**For each quick action, provide a succinct title that instantly communicates the results of the action.** For example, titles like โ€œDirections Home,โ€ โ€œCreate New Contact,โ€ and โ€œNew Messageโ€ can help people understand what happens when they choose the action. If you need to give more context, provide a subtitle too. Mail uses subtitles to indicate whether there are unread messages in the Inbox and VIP folder. Donโ€™t include your app name or any extraneous information in the title or subtitle, keep the text short to avoid truncation, and take localization into account as you write the text. + +**Provide a familiar interface icon for each quick action.** Prefer using [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) to represent actions. For a list of icons that represent common actions, see [Standard icons](https://developer.apple.com/design/human-interface-guidelines/icons#Standard-icons); for additional guidance, see [Menus](https://developer.apple.com/design/human-interface-guidelines/menus). + +If you design your own interface icon, use the Quick Action Icon Template thatโ€™s included with [Apple Design Resources for iOS and iPadOS](https://developer.apple.com/design/resources/#ios-apps). + +**Donโ€™t use an emoji in place of a symbol or interface icon.** Emojis are full color, whereas quick action symbols are monochromatic and change appearance in Dark Mode to maintain contrast. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/home-screen-quick-actions#Platform-considerations) + + _No additional considerations for iOS or iPadOS. Not supported in macOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/home-screen-quick-actions#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/home-screen-quick-actions#Related) + +[Menus](https://developer.apple.com/design/human-interface-guidelines/menus) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/home-screen-quick-actions#Developer-documentation) + +[Add Home Screen quick actions](https://developer.apple.com/documentation/UIKit/add-home-screen-quick-actions) โ€” UIKit + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/live-activities.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/live-activities.md new file mode 100644 index 00000000..4efc4239 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/live-activities.md @@ -0,0 +1,442 @@ +--- +title: "Live Activities | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/live-activities + +# Live Activities + +A Live Activity lets people track the progress of an activity, event, or task at a glance. + +![A stylized representation of the Dynamic Island, in collapsed and expanded form, displaying the score of a live sporting event. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/05e81f3cb457f179fa1343f0753499c7/components-live-activities-intro%402x.png) + +Live Activities let people keep track of tasks and events in glanceable locations across devices. They go beyond push notifications, delivering frequent content and status updates over a few hours and letting people interact with the displayed information. + +For example, a Live Activity might show the remaining time until a food delivery order arrives, live in-game information for a soccer match, or real-time fitness metrics and interactive controls to pause or cancel a workout. + +Live Activities start on iPhone or iPad and automatically appear in system locations across a personโ€™s devices: + +Platform or system experience| Location +---|--- +iPhone and iPad| Lock Screen, Home Screen, in the Dynamic Island and StandBy on iPhone +Mac| The menu bar +Apple Watch| Smart Stack +CarPlay| CarPlay Dashboard + +## [Anatomy](https://developer.apple.com/design/human-interface-guidelines/live-activities#Anatomy) + +Live Activities appear across the system in various locations like the _Dynamic Island_ and the Lock Screen. It serves as a unified home for alerts and indicators of ongoing activity. Depending on the device and system location where a Live Activity appears, the system chooses a _presentation_ style or a combination of styles to compose the appearance of your Live Activity. As a result, your Live Activity must support: + + * [Compact](https://developer.apple.com/design/human-interface-guidelines/live-activities#Compact) + + * [Minimal](https://developer.apple.com/design/human-interface-guidelines/live-activities#Minimal) + + * [Expanded](https://developer.apple.com/design/human-interface-guidelines/live-activities#Expanded) + + * [Lock Screen](https://developer.apple.com/design/human-interface-guidelines/live-activities#Lock-Screen) + + + + +In iOS and iPadOS, your Live Activity appears throughout the system using these presentations. Additionally, the system uses them to create default appearances for other contexts. For example, the compact presentation appears in the Dynamic Island on iPhone and consists of two elements that the system combines into a single view for Apple Watch and in CarPlay. + +### [Compact](https://developer.apple.com/design/human-interface-guidelines/live-activities#Compact) + +In the Dynamic Island, the system uses the compact presentation when only one Live Activity is active. The presentation consists of two separate elements: one on the leading side of the TrueDepth camera and one on the trailing side. Despite its limited space, the compact presentation displays up-to-date information about your appโ€™s Live Activity. + +![An illustration that shows the compact leading and compact trailing views in the Dynamic Island.](https://docs-assets.developer.apple.com/published/4992561b879b5788db4f1f0360f88c38/type-compact%402x.png) + +For design guidance, see [Compact presentation](https://developer.apple.com/design/human-interface-guidelines/live-activities#Compact-presentation). + +### [Minimal](https://developer.apple.com/design/human-interface-guidelines/live-activities#Minimal) + +When multiple Live Activities are active, the system uses the minimal presentation to display two of them in the Dynamic Island. One appears attached to the Dynamic Island while the other appears detached. Depending on its content size, the detached minimal presentation appears circular or oval. As with the compact presentation, people tap the minimal presentation to open its app or touch and hold it to see the expanded presentation. + +![An illustration that shows the minimal presentation in the Dynamic Island.](https://docs-assets.developer.apple.com/published/f90f614f67e66940fa8a3e8ca861b4d9/type-minimal%402x.png) + +For design guidance, see [Minimal presentation](https://developer.apple.com/design/human-interface-guidelines/live-activities#Minimal-presentation). + +### [Expanded](https://developer.apple.com/design/human-interface-guidelines/live-activities#Expanded) + +When people touch and hold a Live Activity in compact or minimal presentation, the system displays the expanded presentation. + +![An illustration that shows the expanded view in the Dynamic Island.](https://docs-assets.developer.apple.com/published/4e5af6b7073ffa8245e0efd5e6815f0d/type-expanded%402x.png) + +For design guidance, see [Expanded presentation](https://developer.apple.com/design/human-interface-guidelines/live-activities#Expanded-presentation). + +### [Lock Screen](https://developer.apple.com/design/human-interface-guidelines/live-activities#Lock-Screen) + +The system uses the Lock Screen presentation to display a banner at the bottom of the Lock Screen. In this presentation, use a layout similar to the expanded presentation. + +![A screenshot of a Live Activity on the Lock Screen of iPhone that supports the Dynamic Island.](https://docs-assets.developer.apple.com/published/fe50abc1d4dff465883107ba9448a19a/live-activity-lock-screen%402x.png) + +When you alert people about Live Activity updates on devices that donโ€™t support the Dynamic Island, the Lock Screen presentation briefly appears as a banner that overlays the Home Screen or other apps. + +![A screenshot of a Live Activity that appears as a banner on the Home Screen of iPhone without Dynamic Island support.](https://docs-assets.developer.apple.com/published/2b1fef03830a1927b085c2efb8ddcaf9/live-activity-notch%402x.png) + +For design guidance, see [Lock Screen presentation](https://developer.apple.com/design/human-interface-guidelines/live-activities#Lock-Screen-presentation). + +### [StandBy](https://developer.apple.com/design/human-interface-guidelines/live-activities#StandBy) + +On iPhone in StandBy, your Live Activity appears in the minimal presentation. When someone taps it, it transitions to the Lock Screen presentation, scaled up by 2x to fill the screen. If your Lock Screen presentation uses a custom background color, the system automatically extends it to the whole screen to create a seamless, full-screen design. + +![An image that shows the Lock Screen presentation of a Live Activity in StandBy, scaled up by 2x, with a dotted border to indicate the 2x scaling of the Live Activity.](https://docs-assets.developer.apple.com/published/fbbd5973af16593f3fd5ee7a2ddbebf8/live-activity-standby-default-outline%402x.png) + +For design guidance, see [StandBy presentation](https://developer.apple.com/design/human-interface-guidelines/live-activities#StandBy-presentation). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/live-activities#Best-practices) + +**Offer Live Activities for tasks and events that have a defined beginning and end.** Live Activities work best for tracking short to medium duration activities that donโ€™t exceed eight hours. + +**Focus on important information that people need to see at a glance.** Your Live Activity doesnโ€™t need to display everything. Think about what information people find most useful and prioritize sharing it in a concise way. When a person wants to learn more, they can tap your Live Activity to open your app where you can provide additional detail. + +**Donโ€™t use a Live Activity to display ads or promotions**. Live Activities help people stay informed about ongoing events and tasks, so itโ€™s important to display only information thatโ€™s related to those events and tasks. + +**Avoid displaying sensitive information.** Live Activities are prominently visible and could be viewed by casual observers; for example, on the Lock Screen or in the Always-On display. For content people might consider sensitive or private, display an innocuous summary and let people tap the Live Activity to view the sensitive information in your app. Alternatively, redact views that may contain sensitive information and let people configure whether to show sensitive data. For developer guidance, see [Creating a widget extension](https://developer.apple.com/documentation/WidgetKit/Creating-a-Widget-Extension#Hide-sensitive-content). + +**Create a Live Activity that matches your appโ€™s visual aesthetic and personality in both dark and light appearances.** This makes it easier for people to recognize your Live Activity and creates a visual connection to your app. + +**If you include a logo mark, display it without a container.** This better integrates the logo mark with your Live Activity layout. Donโ€™t use the entire app icon. + +**Donโ€™t add elements to your app that draw attention to the Dynamic Island.** Your Live Activity appears in the Dynamic Island while your app isnโ€™t in use, and other items can appear in the Dynamic Island when your app is open. + +**Ensure text is easy to read.** Use large, heavier-weight text โ€” a medium weight or higher. Use small text sparingly and make sure key information is legible at a glance. + +![An illustration that shows text in the Dynamic Island that's small and difficult to read.](https://docs-assets.developer.apple.com/published/6305527c5b59013de149075e4a20a138/live-activities-text-incorrect-size%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration that shows text in the Dynamic Island with heavier weights and legible size.](https://docs-assets.developer.apple.com/published/1cdb8d4deff3d5465cf4c71642bc94de/live-activities-text-correct-size%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +### [Creating Live Activity layouts](https://developer.apple.com/design/human-interface-guidelines/live-activities#Creating-Live-Activity-layouts) + +**Adapt to different screen sizes and presentations.** Live Activities scale to fit various device screens. Create layouts and assets for various devices and scale factors, recognizing that the actual size on screen may vary or change. Ensure they look great everywhere by using the values in [Specifications](https://developer.apple.com/design/human-interface-guidelines/live-activities#Specifications) as guidance and providing appropriately sized content. + +**Adjust element size and placement for efficient use of space.** Create a layout that only uses the space you need to clearly display its content. Adapt the size and placement of elements in your Live Activity so they fit well together. + +**Use familiar layouts for custom views and layouts.** Templates with default system margins and recommended text sizes are available in [Apple Design Resources](https://developer.apple.com/design/resources/). Using them helps your Live Activity remain legible at a glance and fit in with the visual language of its surroundings; for example, the Smart Stack on Apple Watch. + +![An illustration that shows content in the Dynamic Island with even margins.](https://docs-assets.developer.apple.com/published/08f636dea59d0355bbc0c42d67026509/live-activities-margins%402x.png) + +**Use consistent margins and concentric placement.** Use even, matching margins between rounded shapes and the edges of the Live Activity, including corners, to ensure a harmonious fit. This prevents elements from poking into the rounded shape of the Live Activity and creating visual tension. For example, when placing a rounded rectangle near a corner of your Live Activity, match its corner radius to the outer corner radius of the Live Activity by subtracting the margin and using a SwiftUI container to apply the correct corner radius. For developer guidance, see [`ContainerRelativeShape`](https://developer.apple.com/documentation/SwiftUI/ContainerRelativeShape). + +![An illustration a Live Activity that draws content to the edge of the Dynamic Island.](https://docs-assets.developer.apple.com/published/881638db784a565ec2af57941e8dec5d/live-activities-rounded-shapes%402x.png) + +Keep content compact and snug within a margin thatโ€™s concentric to the outer edge of the Live Activity. + +![An illustration that shows how a Live Activity places an icon too far from the edge of the Dynamic Island.](https://docs-assets.developer.apple.com/published/f6ab85a99b55f39774f71e5f03d22455/live-activities-content-incorrect-position%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration that shows how a Live Activity places an icon close to the edge of the Dynamic Island without poking into the rounded shape of the Dynamic Island.](https://docs-assets.developer.apple.com/published/c533297be7133d3ebeb70eaa6445b7c4/live-activities-content-correct-position%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +**When separating a block of content, place it in an inset container shape or use a thick line.** Donโ€™t draw content all the way to the edge of the Dynamic Island. + +![An illustration that shows how a Live Activity draws content all the way to the edge of the Dynamic Island to separate content.](https://docs-assets.developer.apple.com/published/555c74e1fb1cdbb7a7f26c4da9a86cc8/live-activities-separating-content-incorrect%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration of a Live Activity with content in an inset, rounded shape to group it together.](https://docs-assets.developer.apple.com/published/2efb360240b1bfe7f7eac8e7a5a72748/live-activities-separating-content-pill%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +![An illustration of a Live Activity that uses a line to separate a block of content.](https://docs-assets.developer.apple.com/published/2494f07e0a4653b67e75bd967c32fb93/live-activities-separating-content-separator%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +Tip + +To align nonrounded content in the rounded corners of the Live Activity view, it may be helpful to blur the nonrounded content in your drawing tool. When the content is blurred, it may be easier to find the positioning that best aligns with the outer perimeter of the view. + +![An illustration that shows a Live Activity with blurred text that's too far from the edge of the Dynamic Island.](https://docs-assets.developer.apple.com/published/803af1a5bf16d67d55e8f71b7e45bf1e/live-activities-blur-content-incorrect-position%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration that shows a Live Activity with blurred text that's close to the edge of the Dynamic Island without poking into the rounded shape of the Dynamic Island.](https://docs-assets.developer.apple.com/published/f616e49dc9d2ab9564930596bd59be97/live-activities-blur-content-correct-position%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +**Dynamically change the height of your Live Activity on the Lock Screen or in the expanded presentation.** When thereโ€™s less information to show, reduce the height of the Live Activity to only use the space needed for the content. When more information becomes available, increase the height to display additional content. For example, a rideshare app might display a more compact Live Activity without additional details while it locates a driver. The appโ€™s height extends as more information is available to display the estimated pickup time, driver details, and so on. + +### [Choosing colors](https://developer.apple.com/design/human-interface-guidelines/live-activities#Choosing-colors) + +**Carefully consider using a custom background color and opacity.** You canโ€™t customize background colors for compact, minimal, and expanded presentations. However, you can use a custom background color for the Lock Screen presentation. If you set a custom background color or image for the Lock Screen presentation, ensure sufficient contrast โ€” especially for tint colors on devices that feature an Always-On display with reduced luminance. + +**Use color to express the character and identity of your app.** Live Activities in the Dynamic Island use a black opaque background. Consider using bold colors for text and objects to convey the personality and brand of your app. Bold colors make your Live Activity recognizable at a glance, stand out from other Live Activities, and feel like a small, glanceable part of your app. Additionally, bold colors can help reinforce the relationship between elements in the Live Activity itself. + +**Tint your Live Activityโ€™s key line color so that it matches your content.** When the background is dark โ€” for example, in Dark Mode โ€” a key line appears around the Dynamic Island to distinguish it from other content. Choose a key line color thatโ€™s consistent with the color of other elements in your Live Activity. For developer guidance, see [Creating custom views for Live Activities](https://developer.apple.com/documentation/ActivityKit/creating-custom-views-for-live-activities#Use-custom-colors). + +### [Adding transitions and animating content updates](https://developer.apple.com/design/human-interface-guidelines/live-activities#Adding-transitions-and-animating-content-updates) + +In addition to extending and contracting transitions, Live Activities use system and custom animations with a maximum duration of two seconds. Note that the system doesnโ€™t perform animations on Always-On displays with reduced luminance. + +**Use animations to reinforce the information youโ€™re communicating and to bring attention to updates.** In addition to moving the position of elements, you can animate elements in and out with the default content-replace transition, or create custom transitions using scale, opacity, and movement. For example, a sports app might use numeric content transitions for score changes or fade a timer in and out when it reaches zero. + +**Animate layout changes.** Content updates can require a change to your Live Activity layout โ€” for example, when it expands to fill the screen in StandBy or when more information becomes available. During the transition to a new layout, preserve as much of the existing layout as possible by animating existing elements to their new positions rather than removing and animating them back in. + +**Try to avoid overlapping elements.** Sometimes, itโ€™s best to animate out certain elements and then re-animate them in at a new position to avoid colliding with other parts of your transition. For example, when animating items in lists, only animate the element that moves to a new position and use fade-in-and-out transitions for the other list items. + +For developer guidance, see [Animating data updates in widgets and Live Activities](https://developer.apple.com/documentation/WidgetKit/Animating-data-updates-in-widgets-and-live-activities). + +### [Offering interactivity](https://developer.apple.com/design/human-interface-guidelines/live-activities#Offering-interactivity) + +**Make sure tapping the Live Activity opens your app at the right location.** Take people directly to related details and actions โ€” donโ€™t make them navigate to find relevant information. For developer guidance on SwiftUI views that support deep linking to specific screens, see [Linking to specific app scenes from your widget or Live Activity](https://developer.apple.com/documentation/WidgetKit/Linking-to-specific-app-scenes-from-your-widget-or-Live-Activity). + +**Focus on simple, direct actions.** Buttons or toggles take up space that might otherwise display useful information. Only include interactive elements for essential functionality thatโ€™s directly related to your Live Activity and that people activate once or temporarily pause and resume, like music playback, workouts, or apps that access the microphone to record live audio. If you offer interactivity, prefer limiting it to a single element to help people avoid accidentally tapping the wrong control. + +**Consider letting people respond to event or progress updates.** If an update to your Live Activity is something that a person could respond to, consider offering a button or toggle to let people take action. For example, the Live Activity of a rideshare app could include a button to contact the driver while waiting for a ride to arrive. + +### [Starting, updating, and ending a Live Activity](https://developer.apple.com/design/human-interface-guidelines/live-activities#Starting-updating-and-ending-a-Live-Activity) + +**Start Live Activities at appropriate times, and make it easy for people to turn them off in your app.** People expect Live Activities to start and provide important updates for a task at hand or at specific times, even automatically. For example, they might expect a Live Activity to start after a food order, making a rideshare request, or when their favorite sports teamโ€™s match begins. However, Live Activities that appear unexpectedly can be surprising or even unwanted. Consider offering controls that allow people to turn off a Live Activity in the app view that corresponds to the activity. For example, a sports app may offer a button that lets people unfollow a game or team. When people canโ€™t easily control the appearance of Live Activities from your app, they may choose to turn off Live Activities in Settings altogether. + +**Offer an App Shortcut that starts your Live Activity.** App Shortcuts expose functionality to the system, allowing access in various contexts. For example, create an App Shortcut that allows people to start your Live Activity using the Action button on iPhone. For more information, see [App Shortcuts](https://developer.apple.com/design/human-interface-guidelines/app-shortcuts). + +**Update a Live Activity only when new content is available.** If the underlying content or status remains the same, maintain the same display until the underlying content or status changes. + +**Alert people only for essential updates that require their attention.** Live Activity alerts light up the screen and by default play the notification sound to alert people about updates they shouldnโ€™t miss. Alerts also show the expanded presentation in the Dynamic Island or a banner on devices that donโ€™t support the Dynamic Island. To ensure your Live Activities provide the most value, avoid alerting people too often or with updates that arenโ€™t crucial, and donโ€™t use push notifications alongside Live Activities for the same updates. + +**Let people track multiple events efficiently with a single Live Activity.** Instead of creating separate Live Activities people need to jump between to track different events, prefer a single Live Activity that uses a dynamic layout and rotates through events. For example, a sports app could offer a single Live Activity that cycles through scored points, substitutions, and fouls across multiple matches. + +**Always end a Live Activity immediately when the task or event ends, and consider setting a custom dismissal time.** When a Live Activity ends, the system immediately removes it from the Dynamic Island and in CarPlay. On the Lock Screen, in the Mac menu bar, and the watchOS Smart Stack, it remains for up to four hours. Depending on the Live Activity, showing a summary may only be relevant for a brief time after it ends. Consider choosing a custom dismissal time thatโ€™s proportional to the duration of your Live Activity. In most cases, 15 to 30 minutes is adequate. For example, a rideshare app could end its Live Activity when a ride completes and remain visible for 30 minutes to allow people to view the ride summary and leave a tip. For developer guidance, refer to [Displaying live data with Live Activities](https://developer.apple.com/documentation/ActivityKit/displaying-live-data-with-live-activities#End-the-Live-Activity). + +## [Presentation](https://developer.apple.com/design/human-interface-guidelines/live-activities#Presentation) + +Your Live Activity needs to support all locations, devices, and their corresponding appearances. Because it appears across systems at different dimensions, create Live Activity layouts that best support each place they appear. + +**Start with the iPhone design, then refine it for other contexts.** Create standard designs for each presentation first. Then, depending on the functionality that your Live Activity provides, design additional custom layouts for specific contexts like iPhone in StandBy, CarPlay, or Apple Watch. For more information about custom layouts, refer to [StandBy](https://developer.apple.com/design/human-interface-guidelines/live-activities#StandBy), [CarPlay](https://developer.apple.com/design/human-interface-guidelines/live-activities#CarPlay), and [watchOS](https://developer.apple.com/design/human-interface-guidelines/live-activities#watchOS). + +### [Compact presentation](https://developer.apple.com/design/human-interface-guidelines/live-activities#Compact-presentation) + +**Focus on the most important information.** Use the compact presentation to show dynamic, up-to-date information thatโ€™s essential to the Live Activity and easy to understand. For example, a sports app could display two team logos and the score. + +**Ensure unified information and design of the compact presentations in the Dynamic Island.** Though the TrueDepth camera separates the leading and trailing elements, design them to read as a single piece of information, and use consistent color and typography to help create a connection between both elements. + +**Keep content as narrow as possible and ensure itโ€™s snug against the TrueDepth camera.** Try not to obscure key information in the status bar, and donโ€™t add padding between content and the TrueDepth camera. Maintain a balanced layout with similarly sized views for both leading and trailing elements; for example, use shortened units or less precise data to maintain appropriate width and balance. + +![An illustration that shows a compact presentation that appears unbalanced and too wide because it uses padding around the TrueDepth camera.](https://docs-assets.developer.apple.com/published/51a8d7f39e6868d94bdc97170aaa48d2/live-activities-unbalanced-content%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration that shows a compact presentation thatโ€™s snug around the TrueDepth camera.](https://docs-assets.developer.apple.com/published/b8e383ef2dd391dab4fee6383311d9a5/live-activities-balanced-content%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +**Link to relevant app content.** When people tap a compact Live Activity, open your app directly to the related details. Ensure both leading and trailing elements link to the same screen. + +### [Minimal presentation](https://developer.apple.com/design/human-interface-guidelines/live-activities#Minimal-presentation) + +**Ensure that your Live Activity is recognizable in the minimal presentation.** If possible, display updated information rather than just a logo, while ensuring people can quickly recognize your app. For example, the Timer appโ€™s minimal Live Activity presentation displays the remaining time instead of a static icon. + +### [Expanded presentation](https://developer.apple.com/design/human-interface-guidelines/live-activities#Expanded-presentation) + +**Maintain the relative placement of elements to create a coherent layout between presentations.** The expanded presentation is an enlarged version of the compact or minimal presentation. Ensure information and layouts expand predictably when the Live Activity expands. + +**Wrap content tightly around the TrueDepth camera.** Arrange content close to the TrueDepth camera, and try to avoid leaving too much room around it to use space more efficiently and to help diminish the cameraโ€™s presence. + +![An illustration that shows an expanded presentation of a Live Activity that leaves empty space next to the TrueDepth camera.](https://docs-assets.developer.apple.com/published/5e156a4d49df18dd990d45ba5f6e7f22/live-activities-layout-incorrect%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration that shows an expanded presentation of a Live Activity that uses the space next to the TrueDepth camera.](https://docs-assets.developer.apple.com/published/9bbe31974cd97e499ef7e6606e195bca/live-activities-layout-correct%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +### [Lock Screen presentation](https://developer.apple.com/design/human-interface-guidelines/live-activities#Lock-Screen-presentation) + +**Donโ€™t replicate notification layouts.** Create a unique layout thatโ€™s specific to the information that appears in the Live Activity. + +**Choose colors that work well on a personalized Lock Screen.** People customize their Lock Screen with wallpapers, custom tint colors, and widgets. To make a Live Activity fit a custom Lock Screen aesthetic while remaining legible, use custom background or tint colors and opacity sparingly. + +**Make sure your design, assets, and colors look great and offer enough contrast in Dark Mode and on an Always-On display.** By default, a Live Activity on the Lock Screen uses a light background color in the light appearance and a dark background color in the dark appearance. If you use a custom background color, choose a color that works well in both modes or a different color for each appearance. Verify your choices on a device with an Always-On display with reduced luminance because the system adapts colors as needed in this appearance. For guidance, see [Dark Mode](https://developer.apple.com/design/human-interface-guidelines/dark-mode) and [Always On](https://developer.apple.com/design/human-interface-guidelines/always-on); for developer guidance, see [About asset catalogs](https://help.apple.com/xcode/mac/current/#/dev10510b1f7). + +**Verify the generated color of the dismiss button.** The system automatically generates a matching dismiss button based on the background and foreground colors of your Live Activity. Verify that the generated color matches your design and adjust it if needed using [`activitySystemActionForegroundColor(_:)`](https://developer.apple.com/documentation/SwiftUI/View/activitySystemActionForegroundColor\(_:\)). + +**Use standard margins to align your design with notifications.** The standard layout margin for Live Activities on the Lock Screen is 14 points. While tighter margins may be appropriate for elements like graphics or buttons, avoid crowding the edges and creating a cluttered appearance. For developer guidance, see [`padding(_:_:)`](https://developer.apple.com/documentation/SwiftUI/View/padding\(_:_:\)). + +### [StandBy presentation](https://developer.apple.com/design/human-interface-guidelines/live-activities#StandBy-presentation) + +**Update your layout for StandBy.** Make sure assets look great at the larger scale, and consider creating a custom layout that makes use of the extra space. For developer guidance, see [Creating custom views for Live Activities](https://developer.apple.com/documentation/ActivityKit/creating-custom-views-for-live-activities). + +**Consider using the default background color in StandBy.** The default background color seamlessly blends your Live Activity with the device bezel, achieves a softer look that integrates with a personโ€™s surroundings, and allows the system to scale the Live Activity slightly larger because it doesnโ€™t need to account for the margins around the TrueDepth camera. + +**Use standard margins and avoid extending graphic elements to the edge of the screen.** Without standard margins, content gets cut off as the Live Activity extends, making it feel broken. + +**Verify your design in Night Mode.** In Night Mode, the system applies a red tint to your Live Activity. Check that your Live Activity design uses colors that provide enough contrast in Night Mode. + +![A Live Activity, scaled to fill the screen on iPhone in StandBy.](https://docs-assets.developer.apple.com/published/c7f65b20bec28281075a61264019fe50/live-activity-standby-night-mode%402x.png) + +## [CarPlay](https://developer.apple.com/design/human-interface-guidelines/live-activities#CarPlay) + +In CarPlay, the system automatically combines the leading and trailing elements of the compact presentation into a single layout that appears on CarPlay Dashboard. + +Your Live Activity design applies to both CarPlay and Apple Watch, so design for both contexts. While Live Activities on Apple Watch can be interactive, the system deactivates interactive elements in CarPlay. For more information, refer to [watchOS](https://developer.apple.com/design/human-interface-guidelines/live-activities#watchOS) below. For developer guidance, refer to [Creating custom views for Live Activities](https://developer.apple.com/documentation/ActivityKit/creating-custom-views-for-live-activities). + +**Consider creating a custom layout if your Live Activity would benefit from larger text or additional information.** Instead of using the default appearance in CarPlay, declare support for a [`ActivityFamily.small`](https://developer.apple.com/documentation/WidgetKit/ActivityFamily/small) supplemental activity family. + +**Carefully consider including buttons or toggles in your custom layout.** In CarPlay, the system deactivates interactive elements in your Live Activity. If people are likely to start or observe your Live Activity while driving, prefer displaying timely content rather than buttons and toggles. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/live-activities#Platform-considerations) + + _No additional considerations for iOS or iPadOS. Not supported in tvOS or visionOS._ + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/live-activities#macOS) + +Active Live Activities automatically appear in the Menu bar of a paired Mac using the compact, minimal, and expanded presentations. Clicking the Live Activity launches iPhone Mirroring to display your app. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/live-activities#watchOS) + +When a Live Activity begins on iPhone, it appears on a paired Apple Watch at the top of the Smart Stack. By default, the view displayed in the Smart Stack combines the leading and trailing elements from the Live Activityโ€™s compact presentation on iPhone. + +If you offer a watchOS app and someone taps the Live Activity in the Smart Stack, it opens your watchOS app. Without a watchOS app, tapping opens a full-screen view with a button to open your app on the paired iPhone. + +**Consider creating a custom watchOS layout.** While the system provides a default view automatically, a custom layout designed for Apple Watch can show more information and add interactive functionality like a button or toggle. + +**Carefully consider including buttons or toggles in your custom layout.** The custom watchOS layout also applies to your Live Activity in CarPlay where the system deactivates interactive elements. If people are likely to start or observe your Live Activity while driving, donโ€™t include buttons or toggles in your custom watchOS layout. For developer guidance, see [Creating custom views for Live Activities](https://developer.apple.com/documentation/ActivityKit/creating-custom-views-for-live-activities). + +![An illustration that shows the compact presentation of a Live Activity in the Dynamic Island on iPhone.](https://docs-assets.developer.apple.com/published/3f81a5e4da2d3e59a8018a9e1d45f52e/live-activities-ios-dynamic-island-default%402x.png)iPhone compact view + +![An illustration that shows the automatically generated default presentation of a Live Activity in a Smart Stack view, with the leading and trailing elements from the iPhone compact view spaced apart in the lower corners.](https://docs-assets.developer.apple.com/published/1441a47a3ba447c6ccbfbd8ba8565bd5/live-activity-watch-default-implementation%402x.png)Default Smart Stack view + +![An illustration that shows a custom presentation of a Live Activity in a Smart Stack view, with a balanced design that shows a graphical countdown timer balanced with explanatory text.](https://docs-assets.developer.apple.com/published/cd8ec7a71aab86b947906afbdd802f6b/live-activity-watch-custom-implementation%402x.png)Custom Smart Stack view + +**Focus on essential information and significant updates.** Use space in the Smart Stack as efficiently as possible and think of the most useful information that a Live Activity can convey: + + * Progress, like the estimated arrival time of a delivery + + * Interactive elements, like stopwatch or timer controls + + * Significant updates, like sports score changes + + + + +## [Specifications](https://developer.apple.com/design/human-interface-guidelines/live-activities#Specifications) + +When you design your Live Activities, use the following values for guidance. + +### [CarPlay dimensions](https://developer.apple.com/design/human-interface-guidelines/live-activities#CarPlay-dimensions) + +The system may scale your Live Activity to best fit a vehicleโ€™s screen size and resolution. Use the listed values to verify your design: + +Live Activity size (pt) +--- +240x78 +240x100 +170x78 + +Test your designs with the CarPlay Simulator and the following configurations for Smart Display Zoom โ€” available in in Settings > Display in CarPlay: + +Configuration| Resolution (pt) +---|--- +Widescreen| 1920x720 +Portrait| 900x1200 +Standard| 800x480 + +### [iOS dimensions](https://developer.apple.com/design/human-interface-guidelines/live-activities#iOS-dimensions) + +All values listed in the tables below are in points. + +Screen dimensions (portrait)| Compact leading| Compact trailing| Minimal (width given as a range)| Expanded (height given as a range)| Lock Screen (height given as a range) +---|---|---|---|---|--- +430x932| 62.33x36.67| 62.33x36.67| 36.67โ€“45x36.67| 408x84โ€“160| 408x84โ€“160 +393x852| 52.33x36.67| 52.33x36.67| 36.67โ€“45x36.67| 371x84โ€“160| 371x84โ€“160 + +The Dynamic Island uses a corner radius of 44 points, and its rounded corner shape matches the TrueDepth camera. + +Presentation type| Device| Dynamic Island width (pt) +---|---|--- +Compact or minimal| iPhone 17 Pro Max| 250 +| iPhone 17 Pro| 230 +| iPhone Air| 250 +| iPhone 17| 230 +| iPhone 16 Pro Max| 250 +| iPhone 16 Pro| 230 +| iPhone 16 Plus| 250 +| iPhone 16| 230 +| iPhone 15 Pro Max| 250 +| iPhone 15 Pro| 230 +| iPhone 15 Plus| 250 +| iPhone 15| 230 +| iPhone 14 Pro Max| 250 +| iPhone 14 Pro| 230 +Expanded| iPhone 17 Pro Max| 408 +| iPhone 17 Pro| 371 +| iPhone Air| 408 +| iPhone 17| 371 +| iPhone 16 Pro Max| 408 +| iPhone 16 Pro| 371 +| iPhone 16 Plus| 408 +| iPhone 16| 371 +| iPhone 15 Pro Max| 408 +| iPhone 15 Pro| 371 +| iPhone 15 Plus| 408 +| iPhone 15| 371 +| iPhone 14 Pro Max| 408 +| iPhone 14 Pro| 371 + +### [iPadOS dimensions](https://developer.apple.com/design/human-interface-guidelines/live-activities#iPadOS-dimensions) + +All values listed in the table below are in points. + +Screen dimensions (portrait)| Lock Screen (height given as a range) +---|--- +1366x1024| 500x84โ€“160 +1194x834| 425x84โ€“160 +1012x834| 425x84โ€“160 +1080x810| 425x84โ€“160 +1024x768| 425x84โ€“160 + +### [macOS dimensions](https://developer.apple.com/design/human-interface-guidelines/live-activities#macOS-dimensions) + +Use the provided iOS dimensions. + +### [watchOS dimensions](https://developer.apple.com/design/human-interface-guidelines/live-activities#watchOS-dimensions) + +Live Activities in the Smart Stack use the same dimensions as watchOS widgets. + +Apple Watch size| Size of a Live Activity in the Smart Stack (pt) +---|--- +40mm| 152x69.5 +41mm| 165x72.5 +44mm| 173x76.5 +45mm| 184x80.5 +49mm| 191x81.5 + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/live-activities#Resources) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/live-activities#Developer-documentation) + +[ActivityKit](https://developer.apple.com/documentation/ActivityKit) + +[SwiftUI](https://developer.apple.com/documentation/SwiftUI) + +[WidgetKit](https://developer.apple.com/documentation/WidgetKit) + +[Developing a WidgetKit strategy](https://developer.apple.com/documentation/WidgetKit/Developing-a-WidgetKit-strategy) โ€” WidgetKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/live-activities#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/3AE21C44-8831-4C0A-BCBE-68C437FB8FC8/9903_wide_250x141_1x.jpg) Turbocharge your app for CarPlay ](https://developer.apple.com/videos/play/wwdc2025/216) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/126547AE-9E47-4E15-AC05-5C50AB08CBEE/9952_wide_250x141_1x.jpg) Whatโ€™s new in widgets ](https://developer.apple.com/videos/play/wwdc2025/278) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/F5E64E98-8296-44DA-89AE-814BAEA0A713/9220_wide_250x141_1x.jpg) Design Live Activities for Apple Watch ](https://developer.apple.com/videos/play/wwdc2024/10098) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/73CFB4C1-8568-43E9-AECF-D679A7355B99/8255_wide_250x141_1x.jpg) Design dynamic Live Activities ](https://developer.apple.com/videos/play/wwdc2023/10194) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/B13DB71E-9E8F-4A86-88B3-306C4E772627/8244_wide_250x141_1x.jpg) Meet ActivityKit ](https://developer.apple.com/videos/play/wwdc2023/10184) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/live-activities#Change-log) + +Date| Changes +---|--- +December 16, 2025| Updated guidance for all platforms, and added guidance for macOS and CarPlay. +June 10, 2024| Added guidance for Live Activities in watchOS. +October 24, 2023| Expanded and updated guidance and added new artwork. +June 5, 2023| Updated guidance to include features of iOS 17 and iPadOS 17. +November 3, 2022| Updated artwork and specifications. +September 23, 2022| New page. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/notifications.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/notifications.md new file mode 100644 index 00000000..0db65b19 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/notifications.md @@ -0,0 +1,153 @@ +--- +title: "Notifications | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/notifications + +# Notifications + +A notification gives people timely, high-value information they can understand at a glance. + +![A stylized representation of a notification mockup. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/49d8e391f869407a8b755b57ee180c83/components-notification-intro%402x.png) + +Before you can send any notifications to people, you have to get their consent (for developer guidance, see [Asking permission to use notifications](https://developer.apple.com/documentation/UserNotifications/asking-permission-to-use-notifications)). After agreeing, people generally use settings to specify the styles of notification they want to receive, and to specify delivery times for notifications that have different levels of urgency. To learn more about the ways people can customize the notification experience, see [Managing notifications](https://developer.apple.com/design/human-interface-guidelines/managing-notifications). + +## [Anatomy](https://developer.apple.com/design/human-interface-guidelines/notifications#Anatomy) + +Depending on the platform, a notification can use various styles, such as: + + * A banner or view on a Lock Screen, Home Screen, Home View, or desktop + + * A badge on an app icon + + * An item in Notification Center + + + + +In addition, a notification related to direct communication โ€” like a phone call or message โ€” can provide an interface thatโ€™s distinct from noncommunication notifications, featuring prominent contact images (or _avatars_) and group names instead of the app icon. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/notifications#Best-practices) + +**Provide concise, informative notifications.** People turn on notifications to get quick updates, so you want to provide valuable information succinctly. + +**Avoid sending multiple notifications for the same thing, even if someone hasnโ€™t responded.** People attend to notifications at their convenience. If you send multiple notifications for the same thing, you fill up Notification Center, and people may turn off all notifications from your app. + +**Avoid sending a notification that tells people to perform specific tasks within your app.** If it makes sense to offer simple tasks that people can perform without opening your app, you can provide [notification actions](https://developer.apple.com/design/human-interface-guidelines/notifications#Notification-actions). Otherwise, avoid telling people what to do because itโ€™s hard for people to remember such instructions after they dismiss the notification. + +**Use an alert โ€” not a notification โ€” to display an error message.** People are familiar with both alerts and notifications, so you donโ€™t want to cause confusion by using the wrong component. For guidance, see [Alerts](https://developer.apple.com/design/human-interface-guidelines/alerts). + +**Handle notifications gracefully when your app is in the foreground.** Your appโ€™s notifications donโ€™t appear when your app is in the front, but your app still receives the information. In this scenario, present the information in a way thatโ€™s discoverable but not distracting or invasive, such as incrementing a badge or subtly inserting new data into the current view. For example, when a new message arrives in a mailbox that people are currently viewing, Mail simply adds it to the list of unread messages because sending a notification about it would be unnecessary and distracting. + +**Avoid including sensitive, personal, or confidential information in a notification.** You canโ€™t predict what people will be doing when they receive a notification, so itโ€™s essential to avoid including private information that could be visible to others. + +## [Content](https://developer.apple.com/design/human-interface-guidelines/notifications#Content) + +When a notification includes a title, the system displays it at the top where itโ€™s most visible. In a notification related to direct communication, the system automatically displays the senderโ€™s name in the title area; in a noncommunication notification, the system displays your app name if you donโ€™t provide a title. + +**Create a short title if it provides context for the notification content.** Prefer brief titles that people can read at a glance, especially on Apple Watch, where space is limited. When possible, take advantage of the prominent notification title area to provide useful information, like a headline, event name, or email subject. If you can only provide a generic title for a noncommunication notification โ€” like New Document โ€” it can be better to let the system display your app name instead. Use title-style [capitalization](https://support.apple.com/guide/applestyleguide/c-apsgb744e4a3/web#apdca93e113f1d64) and no ending punctuation. + +**Write succinct, easy-to-read notification content.** Use complete sentences, sentence case, and proper punctuation, and donโ€™t truncate your message โ€” the system does this automatically when necessary. + +**Provide generically descriptive text to display when notification previews arenโ€™t available.** In Settings, people can choose to hide notification previews for all apps. In this situation, the system shows only your app icon and the default title _Notification_. To give people sufficient context to know whether they want to view the full notification, write body text that succinctly describes the notification content without revealing too many details, like โ€œFriend request,โ€ โ€œNew comment,โ€ โ€œReminder,โ€ or โ€œShipmentโ€ (for developer guidance, see [`hiddenPreviewsBodyPlaceholder`](https://developer.apple.com/documentation/UserNotifications/UNNotificationCategory/hiddenPreviewsBodyPlaceholder)). Use sentence-style [capitalization](https://support.apple.com/guide/applestyleguide/c-apsgb744e4a3/web#apdca93e113f1d64) for this text. + +**Avoid including your app name or icon.** The system automatically displays a large version of your app icon at the leading edge of each notification; in a communication notification, the system displays the senderโ€™s contact image badged with a small version of your icon. + +**Consider providing a sound to supplement your notifications.** Sound can be a great way to distinguish your appโ€™s notifications and get someoneโ€™s attention when theyโ€™re not looking at the device. You can create a custom sound that coordinates with the style of your app or use a system-provided alert sound. If you use a custom sound, make sure itโ€™s short, distinctive, and professionally produced. A notification sound can enhance the user experience, but donโ€™t rely on it to communicate important information, because people may not hear it. Although people might also want a vibration to accompany alert sounds, you canโ€™t provide such a vibration programmatically. For developer guidance, see [`UNNotificationSound`](https://developer.apple.com/documentation/UserNotifications/UNNotificationSound). + +## [Notification actions](https://developer.apple.com/design/human-interface-guidelines/notifications#Notification-actions) + +A notification can present a customizable detail view that contains up to four buttons people use to perform actions without opening your app. For example, a Calendar event notification provides a Snooze button that postpones the eventโ€™s alarm for a few minutes. For developer guidance, see [Handling notifications and notification-related actions](https://developer.apple.com/documentation/UserNotifications/handling-notifications-and-notification-related-actions). + +**Provide beneficial actions that make sense in the context of your notification.** Prefer actions that let people perform common, time-saving tasks that eliminate the need to open your app. For each button, use a short, title-case term or phrase that clearly describes the result of the action. Donโ€™t include your app name or any extraneous information in the button label, keep the text brief to avoid truncation, and take localization into account as you write it. + +**Avoid providing an action that merely opens your app.** When people tap a notification or its preview, they expect your app to display related content, so presenting an action button that does the same thing clutters the detail view and can be confusing. + +**Prefer nondestructive actions.** If you must provide a destructive action, make sure people have enough context to avoid unintended consequences. The system gives a distinct appearance to the actions you identify as destructive. + +**Provide a simple, recognizable interface icon for each notification action.** An interface icon reinforces an actionโ€™s meaning, helping people instantly understand what it does. The system displays your interface icon on the trailing side of the action title. When you use [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols), you can choose an existing symbol that represents your command or edit a related symbol to create a custom icon. + +## [Badging](https://developer.apple.com/design/human-interface-guidelines/notifications#Badging) + +A badge is a small, filled oval containing a number that can appear on an app icon to indicate the number of unread notifications that are available. After people address unread notifications, the badge disappears from the app icon, reappearing when new notifications arrive. People can choose whether to allow an app to display badges in their notification settings. + +**Use a badge only to show people how many unread notifications they have.** Donโ€™t use a badge to convey numeric information that isnโ€™t related to notifications, such as weather-related data, dates and times, stock prices, or game scores. + +**Make sure badging isnโ€™t the only method you use to communicate essential information.** People can turn off badging for your app, so if you rely on it to show people when thereโ€™s important information, people can miss the message. Always make sure that you make important information easy for people to find as soon as they open your app. + +**Keep badges up to date.** Update your appโ€™s badge as soon as people open the corresponding notifications. You donโ€™t want people to think there are new notifications available, only to find that theyโ€™ve already viewed them all. Note that reducing a badgeโ€™s count to zero removes all related notifications from Notification Center. + +**Avoid creating a custom image or component that mimics the appearance or behavior of a badge.** People can turn off notification badges if they choose, and will become frustrated if they have done so and then see what appears to be a badge. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/notifications#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, or visionOS._ + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/notifications#watchOS) + +On Apple Watch, notifications occur in two stages: _short look_ and _long look_. People can also view notifications in Notification Center. On supported devices, people can double-tap to respond to notifications. + +You can help people have a great notification experience by designing app-specific assets and actions that are relevant on Apple Watch. If your watchOS app has an iPhone companion that supports notifications, watchOS can automatically provide default short-look and long-look interfaces if necessary. + +#### [Short looks](https://developer.apple.com/design/human-interface-guidelines/notifications#Short-looks) + +A short look appears when the wearerโ€™s wrist is raised and disappears when itโ€™s lowered. + +![An illustration that represents a short look notification from a generic app. It includes a large primary image in the center, a title, and a short preview of the notification content.](https://docs-assets.developer.apple.com/published/609f4ae816d78b5c87a340416f4874a9/notifications-short-looks%402x.png) + +**Avoid using a short look as the only way to communicate important information.** A short look appears only briefly, giving people just enough time to see what the notification is about and which app sent it. If your notification information is critical, make sure you deliver it in other ways, too. + +**Keep privacy in mind.** Short looks are intended to be discreet, so itโ€™s important to provide only basic information. Avoid including potentially sensitive information in the notificationโ€™s title. + +#### [Long looks](https://developer.apple.com/design/human-interface-guidelines/notifications#Long-looks) + +Long looks provide more detail about a notification. If necessary, people can swipe vertically or use the Digital Crown to scroll a long look. After viewing a long look, people can dismiss it by tapping it or simply by lowering their wrist. + +![An illustration that represents a long look notification from a generic app. It includes a small primary image in the upper left corner, badging a platter with the notification title and content. Beneath the notification are two full width action buttons, the second of which extends off the screen to indicate that the view is scrollable.](https://docs-assets.developer.apple.com/published/a48f434c960e0f14429018803ee4b180/notifications-long-looks%402x.png) + +A custom long-look interface can be static or dynamic. The _static_ interface lets you display a notificationโ€™s message along with additional static text and images. The _dynamic_ interface gives you access to the notificationโ€™s full content and offers more options for configuring the appearance of the interface. + +You can customize the content area for both static and dynamic long looks, but you canโ€™t change the overall structure of the interface. The system-defined structure includes a _sash_ at the top of the interface and a Dismiss button at the bottom, below all custom buttons. + +**Consider using a rich, custom long-look notification to let people get the information they need without launching your app.** You can use SwiftUI [Animations](https://developer.apple.com/documentation/SwiftUI/Animations) to create engaging, interruptible animations; alternatively, you can use [SpriteKit](https://developer.apple.com/documentation/SpriteKit) or [SceneKit](https://developer.apple.com/documentation/SceneKit). + +**At the minimum, provide a static interface; prefer providing a dynamic interface too.** The system defaults to the static interface when the dynamic interface is unavailable, such as when there is no network or the iPhone companion app is unreachable. Be sure to create the resources for your static interface in advance and package them with your app. + +**Choose a background appearance for the sash.** The system-provided sash, at the top of the long-look interface, displays your app icon and name. You can customize the sashโ€™s color or give it a blurred appearance. If you display a photo at the top of the content area, youโ€™ll probably want to use the blurred sash, which has a light, translucent appearance that gives the illusion of overlapping the image. + +**Choose a background color for the content area.** By default, the long lookโ€™s background is transparent. If you want to match the background color of other system notifications, use white with 18% opacity; otherwise, you can use a custom color, such as a color within your brandโ€™s palette. + +**Provide up to four custom actions below the content area.** For each long look, the system uses the notificationโ€™s type to determine which of your custom actions to display as buttons in the notification UI. In addition, the system always displays a Dismiss button at the bottom of the long-look interface, below all custom buttons. If your watchOS app has an iPhone companion that supports notifications, the system shares the actionable notification types already registered by your iPhone app and uses them to configure your custom action buttons. + +#### [Double tap](https://developer.apple.com/design/human-interface-guidelines/notifications#Double-tap) + +People can double-tap to respond to notifications on supported devices. When a person responds to a notification with a double tap, the system selects the first nondestructive action as the response. + +**Keep double tap in mind when choosing the order of custom actions you present as responses to a notification.** Because a double tap runs the first nondestructive action, consider placing the action that people use most frequently at the top of the list. For example, a parking app that provides custom actions for extending the time on a paid parking spot could offer options to extend the time by 5 minutes, 15 minutes, or an hour, with the most common choice listed first. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/notifications#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/notifications#Related) + +[Managing notifications](https://developer.apple.com/design/human-interface-guidelines/managing-notifications) + +[Alerts](https://developer.apple.com/design/human-interface-guidelines/alerts) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/notifications#Developer-documentation) + +[Asking permission to use notifications](https://developer.apple.com/documentation/UserNotifications/asking-permission-to-use-notifications) โ€” User Notifications + +[User Notifications UI](https://developer.apple.com/documentation/UserNotificationsUI) + +[User Notifications](https://developer.apple.com/documentation/UserNotifications) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/notifications#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/B63A08EA-8856-4C77-9E1B-EA1CAD990619/4986_wide_250x141_1x.jpg) Send communication and Time Sensitive notifications ](https://developer.apple.com/videos/play/wwdc2021/10091) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/49/3D8237BC-06E3-4711-8552-7008A5D5BAAD/3764_wide_250x141_1x.jpg) The Push Notifications primer ](https://developer.apple.com/videos/play/wwdc2020/10095) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/notifications#Change-log) + +Date| Changes +---|--- +October 24, 2023| Updated watchOS platform considerations with guidance for presenting notification responses to double tap. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/top-shelf.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/top-shelf.md new file mode 100644 index 00000000..d835a2fa --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/top-shelf.md @@ -0,0 +1,135 @@ +--- +title: "Top Shelf | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/top-shelf + +# Top Shelf + +The Apple TV Home Screen provides an area called Top Shelf, which showcases your content in a rich, engaging way while also giving people access to their favorite apps in the Dock. + +![A stylized representation of a horizontal list of media previews above rows of Apple TV apps. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/73359c0fed7c9f8fd20b9a2c03ebfe66/components-top-shelf-intro%402x.png) + +When you support full-screen Top Shelf, people can swipe through multiple full-screen content views, play trailers and previews, and get more information about your content. + +Top Shelf is a unique opportunity to highlight new, featured, or recommended content and let people jump directly to your app or game to view it. For example, when people select Apple TV in the Dock, full-screen previews immediately begin playing and soon the Dock slides away. As people watch previews for the first show, they can swipe through previews of all other featured shows, stopping to select Play or More Info for a preview that interests them. + +The system defines several layout templates that you can use to give people a compelling Top Shelf experience when they select your app in the Dock. To help you position content, you can download these templates from [Apple Design Resources](https://developer.apple.com/design/resources/#tvos-apps). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/top-shelf#Best-practices) + +**Help people jump right into your content.** Top Shelf provides a path to the content people care about most. Two of the system-provided layout templates โ€” [carousel actions](https://developer.apple.com/design/human-interface-guidelines/top-shelf#Carousel-actions) and [carousel details](https://developer.apple.com/design/human-interface-guidelines/top-shelf#Carousel-details) โ€” each include two buttons by default: A primary button intended to begin playback and a More Info button intended to open your app to a view that displays details about the content. + +**Feature new content.** For example, showcase new releases or episodes, highlight upcoming movies and shows, and avoid promoting content that people have already purchased, rented, or watched. + +**Personalize peopleโ€™s favorite content.** People typically put the apps they use most often into Top Shelf. You can personalize their experience by showing targeted recommendations in the Top Shelf content you supply, letting people resume media playback or jump back into active gameplay. + +**Avoid showing advertisements or prices.** People put your app into Top Shelf because youโ€™ve already sold them on it, so they may not appreciate seeing lots of ads from your app. Showing purchasable content in the Top Shelf is fine, but prefer putting the focus on new and exciting content, and consider displaying prices only when people show interest. + +**Showcase compelling dynamic content that can help draw people in and encourage them to view more.** If necessary, you can supply static images, but people typically prefer a captivating, dynamic Top Shelf experience that features the newest or highest rated content. To provide this experience, prefer creating [layered images](https://developer.apple.com/design/human-interface-guidelines/images#Layered-images) to display in Top Shelf. + +**If you donโ€™t provide the recommended full-screen content, supply at least one static image as a fallback.** The system displays a static image when your app is in the Dock and in focus and full-screen content is unavailable. tvOS flips and blurs the image, ensuring that it fits into a width of 1920 pixels at the 16:9 aspect ratio. Use the following values for guidance. + +Image size +--- +2320x720 pt (2320x720 px @1x, 4640x1440 px @2x) + +**Avoid implying interactivity in a static image.** A static Top Shelf image isnโ€™t focusable, and you donโ€™t want to make people think itโ€™s interactive. + +## [Dynamic layouts](https://developer.apple.com/design/human-interface-guidelines/top-shelf#Dynamic-layouts) + +Dynamic Top Shelf images can appear in several ways: + + * A carousel of full-screen video and images that includes two buttons and optional details + + * A row of focusable content + + * A set of scrolling banners + + + + +### [Carousel actions](https://developer.apple.com/design/human-interface-guidelines/top-shelf#Carousel-actions) + +The carousel actions layout style focuses on full-screen video and images and includes a few unobtrusive controls that help people see more. This layout style works especially well to showcase content that people already know something about. For example, itโ€™s great for displaying user-generated content, like photos, or new content from a franchise or show that people are likely to enjoy. + +**Provide a title.** Include a succinct title, like the title of the show or movie or the title of a photo album. If necessary, you can also provide a brief subtitle. For example, a subtitle for a photo album could be a range of dates; a subtitle for an episode could be the name of the show. + +### [Carousel details](https://developer.apple.com/design/human-interface-guidelines/top-shelf#Carousel-details) + +This layout style extends the carousel actions layout style, giving you the opportunity to include some information about the content. For example, you might provide information like a plot summary, cast list, and other metadata that helps people decide whether to choose the content. + +**Provide a title that identifies the currently playing content.** The content title appears near the top of the screen so itโ€™s easy for people to read it at a glance. Above the title, you can also provide a succinct phrase or app attribution, like โ€œFeatured on _My App_.โ€ + +### [Sectioned content row](https://developer.apple.com/design/human-interface-guidelines/top-shelf#Sectioned-content-row) + +This layout style shows a single labeled row of sectioned content, which can work well to highlight recently viewed content, new content, or favorites. Row content is focusable, which lets people scroll quickly through it. A label appears when an individual piece of content comes into focus, and small movements on the remoteโ€™s Touch surface bring the focused image to life. You can also configure a sectioned content row to show multiple labels. + +**Provide enough content to constitute a complete row.** At a minimum, load enough images in a sectioned content row to span the full width of the screen. In addition, include at least one label for greater platform consistency and to provide additional context. + +You can use the following image sizes in a sectioned content row. + +#### [Poster (2:3)](https://developer.apple.com/design/human-interface-guidelines/top-shelf#Poster-23) + +![An illustration showing an outlined rectangle that contains a slightly smaller rectangle, which contains a slight narrower rectangle. The outermost rectangle represents the actual size, the middle rectangle represents the visible or safe zone, and the innermost rectangle represents the unfocused size.](https://docs-assets.developer.apple.com/published/13d162f243a286d45bb107b4a7cb799b/icons-and-images-content-layout-2x3%402x.png) + +Aspect| Image size +---|--- +Actual size| 404x608 pt (404x608 px @1x, 808x1216 px @2x) +Focused/Safe zone size| 380x570 pt (380x570 px @1x, 760x1140 px @2x) +Unfocused size| 333x570 pt (333x570 px @1x, 666x1140 px @2x) + +#### [Square (1:1)](https://developer.apple.com/design/human-interface-guidelines/top-shelf#Square-11) + +![An illustration showing an outlined square that contains a slightly smaller square, which contains a slightly smaller square. The outermost square represents the actual size, the middle square represents the visible or safe zone, and the innermost square represents the unfocused size.](https://docs-assets.developer.apple.com/published/eba53b409d9ed6226c9b1e965dc17033/icons-and-images-content-layout-1x1%402x.png) + +Aspect| Image size +---|--- +Actual size| 608x608 pt (608x608 px @1x, 1216x1216 px @2x) +Focused/Safe zone size| 570x570 pt (570x570 px @1x, 1140x1140 px @2x) +Unfocused size| 500x500 pt (500x500 px @1x, 1000x1000 px @2x) + +#### [16:9](https://developer.apple.com/design/human-interface-guidelines/top-shelf#169) + +![An illustration showing an outlined rectangle that contains a slightly smaller rectangle, which contains a slightly smaller rectangle. The outermost rectangle represents the actual size, the middle rectangle represents the visible or safe zone, and the innermost rectangle represents the unfocused size.](https://docs-assets.developer.apple.com/published/2b45c73a75fdeafef21cbb3c2923259a/icons-and-images-content-layout-16x9%402x.png) + +Aspect| Image size +---|--- +Actual size| 908x512 pt (908x512 px @1x, 1816x1024 px @2x) +Focused/Safe zone size| 852x479 pt (852x479 px @1x, 1704x958 px @2x) +Unfocused size| 782x440 pt (782x440 px @1x, 1564x880 px @2x) + +**Be aware of additional scaling when combining image sizes.** If your Top Shelf design includes a mixture of image sizes, keep in mind that images will automatically scale up to match the height of the tallest image if necessary. For example, a 16:9 image scales to 500 pixels high if included in a row with a poster or square image. + +#### [Scrolling inset banner](https://developer.apple.com/design/human-interface-guidelines/top-shelf#Scrolling-inset-banner) + +This layout shows a series of large images, each of which spans almost the entire width of the screen. Apple TV automatically scrolls through these banners on a preset timer until people bring one into focus. The sequence circles back to the beginning after the final image is reached. + +When a banner is in focus, a small, circular gesture on the remoteโ€™s Touch surface enacts the system focus effect, animating the item, applying lighting effects, and, if the banner contains layered images, producing a 3D effect. Swiping on the Touch surface pans to the next or previous banner in the sequence. Use this style to showcase rich, captivating content, such as a popular new movie. + +**Provide three to eight images.** A minimum of three images is recommended for a scrolling banner to feel effective. More than eight images can make it hard to navigate to a specific image. + +**If you need text, add it to your image.** This layout style doesnโ€™t show labels under content, so all text must be part of the image itself. In layered images, consider elevating text by placing it on a dedicated layer above the others. Add the text to the accessibility label of the image too, so [VoiceOver](https://developer.apple.com/design/human-interface-guidelines/voiceover) can read it. + +![An illustration showing a wide rectangle that contains of a smaller rectangle, which contains a slightly narrower rectangle. The outermost rectangle represents the actual size, the middle rectangle represents the visible or safe zone, and the innermost rectangle represents the unfocused size.](https://docs-assets.developer.apple.com/published/e7ca852b559e9d981c968703dbad0315/icons-and-images-content-layout-extra-wide%402x.png) + +Use the following size for a scrolling inset banner image: + +Aspect| Image size +---|--- +Actual size| 1940x692 pt (1940x692 px @1x, 3880x1384 px @2x) +Focused/Safe zone size| 1740x620 pt (1740x620 px @1x, 3480x1240 px @2x) +Unfocused size| 1740x560 pt (1740x560 px @1x, 3480x1120 px @2x) + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/top-shelf#Platform-considerations) + + _Not supported in iOS, iPadOS, macOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/top-shelf#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/top-shelf#Related) + +[Apple Design Resources](https://developer.apple.com/design/resources/#tvos-apps) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/top-shelf#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/48/21CB7C2D-31A3-4DE5-A0EE-58FE214031F0/2713_wide_250x141_1x.jpg) Mastering the Living Room With tvOS ](https://developer.apple.com/videos/play/wwdc2019/211) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/watch-faces.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/watch-faces.md new file mode 100644 index 00000000..75b39ac3 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/watch-faces.md @@ -0,0 +1,40 @@ +--- +title: "Watch faces | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/watch-faces + +# Watch faces + +A watch face is a view that people choose as their primary view in watchOS. + +![A stylized representation of a series of Apple Watch faces. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/bdf9c3962f71506a149c0ee448c2a0ad/components-faces-intro%402x.png) + +The watch face is at the heart of the watchOS experience. People choose a watch face they want to see every time they raise their wrist, and they customize it with their favorite complications. People can even customize different watch faces for different activities, so they can switch to the watch face that fits their current context. + +In watchOS 7 and later, people can share the watch faces they configure. For example, a fitness instructor might configure a watch face to share with their students by choosing the Gradient watch face, customizing the color, and adding their favorite health and fitness complications. When the students add the shared watch face to their Apple Watch or the Watch app on their iPhone, they get a custom experience without having to configure it themselves. + +You can also configure a watch face to share from within your app, on your website, or through Messages, Mail, or social media. Offering shareable watch faces can help you introduce more people to your complications and your app. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/watch-faces#Best-practices) + +**Help people discover your app by sharing watch faces that feature your complications.** Ideally, you support multiple complications so that you can showcase them in a shareable watch face and provide a curated experience. For some watch faces, you can also specify a system accent color, images, or styles. If people add your watch face but havenโ€™t installed your app, the system prompts them to install it. + +**Display a preview of each watch face you share.** Displaying a preview that highlights the advantages of your watch face can help people visualize its benefits. You can get a preview by using the iOS Watch app to email the watch face to yourself. The preview includes an illustrated device bezel that frames the face and is suitable for display on websites and in watchOS and iOS apps. Alternatively, you can replace the illustrated bezel with a high-fidelity hardware bezel that you can download from [Apple Design Resources](https://developer.apple.com/design/resources/#product-bezels) and composite onto the preview. For developer guidance, see [Sharing an Apple Watch face](https://developer.apple.com/documentation/ClockKit/sharing-an-apple-watch-face). + +**Aim to offer shareable watch faces for all Apple Watch devices.** Some watch faces are available on Series 4 and later โ€” such as California, Chronograph Pro, Gradient, Infograph, Infograph Modular, Meridian, Modular Compact, and Solar Dial โ€” and Explorer is available on Series 3 (with cellular) and later. If you use one of these faces in your configuration, consider offering a similar configuration using a face thatโ€™s available on Series 3 and earlier. To help people make a choice, you can clearly label each shareable watch face with the devices it supports. + +**Respond gracefully if people choose an incompatible watch face.** The system sends your app an error when people try to use an incompatible watch face on Series 3 or earlier. In this scenario, consider immediately offering an alternative configuration that uses a compatible face instead of displaying an error. Along with the previews you provide, help people understand that they might receive an alternative watch face if they choose a face that isnโ€™t compatible with their Apple Watch. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/watch-faces#Platform-considerations) + + _Not supported in iOS, iPadOS, macOS, tvOS, or visionOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/watch-faces#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/watch-faces#Related) + +[Apple Design Resources โ€” Product Bezels](https://developer.apple.com/design/resources/#product-bezels) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/watch-faces#Developer-documentation) + +[Sharing an Apple Watch face](https://developer.apple.com/documentation/ClockKit/sharing-an-apple-watch-face) โ€” ClockKit + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/widgets.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/widgets.md new file mode 100644 index 00000000..09384383 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-components-system/references/widgets.md @@ -0,0 +1,517 @@ +--- +title: "Widgets | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/widgets + +# Widgets + +A widget provides quick access to essential information and focused interactions from your app or game in additional contexts. + +![A stylized representation of a set of different-sized widgets on an iPad Home Screen. The image is tinted red to subtly reflect the red in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/1d1cd14d565d4afe6ecd373b26d9ffc5/components-widgets-intro%402x.png) + +Widgets help people organize and personalize their devices by displaying timely, glanceable content and offering specific functionality. They appear in various contexts for a consistent experience across platforms. For example, a person might place a Weather widget: + + * On the Home Screen and Lock Screen of their iPhone and iPad + + * On the desktop and Notification Center of their Mac + + * On a horizontal or vertical surface when they wear Apple Vision Pro + + * At a fixed position in the Smart Stack of Apple Watch + + + + +## [Anatomy](https://developer.apple.com/design/human-interface-guidelines/widgets#Anatomy) + +Widgets come in different sizes, ranging from small accessory widgets on iPhone, iPad, and Apple Watch to system family widgets that include an extra large size on iPad, Mac, and Apple Vision Pro. Additionally, widgets adapt their appearance to the context in which they appear and respond to a personโ€™s device customization. Consider the following aspects when you design widgets: + + * The widget size to support + + * The context โ€” devices and system experiences โ€” in which the widget may appear + + * The rendering modes and color treatment that the widget receives based on the size and context + + + + +The WidgetKit framework provides default appearances and treatments for each widget size to fit the system experience or device where it appears. However, itโ€™s important to consider creating a custom widget design that can provide the best experience for your content in each specific context. + +### [System family widgets](https://developer.apple.com/design/human-interface-guidelines/widgets#System-family-widgets) + +System family widgets offer a broad range of sizes and may include one or more interactive elements. + + * Small + * Medium + * Large + * Extra large + * Extra large portrait + + + +![An image of the small Calendar widget, showing only the current date and one event.](https://docs-assets.developer.apple.com/published/0089454df2b0b32f6ca892f3131bdb01/widgets-calendar-small%402x.png) + +![An image of the medium Calendar widget, which has the same height as the small widget, but twice the width. On the left, the widget repeats the small widgetโ€™s information, adding a birthday event; on the right the medium widget adds information about tomorrowโ€™s events.](https://docs-assets.developer.apple.com/published/a29b600e231c019ccc0fd855603ac1be/widgets-calendar-medium%402x.png) + +![An image of the large Calendar widget, which has the same width as the medium widget, but a little more than twice the height. Like the medium widget, the large widget displays information about today on the left and information about tomorrow on the right. On both sides, the large version includes relevant time-of-day indicators.](https://docs-assets.developer.apple.com/published/d2a3a81b0ccbc123bb61e12b7da6b1ed/widgets-calendar-large%402x.png) + +![An image of the extra large Calendar widget, which is about twice the width of the large widget and a little shorter. The widget displays information about today and the following three days, including relevant time-of-day indicators in all four columns. The today column is a little wider than the columns for the other days.](https://docs-assets.developer.apple.com/published/f2738320bf945e097a8d16f35ca8d9c1/widgets-calendar-extra-large%402x.png) + +![An image of the extra large portrait Music widget, which has the same width as the large widget but is taller.](https://docs-assets.developer.apple.com/published/4ff038f07eaedfa7579dfa7e0ef38099/widgets-music-extra-large-portrait%402x.png) + +The following table shows supported contexts for each system family widget size: + +Widget size| iPhone| iPad| Mac| Apple Vision Pro +---|---|---|---|--- +System small| Home Screen, Today View, StandBy, and CarPlay| Home Screen, Today View, and Lock Screen| Desktop and Notification Center| Horizontal and vertical surfaces +System medium| Home Screen and Today View| Home Screen and Today View| Desktop and Notification Center| Horizontal and vertical surfaces +System large| Home Screen and Today View| Home Screen and Today View| Desktop and Notification Center| Horizontal and vertical surfaces +System extra large| Not supported| Home Screen and Today View| Desktop and Notification Center| Horizontal and vertical surfaces +System extra large portrait| Not supported| Not supported| Not supported| Horizontal and vertical surfaces + +### [Accessory widgets](https://developer.apple.com/design/human-interface-guidelines/widgets#Accessory-widgets) + +Accessory widgets display a very limited amount of information because of their size. + + * Accessory circular + * Accessory corner + * Accessory inline + * Accessory rectangular + + + +![An image of the circular accessory Calendar widget, showing only the time for the next event.](https://docs-assets.developer.apple.com/published/8f496e2dce59da3b8607cb07e4c4215c/widgets-accessory-calendar-circular%402x.png) + +![An image of the corner accessory Calendar widget. It displays the time and title of an upcoming meeting.](https://docs-assets.developer.apple.com/published/ee66466be7aa2d47efc551196156b8fa/widgets-accessory-corner-widget-watch%402x.png) + +![An image of the inline accessory Calendar widget, showing the date and the number of the next events.](https://docs-assets.developer.apple.com/published/b9af2a21096e381b9e3734e0119b7b09/widgets-accessory-calendar-inline%402x.png) + +![An image of the rectangular accessory Calendar widget. It displays the time of two simultaneous events and their titles.](https://docs-assets.developer.apple.com/published/8709b464372ab0810be34a951879ceec/widgets-accessory-calendar-rectangular%402x.png) + +They appear on the following devices: + +Widget size| iPhone| iPad| Apple Watch +---|---|---|--- +Accessory circular| Lock Screen| Lock Screen| Watch complications and in the Smart Stack +Accessory corner| Not supported| Not supported| Watch complications +Accessory inline| Lock Screen| Lock Screen| Watch complications +Accessory rectangular| Lock Screen| Lock Screen| Watch complications and in the Smart Stack + +### [Appearances](https://developer.apple.com/design/human-interface-guidelines/widgets#Appearances) + +A widget can appear in full-color, in monochrome with a tint color, or in a clear, translucent style. Depending on the location, device, and a personโ€™s customization, the system may apply a tinted or clear appearance to the widget and its included full-color images, symbols, and glyphs. + +For example, a small system widget appears differently depending on the device and location: + + * On the Home Screen of iPhone and iPad, people choose from different appearances for widgets: light, dark, clear, and tinted. In light and dark appearances, widgets have a full-color design. In a clear appearance, the system desaturates the widget and adds translucency, highlights, and the Liquid Glass material. In a tinted appearance, the system desaturates the widget and its content, then applies a personโ€™s selected tint color. + + + + +![An image of the small Stocks widget on the Home Screen in the full-color appearance.](https://docs-assets.developer.apple.com/published/c001f107005e17afb9e12d48d162dcb2/widgets-stocks-default%402x.png)Full-color + +![An image of the small Stocks widget on the Home Screen in the clear appearance.](https://docs-assets.developer.apple.com/published/ad7a87eea7f2be4863cf6a2e8d8a7639/widgets-stocks-clear%402x.png)Clear + +![An image of the small Stocks widget on the Home Screen in the tinted appearance.](https://docs-assets.developer.apple.com/published/25e58f2c47d8b6d863187721d5c4abe9/widgets-stocks-tinted%402x.png)Tinted + + * On Apple Vision Pro, the widget appears as a 3D object, surrounded by a frame. It takes on a full-color appearance with a glass- or paper-like coating layer that responds to lighting conditions. Additionally, people can choose a tinted appearance that applies a color from a set of system-provided color palettes. + + + + +![An image of the small Stocks widget on Apple Vision Pro.](https://docs-assets.developer.apple.com/published/4b4e42b658c77b47c0f40d4d433d7b3b/widgets-stocks-visionos-frame%402x.png) + + * On the Lock Screen of iPad, the widget takes on a monochromatic appearance without a tint color. + + + + +![An image of the small Stocks widget on the Lock Screen, showing the price of Apple stock.](https://docs-assets.developer.apple.com/published/58aef111a6a92a03981f5998720bca48/widgets-stocks-ipad-lock-screen%402x.png) + + * On the Lock Screen of iPhone in StandBy, the widget appears scaled up in size with the background removed. When the ambient light falls below a threshold, the system renders the widget with a monochromatic red tint. + + + + +![An image of the Stocks widget on the Lock Screen in StandBy, showing the price of Apple stock.](https://docs-assets.developer.apple.com/published/cc1129210235e90f1ee9045eaf85dfb9/widgets-stocks-standby%402x.png)StandBy + +![An image of the Stocks widget on the Lock Screen that uses the dark appearance in low-light conditions, showing the price of Apple stock.](https://docs-assets.developer.apple.com/published/eb666a28d966abb92b34bdb0d14d8e0b/widgets-stocks-standby-night-mode%402x.png)iPhone in StandBy during low-light conditions + +Similarly, a rectangular accessory widget appears as follows: + + * On the Lock Screen of iPhone and iPad, it takes on a monochromatic appearance without a tint color. + + * On Apple Watch, the widget can appear as a watch complication in both full-color and tinted appearances, and it can also appear in the Smart Stack. + + + + + * iPhone Lock Screen + * Watch complication + * Smart Stack on Apple Watch + + + +![A rectangular accessory Calendar widget on the Lock Screen of iPhone, displaying a team meeting at 4 P.M. in a conference room.](https://docs-assets.developer.apple.com/published/1bf7a8ed9890752d8aac424f5406e978/widgets-calendar-rectangular-ios%402x.png) + +![A rectangular Calendar watch complication, displaying a team meeting at 4 P.M. in a conference room.](https://docs-assets.developer.apple.com/published/817c517d1183f2d8a797d7b304cefab3/widgets-calendar-rectangular-watchos-complication%402x.png) + +![A Calendar widget in the Smart Stack on Apple Watch. It displays a team meeting at 4 P.M. in a conference room.](https://docs-assets.developer.apple.com/published/a2d56d27563c849f1a5bc6b24406c59d/widgets-calendar-rectangular-watchos-smart-stack%402x.png) + +Each appearance described above includes a [rendering mode](https://developer.apple.com/design/human-interface-guidelines/widgets#Rendering-modes) that depends on the platform and a personโ€™s appearance settings: + + * The system uses the [full color](https://developer.apple.com/documentation/WidgetKit/WidgetRenderingMode/fullColor) rendering mode for system family widgets across all platforms to display your widget in full color. It doesnโ€™t change the color of your views. + + * The system uses the [accented](https://developer.apple.com/documentation/WidgetKit/WidgetRenderingMode/accented) rendering mode for system family widgets across all platforms and for accessory widgets on Apple Watch. In the accented rendering mode, the system removes the background and replaces it with a tinted color effect for a tinted appearance and a Liquid Glass background for a clear appearance. Additionally, it divides the widgetโ€™s views into an accent group and a primary group, and then applies a solid color to each group. + + * The system uses the [vibrant](https://developer.apple.com/documentation/WidgetKit/WidgetRenderingMode/vibrant) rendering mode for widgets on the Lock Screen of iPhone and iPad, and on iPhone in StandBy in low-light conditions. It desaturates text, images, and gauges, and creates a vibrant effect by coloring your content appropriately for the Lock Screen background or a macOS desktop. Note that people can customize the Lock Screen with a tint color, and the system applies a red tint for widgets that appear on iPhone in StandBy in low-light conditions. + + + + +The following table lists the occurrences for each rendering mode per platform: + +Platform| Full-color| Accented| Vibrant +---|---|---|--- +iPhone| Home Screen, Today view, StandBy and CarPlay (with the background removed)| Home Screen and Today view| Lock Screen, StandBy in low-light conditions +iPad| Home Screen and Today view| Home Screen and Today view| Lock Screen +Apple Watch| Smart Stack, complications| Smart Stack, complications| Not supported +Mac| Desktop and Notification Center| Not supported| Desktop +Apple Vision Pro| Horizontal and vertical surfaces| Horizontal and vertical surfaces| Not supported + +For additional design guidance, see [Rendering modes](https://developer.apple.com/design/human-interface-guidelines/widgets#Rendering-modes). For developer guidance, see [Preparing widgets for additional platforms, contexts, and appearances](https://developer.apple.com/documentation/WidgetKit/Preparing-widgets-for-additional-contexts-and-appearances) and [`WidgetRenderingMode`](https://developer.apple.com/documentation/WidgetKit/WidgetRenderingMode). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/widgets#Best-practices) + +**Choose simple ideas that relate to your appโ€™s main purpose.** Include timely content and relevant functionality. For example, people who use the Weather app are often most interested in the current high and low temperatures and weather conditions, so the Weather widgets prioritize this information. + +![An image of a small Weather widget showing current conditions for Cupertino. In text, the widget displays a temperature of 70 degrees, the condition Sunny, and forecast high and low temperatures of 75 degrees and 59 degrees, respectively. The widget also displays a yellow sun symbol above the word Sunny and the filled-in location indicator to the right of the word Cupertino.](https://docs-assets.developer.apple.com/published/6a3718b9d1eb04c89acbf3c32cf9101e/widgets-ios-weather-small%402x.png) + +**Aim to create a widget that gives people quick access to the content they want.** People appreciate widgets that display meaningful content and offer useful actions and deep links to key areas of your app. Replicating an app icon offers little additional value, and people may be less likely to keep it on their screens. + +**Prefer dynamic information that changes throughout the day.** If a widgetโ€™s content never appears to change, people may not keep it in a prominent position. Although widgets donโ€™t update from minute to minute, itโ€™s important to find ways to keep their content fresh to invite frequent viewing. + +**Look for opportunities to surprise and delight.** For example, you might design a unique visual treatment for your calendar widget to display on meaningful occasions, like birthdays or holidays. + +**Offer widgets in multiple sizes when doing so adds value.** Small widgets use their limited space to typically show a single piece of information while larger sizes support additional layers of information and actions. Avoid expanding a smaller widgetโ€™s content to simply fill a larger area. Itโ€™s more important to create one widget in the size that best represents the content than providing the widget in all sizes. + +**Balance information density.** Sparse layouts can make the widget seem unnecessary, while overly dense layouts are less glanceable. Create a layout that provides essential information at a glance and allows people to view additional details by taking a longer look. If your layout is too dense, consider improving its clarity by using a larger widget size or replacing text with graphics. + +**Display only the information thatโ€™s directly related to the widgetโ€™s main purpose.** In larger widgets, you can display more data โ€” or more detailed visualizations of the data โ€” but you donโ€™t want to lose sight of the widgetโ€™s primary purpose. For example, all Calendar widgets display a personโ€™s upcoming events. In each size, the widget remains centered on events while expanding the range of information as the size increases. + +**Use brand elements thoughtfully.** Incorporate brand colors, typefaces, and stylized glyphs to make your widget recognizable but donโ€™t overpower useful information or make your widget look out of place. When you include brand elements, people seldom need your logo or app icon to help them recognize your widget. If your widget benefits from including a small logo โ€” for example, if your widget displays content from multiple sources โ€” a small logo in the top-right corner is sufficient. + +**Choose between automatically displaying content and letting people customize displayed information.** In some cases, people need to configure a widget to ensure it displays the information thatโ€™s most useful for them. For example, the Stocks widget lets people select the stocks they wish to track. In contrast, some widgets โ€” like the Podcasts widget โ€” automatically display recent content, so people donโ€™t need to customize them. For developer guidance, see [Making a configurable widget](https://developer.apple.com/documentation/WidgetKit/Making-a-Configurable-Widget). + +**Avoid mirroring your widgetโ€™s appearance within your app.** Including an element in your app that looks like your widget but doesnโ€™t behave like it can confuse people. Additionally, people may be less likely to try other ways to interact with such an element in your app because they expect it to behave like a widget. + +**Let people know when authentication adds value.** If your widget provides additional functionality when someone is signed in to your app, make sure people know that. For example, an app that shows upcoming reservations might include a message like โ€œSign in to view reservationsโ€ when people are signed out. + +### [Updating widget content](https://developer.apple.com/design/human-interface-guidelines/widgets#Updating-widget-content) + +To remain relevant and useful, widgets periodically refresh their information but donโ€™t support continuous, real-time updates, and the system may adjust the limits for updates depending on various factors. + +**Keep your widget up to date.** Finding the appropriate update frequency for your widget depends on knowing how often the data changes and estimating when people need to see new data. For example, a widget that provides information about tidal conditions at a beach is useful if it updates on an hourly basis even though conditions change constantly. If people are likely to check your widget more frequently than you can update it, consider displaying text that describes when the data was last updated. + +**Use system functionality to refresh dates and times in your widget.** Because widget update frequency is limited, let the system automatically refresh date and time information to preserve update opportunities. Determine the update frequency that fits with the data you display and show content quickly without hiding stale data behind placeholder content. For developer guidance about widget updates, see [Keeping a widget up to date](https://developer.apple.com/documentation/WidgetKit/Keeping-a-Widget-Up-To-Date). + +**Use animated transitions to bring attention to data updates.** By default, many SwiftUI views animate content updates. Additionally, use standard and custom animations with a duration of up to two seconds to let people know when new information is available or when content displays differently. For developer guidance, see [Animating data updates in widgets and Live Activities](https://developer.apple.com/documentation/WidgetKit/Animating-data-updates-in-widgets-and-live-activities). + +### [Adding interactivity](https://developer.apple.com/design/human-interface-guidelines/widgets#Adding-interactivity) + +People tap or click a widget to launch its corresponding app. It can also include buttons and toggles to offer additional functionality without launching the app. For example, the Reminders widget includes toggles to mark a task as completed. When people interact with your widget in areas that arenโ€™t buttons or toggles, the interaction launches your app. + +![An image of the large Reminders widget with a toggle for each task. None of the tasks is complete.](https://docs-assets.developer.apple.com/published/71a7227a45af08e53dccfcc4b9d9ffe4/widgets-reminders-large-unselected%402x.png)Incomplete tasks + +![An image of the large Reminders widget with a toggle for each task. The toggles for the first and third items in the list indicate that these tasks are complete.](https://docs-assets.developer.apple.com/published/6d2286d6fb3ded274a98ce9c2108f59f/widgets-reminders-large-selected%402x.png)Completed tasks + +**Offer simple, relevant functionality and reserve complexity for your app.** Useful widgets offer an easy way to complete a task or action thatโ€™s directly related to its content. + +**Ensure that a widget interaction opens your app at the right location.** Deep link to details and actions that directly relate to the widgetโ€™s content, and donโ€™t make people navigate to the relevant area in the app. For example, when people click or tap a medium Stocks widget, the Stocks app opens to a page that displays information about the symbol. + +![An image of a medium Stocks watchlist widget, listing two stock market indices and one stock symbol. Each row displays the index or symbol name on the left, a graph section in the middle, and a current quote, including a value change, on the right.](https://docs-assets.developer.apple.com/published/bfe482d5903ff332d0027450f18a6a43/widgets-stocks-medium%402x.png) + +**Offer interactivity while remaining glanceable and uncluttered.** Multiple interaction targets โ€” SwiftUI links, buttons, and toggles โ€” might make sense for your content, but avoid creating app-like layouts in your widgets. Pay attention to the size of targets and make sure people can tap or click them with confidence and without accidentally performing unintended interactions. Note that inline accessory widgets offer only one tap target. + +### [Choosing margins and padding](https://developer.apple.com/design/human-interface-guidelines/widgets#Choosing-margins-and-padding) + +Widgets scale to adapt to the screen sizes of different devices and onscreen areas. Supply content at appropriate sizes to make sure that your widget looks great on every device and let the system resize or scale it as necessary. In iOS, the system ensures that your widget looks good on small devices by resizing the content you design for large devices. In iPadOS, the system renders your widget at a large size before scaling it down for display on the Home Screen. + +As you design for various devices and scale factors, use the values listed in [Specifications](https://developer.apple.com/design/human-interface-guidelines/widgets#Specifications) and the [Apple Design Resources](https://developer.apple.com/design/resources/) for guidance; for your production widget, use [SwiftUI](https://developer.apple.com/documentation/SwiftUI) to ensure flexibility. + +**In general, use standard margins to ensure legibility.** Use the standard margin width for widgets โ€” 16 points for most widgets โ€” to avoid crowding their edges and creating a cluttered appearance. If you need to use tighter margins โ€” for example, to create content groupings for graphics, buttons, or background shapes โ€” setting margins of 11 points can work well. Additionally, note that widgets use smaller margins on the desktop on Mac and on the Lock Screen, including in StandBy. For developer guidance, see [`padding(_:_:)`](https://developer.apple.com/documentation/SwiftUI/View/padding\(_:_:\)). + +**Coordinate the corner radius of your content with the corner radius of the widget.** To ensure that your content looks good within a widgetโ€™s rounded corners, use a SwiftUI container to apply the correct corner radius. For developer guidance, see [`ContainerRelativeShape`](https://developer.apple.com/documentation/SwiftUI/ContainerRelativeShape). + +### [Displaying text in widgets](https://developer.apple.com/design/human-interface-guidelines/widgets#Displaying-text-in-widgets) + +**Prefer using the system font, text styles, and SF Symbols.** Using the system font helps your widget look at home on any platform, while making it easier for you to display great-looking text in a variety of weights, styles, and sizes. Use SF Symbols to align and scale symbols with text that uses the system font. If you use a custom font, do so sparingly, and be sure itโ€™s easy for people to read at a glance. It often works well to use a custom font for the large text in a widget and SF Pro for the smaller text. For guidance, see [Typography](https://developer.apple.com/design/human-interface-guidelines/typography) and [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols). + +**Avoid very small font sizes.** In general, display text using fonts at 11 points or larger. Text in a font thatโ€™s smaller than 11 points can be too hard for many people to read. + +**Avoid rasterizing text.** Always use text elements and styles to ensure that your text scales well and to allow VoiceOver to speak your content. + +Note + +In iOS, iPadOS, and visionOS, widgets support Dynamic Type sizes from Large to AX5 when you use [`Font`](https://developer.apple.com/documentation/SwiftUI/Font) to choose a system font or [`custom(_:size:)`](https://developer.apple.com/documentation/SwiftUI/Font/custom\(_:size:\)) to choose a custom font. For more information about Dynamic Type sizes, see [Supporting Dynamic Type](https://developer.apple.com/design/human-interface-guidelines/typography#Supporting-Dynamic-Type). + +### [Using color](https://developer.apple.com/design/human-interface-guidelines/widgets#Using-color) + +**Use color to enhance a widgetโ€™s appearance without competing with its content.** Beautiful colors draw the eye, but theyโ€™re best when they donโ€™t prevent people from absorbing a widgetโ€™s information at a glance. In your asset catalog, you can also specify the colors you want the system to use as it generates your widgetโ€™s editing-mode user interface. + +**Convey meaning without relying on specific colors to represent information.** Widgets can appear monochromatic (with or without a custom tint color), and in watchOS, the system may invert colors depending on the watch face a person chooses. Use text and iconography in addition to color to express meaning. + +**Use full-color images judiciously.** When a person chooses a tinted or clear appearance for their widgets, the system by default desaturates full-color images. You can choose to render images in full-color, even when a person chooses a tinted or clear widget appearance. However, full-color images in these appearances draw special attention to the widget, which might make it feel as if the widget doesnโ€™t belong to the platform. For example, a full-color image in a widget might appear out of place when a person chooses a clear widget appearance. Consider reserving full-color images to represent media content, such as album art for a music appโ€™s widget, and use full-color images with smaller dimensions than the size of the widget. + +## [Rendering modes](https://developer.apple.com/design/human-interface-guidelines/widgets#Rendering-modes) + +### [Full-color](https://developer.apple.com/design/human-interface-guidelines/widgets#Full-color) + +**Support light and dark appearances.** Prefer light backgrounds for the light appearance and dark backgrounds for the dark appearance, and consider using the semantic system colors for text and backgrounds to let the colors dynamically adapt to the current appearance. You can also support different appearances by putting color variants in your asset catalog. For guidance, see [Dark Mode](https://developer.apple.com/design/human-interface-guidelines/dark-mode); for developer guidance, see [Asset management](https://developer.apple.com/documentation/Xcode/asset-management) and [Supporting Dark Mode in your interface](https://developer.apple.com/documentation/UIKit/supporting-dark-mode-in-your-interface). + +![An image of the small Notes widget. Below the yellow bar that contains the app icon and name, the widget displays a single note in black text on a white background.](https://docs-assets.developer.apple.com/published/fe8bbb296cb8ad12a123b95562d7c1f5/widgets-notes-light-appearance%402x.png) + +![An image of the small Notes widget. Below the yellow bar that contains the app icon and name, the widget displays a single note in white text on a black background.](https://docs-assets.developer.apple.com/published/3e2a0cd6e7b33ef2ea47970a271330fb/widgets-notes-dark-appearance%402x.png) + +### [Accented](https://developer.apple.com/design/human-interface-guidelines/widgets#Accented) + +**Group widget components into an accented and a primary group.** The accented rendering mode divides the widgetโ€™s view hierarchy into an accent group and a primary group. On iPhone, iPad, and Mac, the system tints primary and accented content white. On Apple Watch, the system tints primary content white and accented content in the color of the watch face. + +For developer guidance, see [`widgetAccentable(_:)`](https://developer.apple.com/documentation/SwiftUI/View/widgetAccentable\(_:\)) and [Optimizing your widget for accented rendering mode and Liquid Glass](https://developer.apple.com/documentation/WidgetKit/optimizing-your-widget-for-accented-rendering-mode-and-liquid-glass). + +### [Vibrant](https://developer.apple.com/design/human-interface-guidelines/widgets#Vibrant) + +**Offer enough contrast to ensure legibility.** In the vibrant rendering mode, the opacity of pixels within an image determines the strength of the blurred background material effect. Fully transparent pixels let the background material pass through as is. The brightness of pixels determines how vibrant they appear on the Lock Screen. Brighter gray values provide more contrast, and darker values provide less contrast. + +**Create optimized assets for the best vibrant effect.** Render content like images, numbers, and text at full opacity. Use white or light gray for the most prominent content and darker grayscale values for secondary elements to establish hierarchy. Confirm that image content has sufficient contrast in grayscale, and use opaque grayscale values, rather than opacities of white, to achieve the best vibrant material effect. + +## [Previews and placeholders](https://developer.apple.com/design/human-interface-guidelines/widgets#Previews-and-placeholders) + +**Design a realistic preview to display in the widget gallery.** Highlighting your widgetโ€™s capabilities โ€” and clearly representing the experiences each widget type or size can provide โ€” helps people make an informed decision. You can display real data in your widget preview, but if the data takes too long to generate or load, display realistic simulated data instead. + +**Design placeholder content that helps people recognize your widget.** An installed widget displays placeholder content while its data loads. Create an effective placeholder appearance by combining static interface components with semi-opaque shapes that stand in for dynamic content. For example, use rectangles of different widths to suggest lines of text, and circles or squares in place of glyphs and images. + +![An image of a small Tips widget that displays placeholder content on top of a yellow background. In the bottom half of the widget, three horizontal bars in different shades of yellow represent lines of text.](https://docs-assets.developer.apple.com/published/9f4ce7356e15184d0183af0e5effa04a/widgets-tips-placeholder-content%402x.png) + +![An image of a small Tips widget that displays actual data on top of a yellow background. The horizontal bars in the placeholder widget are replaced by three short lines of text in different shades of yellow.](https://docs-assets.developer.apple.com/published/aa5e504f019f5558fa61813932709755/widgets-tips-full-content%402x.png) + +**Write a succinct widget description.** The widget gallery displays descriptions that help people understand what each widget does. Begin a description with an action verb โ€” for example, โ€œSee the current weather conditions and forecast for a locationโ€ or โ€œKeep track of your upcoming events and meetings.โ€ Avoid including unnecessary phrases that reference the widget itself, like โ€œThis widget showsโ€ฆ,โ€ โ€œUse this widget toโ€ฆ,โ€ or โ€œAdd this widget.โ€ Use approachable language and [sentence-style capitalization](https://support.apple.com/guide/applestyleguide/c-apsgb744e4a3/web#apdca93e113f1d64). + +**Group your widgetโ€™s sizes together, and provide a single description.** If your widget is available in multiple sizes, group them together so people donโ€™t think each size is a different widget. Provide a single description of your widget โ€” regardless of how many sizes you offer โ€” to avoid repetition and to help people understand how each size provides a slightly different perspective on the same content and functionality. + +**Consider coloring the Add button.** After people choose your app in the widget gallery, an Add button appears below the group of widgets you offer. You can specify a color for this button to help remind people of your brand. + +![An illustration that represents the widget gallery open to the small widget for the Notes app. Below the widget is a page control showing that this is the first page of six; below the page control is a button that uses the Notes app's yellow accent color.](https://docs-assets.developer.apple.com/published/38cf8810029187a42a6873242a7a1571/widgets-add-button-tint-color-notes%402x.png) + +![An illustration that represents the widget gallery open to the small widget for the Weather app. Below the widget is a page control showing that this is the first page of six; below the page control is a button that uses the Weather app's blue accent color.](https://docs-assets.developer.apple.com/published/c1a2eced9508911b18e05bee9d3d107e/widgets-add-button-tint-color-weather%402x.png) + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/widgets#Platform-considerations) + + _No additional considerations for macOS. Not supported in tvOS._ + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/widgets#iOS-iPadOS) + +Widgets on the Lock Screen are functionally similar to watch complications and follow design principles for [Complications](https://developer.apple.com/design/human-interface-guidelines/complications) in addition to design principles for widgets. Provide useful information in your Lock Screen widget, and donโ€™t treat it only as an additional way for people to launch into your app. In many cases, a design for complications also works well for widgets on the Lock Screen (and vice versa), so consider creating them in tandem. + +Your app can offer widgets on the Lock Screen in three different shapes: as inline text that appears above the clock, and as circular and rectangular shapes that appear below the clock. + +![A partial screenshot of the Lock Screen on iPhone that shows a Calendar widget and two Weather widgets below the time. From the left, the widgets are an inline text widget and two circular widgets.](https://docs-assets.developer.apple.com/published/685e1b1e81cd591a59e63f4b1ae3bde4/widget-lock-screen-display-appearances%402x.png) + +**Support the Always-On display on iPhone.** Devices with the Always-On display render widgets on the Lock Screen with reduced luminance. Use levels of gray that provide enough contrast in the Always-On display, and make sure your content remains legible. + +For developer guidance, see [Creating accessory widgets and watch complications](https://developer.apple.com/documentation/WidgetKit/Creating-accessory-widgets-and-watch-complications). + +**Offer Live Activities to show real-time updates.** Widgets donโ€™t show real-time information. If your app allows people to track the progress of a task or event for a limited amount of time with frequent updates, consider offering Live Activities. Widgets and Live Activities use the same underlying frameworks and share design similarities. As a result, it can be a good idea to develop widgets and Live Activities in tandem and reuse code and design components for both features. For design guidance on Live Activities, see [Live Activities](https://developer.apple.com/design/human-interface-guidelines/live-activities); for developer guidance, see [ActivityKit](https://developer.apple.com/documentation/ActivityKit). + +#### [StandBy and CarPlay](https://developer.apple.com/design/human-interface-guidelines/widgets#StandBy-and-CarPlay) + +On iPhone in StandBy, the system displays two small system family widgets side-by-side, scaled up so they fill the Lock Screen. By supporting StandBy, you also ensure your widgets work well in CarPlay. CarPlay and StandBy widgets both use the small system family widget with the background removed and scaled up to best fit the grid on the Widgets screen. Glanceable information and large text are especially important in CarPlay to make your widget easy to read on a carโ€™s display. + +**Limit usage of rich images or color to convey meaning in StandBy.** Instead, make use of the additional space by scaling up and rearranging text so people can glance at the widget content from a greater distance. To seamlessly blend with the black background, donโ€™t use background colors for your widget when it appears in StandBy. + + * Correct usage + * Incorrect usage + + + +![An image of iPhone in StandBy. It shows a Clock widget on the left that displays the time as 9:41 a.m. and a Weather widget set to Cupertino with the temperature at 70 degrees Fahrenheit on the right.](https://docs-assets.developer.apple.com/published/50672d631597de47734d331e2acfc4d7/widgets-standby-removed-background-correct%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +![An image of iPhone in StandBy. It shows a Clock widget on the left that displays the time as 9:41 a.m. and a Weather widget set to Cupertino with the temperature at 70 degrees Fahrenheit on the right. The Watch widget appears with the background removed and the Weather widget isn't optimized for StandBy.](https://docs-assets.developer.apple.com/published/853c1452b137d14fde7385a83cd1238e/widgets-standby-with-background-incorrect%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +For developer guidance, see [Displaying the right widget background](https://developer.apple.com/documentation/WidgetKit/Displaying-the-right-widget-background). + +On iPhone in StandBy in low-light conditions, the system renders widgets in a monochromatic look with a red tint. + +![An image of iPhone in low-light conditions. It shows a Clock widget on the left that displays the time as 9:41 a.m. and a Weather widget set to Cupertino with the temperature at 70 degrees Fahrenheit on the right.](https://docs-assets.developer.apple.com/published/52d35d40b1ddb2272b2d9abed4354afb/widgets-standby-low-light%402x.png) + +iPhone in low-light conditions + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/widgets#visionOS) + +Widgets in visionOS are 3D objects that people place on a horizontal or vertical surface. When a person places a widget on a surface, the widget persists in that location even when the person turns Apple Vision Pro off and back on. Widgets have a consistent, real-world scale. Their size, _mounting style_ , and _treatment style_ impact how a person perceives them. + +visionOS widgets appear in full-color by default, but they appear in the accented rendering mode when people personalize them with tint colors using a range of system-provided color palettes. Additionally, people can customize the frame width of widgets that use the elevated mounting style, and custom options that are unique to the widget. For example, visionOS doesnโ€™t provide systemwide light or dark appearances. However, the Music poster widget offers its own customization option that lets people choose between a light and a dark theme that the app generates from the displayed album art. + +For developer guidance, see [Updating your widgets for visionOS](https://developer.apple.com/documentation/WidgetKit/Updating-your-widgets-for-visionOS). + +**Adapt your design and content for the spatial experience Apple Vision Pro provides.** In visionOS, widgets donโ€™t float in isolation but are part of living rooms, kitchens, offices, and more. Consider this context early and think of widgets as part of someoneโ€™s surroundings when you bring your existing widgets to visionOS or design them from scratch. For example, the Music widget adapts to a poster-like appearance thatโ€™s glanceable across the room with large typography and a high-resolution image, and a productivity app might offer a small widget that easily fits on a desk. + +**Test your widgets across the full range of system color palettes and in different lighting conditions.** Make sure your widgetโ€™s tone, contrast, and legibility remain consistent and intentional. If you choose to exclude UI elements from tinting, test your widget in every provided tint color palette to make sure the untinted elements remain legible when a person customizes their widgets with tint colors. + +#### [Thresholds and sizes](https://developer.apple.com/design/human-interface-guidelines/widgets#Thresholds-and-sizes) + +Widgets on Apple Vision Pro can adapt based on a personโ€™s proximity, and visionOS provides widgets with two key thresholds to design for: the [`simplified`](https://developer.apple.com/documentation/WidgetKit/LevelOfDetail/simplified) threshold for when a person views a widget at a distance, and the [`default`](https://developer.apple.com/documentation/WidgetKit/LevelOfDetail/default) threshold when a person views it nearby. + +![A placeholder image showing a widget viewed from a distance in visionOS.](https://docs-assets.developer.apple.com/published/0ab3137bf116e4640762c25ac6139f93/widgets-extra-large-portrait-far-proximity%402x.png)Viewed from a distance + +![A placeholder image showing a widget viewed from nearby in visionOS.](https://docs-assets.developer.apple.com/published/c3f6fbe96de6d0b9524ad7f2001755a6/widgets-extra-large-portrait-close-proximity%402x.png)Viewed from nearby + +Because widgets can appear throughout a personโ€™s environment, itโ€™s also important to match a widgetโ€™s size to the type of content it contains, and to be aware of how it appears at a variety of distances. + +**Design a responsive layout that shows the right level of detail for each of the two thresholds.** When a person views the widget at a distance, display a simplified version of your widget that shows fewer details and has a larger type size, and remove interactive elements like buttons or toggles. When a person views the widget from nearby, show more details and use a smaller type size. To create a smooth and consistent experience and help your layout feel continuous, maintain shared elements across both distance thresholds. + +**Offer widget family sizes that fit a personโ€™s surroundings well.** Widgets map to real-world dimensions and have a permanent presence in a personโ€™s spatial environment. Think about where people might place your widget โ€” mounted to a wall, placed on a sideboard, or sitting next to a workplace โ€” and choose a widget family size thatโ€™s right for that context. For example, offer a small system widget with content that people might place on a desk or an extra large widget to let people decorate their surroundings with something visually rich, like artwork or photography. + +**Display content in a way that remains legible from a range of distances.** To make a widget feel intentional and proportionate to where they place it, people can scale a widget from 75 to 125 percent in size. Use print design principles like clear hierarchy, strong typography, and scale to make sure your content remains glanceable. Include high-resolution assets that look good scaled up to every size. + +#### [Mounting styles](https://developer.apple.com/design/human-interface-guidelines/widgets#Mounting-styles) + +The way a widget appears on a surface plays a big role in how a person perceives it. To make it feel intentional and integrated into their surroundings, people place a widget on surfaces in distinct mounting styles. + + * **[Elevated](https://developer.apple.com/documentation/WidgetKit/WidgetMountingStyle/elevated) style**. On horizontal surfaces โ€” for example, on a desk โ€” the widget always appears elevated and gently tilts backward, providing a subtle angle that improves readability, and casts a soft shadow that helps it feel grounded on the surface. On vertical surfaces โ€” for example, on a wall โ€” the widget either appears elevated, sitting flush on the surface and similar to how you mount a picture frame. + + * **[Recessed](https://developer.apple.com/documentation/WidgetKit/WidgetMountingStyle/recessed) style**. On vertical surfaces โ€” for example, on a wall โ€” the widget can appear recessed, with content set back into the surface, creating a depth effect that gives the illusion of a cutout in the surface. Horizontal surfaces donโ€™t use the recessed mounting style. + + + + +By default, widgets use the elevated mounting style, because it works for horizontal and vertical surfaces. + +**Choose the mounting style that fits your content and the experience you want to create.** By default, visionOS widgets use the elevated mounting style, which is ideal for content that you want to stand out and feel present, like reminders, media, or glanceable data. Recessed widgets are ideal for immersive or ambient content, like weather or editorial content, and people can only place them on a vertical surface. If a style doesnโ€™t suit your widget, you can opt out of it for each widget. If you choose to only support the recessed mounting style, people canโ€™t place the widget on a horizontal surface. For example, a weather app might only support the recessed mounting style to give the illusion of looking out of a window for its large and extra-large system family widgets, and only support the elevated style for its small system family widget. + +Developer note + +Use the [`supportedMountingStyles(_:)`](https://developer.apple.com/documentation/SwiftUI/WidgetConfiguration/supportedMountingStyles\(_:\)) property of your [`WidgetConfiguration`](https://developer.apple.com/documentation/SwiftUI/WidgetConfiguration) to declare supported mounting styles โ€” elevated, recessed, or both โ€” for all widgets included in the configuration. To offer a widget that only supports one mounting style and other widgets that support both mounting styles, create separate widget configurations. For example, create one widget configuration for the widget that only supports the recessed mounting style, and a second configuration for the widgets that support both mounting styles. + +**Test your elevated widget designs with each system-provided frame width.** People can choose from different system-defined frame widths for widgets that use the elevated mounting style. You canโ€™t change your layout based on the frame width a person chooses, so make sure your widget layout stays visually balanced for each frame width. + +#### [Treatment styles](https://developer.apple.com/design/human-interface-guidelines/widgets#Treatment-styles) + +In addition to size and mounting style, the system applies one of two treatment styles to visionOS widgets. Choosing the right treatment for your widget helps reinforce the experience you want to create. + + * The [`paper`](https://developer.apple.com/documentation/WidgetKit/WidgetTexture/paper) style creates a more grounded, print-like style that feels solid and makes the widget feel like part of its surroundings. When lighting conditions change, widgets in the paper style become darker or lighter in response. + + * The [`glass`](https://developer.apple.com/documentation/WidgetKit/WidgetTexture/glass) style creates a lighter, layered look that adds depth and visual separation between foreground and background elements to emphasize clarity and contrast. The foreground elements always stay bright and legible, and donโ€™t dim or brighten, even as ambient light changes. + + + + +**Choose the paper style for a print-like look that feels more like a real object in the room.** The entire widget responds to the ambient lighting and blends naturally into its surroundings. For example, the Music poster widget uses the paper style to display albums and playlists like framed artwork on a wall. + +**Choose the glass style for information-rich widgets.** Glass visually separates foreground and background elements, allowing you to decide which parts of your interface adapt to the surroundings and which stay visually consistent. Foreground elements appear in full color, unaffected by ambient lighting, to make sure important content stays sharp and legible. For example, a News widget appears with editorial images in the background with a soft, print-like look. Its headlines stay in the foreground, crisp and easy to read. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/widgets#watchOS) + +**Provide a colorful background that conveys meaning.** By default, widgets in the Smart Stack use a black background. Consider using a custom background color that provides additional meaning. For example, the Stocks app uses a red background for falling stock values and a green background if a stockโ€™s value rises. + +**Encourage the system to display or elevate the position of your watchOS widget in the Smart Stack.** Relevancy information helps the system show your widget when people need it most. Relevance can be location-based or specific to ongoing system actions, like a workout. For developer guidance, see [RelevanceKit](https://developer.apple.com/documentation/RelevanceKit). + +## [Specifications](https://developer.apple.com/design/human-interface-guidelines/widgets#Specifications) + +As you design your widgets, use the following values for guidance. + +### [iOS dimensions](https://developer.apple.com/design/human-interface-guidelines/widgets#iOS-dimensions) + +Screen size (portrait, pt)| Small (pt)| Medium (pt)| Large (pt)| Circular (pt)| Rectangular (pt)| Inline (pt) +---|---|---|---|---|---|--- +430ร—932| 170x170| 364x170| 364x382| 76x76| 172x76| 257x26 +428x926| 170x170| 364x170| 364x382| 76x76| 172x76| 257x26 +414x896| 169x169| 360x169| 360x379| 76x76| 160x72| 248x26 +414x736| 159x159| 348x157| 348x357| 76x76| 170x76| 248x26 +393x852| 158x158| 338x158| 338x354| 72x72| 160x72| 234x26 +390x844| 158x158| 338x158| 338x354| 72x72| 160x72| 234x26 +375x812| 155x155| 329x155| 329x345| 72x72| 157x72| 225x26 +375x667| 148x148| 321x148| 321x324| 68x68| 153x68| 225x26 +360x780| 155x155| 329x155| 329x345| 72x72| 157x72| 225x26 +320x568| 141x141| 292x141| 292x311| N/A| N/A| N/A + +### [iPadOS dimensions](https://developer.apple.com/design/human-interface-guidelines/widgets#iPadOS-dimensions) + +Screen size (portrait, pt)| Target| Small (pt)| Medium (pt)| Large (pt)| Extra large (pt) +---|---|---|---|---|--- +768x1024| Canvas| 141x141| 305.5x141| 305.5x305.5| 634.5x305.5 +Device| 120x120| 260x120| 260x260| 540x260 +744x1133| Canvas| 141x141| 305.5x141| 305.5x305.5| 634.5x305.5 +Device| 120x120| 260x120| 260x260| 540x260 +810x1080| Canvas| 146x146| 320.5x146| 320.5x320.5| 669x320.5 +Device| 124x124| 272x124| 272x272| 568x272 +820x1180| Canvas| 155x155| 342x155| 342x342| 715.5x342 +Device| 136x136| 300x136| 300x300| 628x300 +834x1112| Canvas| 150x150| 327.5x150| 327.5x327.5| 682x327.5 +Device| 132x132| 288x132| 288x288| 600x288 +834x1194| Canvas| 155x155| 342x155| 342x342| 715.5x342 +Device| 136x136| 300x136| 300x300| 628x300 +954x1373 *| Canvas| 162x162| 350x162| 350x350| 726x350 +Device| 162x162| 350x162| 350x350| 726x350 +970x1389 *| Canvas| 162x162| 350x162| 350x350| 726x350 +Device| 162x162| 350x162| 350x350| 726x350 +1024x1366| Canvas| 170x170| 378.5x170| 378.5x378.5| 795x378.5 +Device| 160x160| 356x160| 356x356| 748x356 +1192x1590 *| Canvas| 188x188| 412x188| 412x412| 860x412 +Device| 188x188| 412x188| 412x412| 860x412 + +* When Display Zoom is set to More Space. + +### [visionOS dimensions](https://developer.apple.com/design/human-interface-guidelines/widgets#visionOS-dimensions) + +Widget| Size in pt| Size in mm (scaled to 100%) +---|---|--- +Small| 158x158| 268x268 +Medium| 338x158| 574x268 +Large| 338x354| 574x600 +Extra large| 450x338| 763x574 +Extra large portrait| 338x450| 574x763 + +### [watchOS dimensions](https://developer.apple.com/design/human-interface-guidelines/widgets#watchOS-dimensions) + +Apple Watch size| Size of a widget in the Smart Stack (pt) +---|--- +40mm| 152x69.5 +41mm| 165x72.5 +44mm| 173x76.5 +45mm| 184x80.5 +49mm| 191x81.5 + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/widgets#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/widgets#Related) + +[Layout](https://developer.apple.com/design/human-interface-guidelines/layout) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/widgets#Developer-documentation) + +[WidgetKit](https://developer.apple.com/documentation/WidgetKit) + +[Developing a WidgetKit strategy](https://developer.apple.com/documentation/WidgetKit/Developing-a-WidgetKit-strategy) โ€” WidgetKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/widgets#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/126547AE-9E47-4E15-AC05-5C50AB08CBEE/9952_wide_250x141_1x.jpg) Whatโ€™s new in widgets ](https://developer.apple.com/videos/play/wwdc2025/278) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/4D354E1F-5016-49F7-9296-3D3722626480/9957_wide_250x141_1x.jpg) Design widgets for visionOS ](https://developer.apple.com/videos/play/wwdc2025/255) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/5631C647-3158-42F6-A1D3-50566815A1BB/8056_wide_250x141_1x.jpg) Bring widgets to life ](https://developer.apple.com/videos/play/wwdc2023/10028) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/widgets#Change-log) + +Date| Changes +---|--- +December 16, 2025| Updated guidance for all platforms, and added guidance for visionOS and CarPlay. +January 17, 2025| Corrected watchOS widget dimensions. +June 10, 2024| Updated to include guidance for accented widgets in iOS 18 and iPadOS 18. +June 5, 2023| Updated guidance to include widgets in watchOS, widgets on the iPad Lock Screen, and updates for iOS 17, iPadOS 17, and macOS 14. +November 3, 2022| Added guidance for widgets on the iPhone Lock Screen and updated design comprehensives for iPhone 14, iPhone 14 Pro, and iPhone 14 Pro Max. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/SKILL.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/SKILL.md new file mode 100644 index 00000000..4c6ed762 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/SKILL.md @@ -0,0 +1,94 @@ +--- +name: hig-foundations +description: Apple Human Interface Guidelines design foundations. +risk: unknown +source: community +date_added: '2026-02-27' +--- + +# Apple HIG: Design Foundations + +Check for `.claude/apple-design-context.md` before asking questions. Use existing context and only ask for information not already covered. + +## Key Principles + +1. **Prioritize content over chrome.** Reduce visual clutter. Use system-provided materials and subtle separators rather than heavy borders and backgrounds. + +2. **Build in accessibility from the start.** Design for VoiceOver, Dynamic Type, Reduce Motion, Increase Contrast, and Switch Control from day one. Every interactive element needs an accessible label. + +3. **Use system colors and materials.** System colors adapt to light/dark mode, increased contrast, and vibrancy. Prefer semantic colors (`label`, `secondaryLabel`, `systemBackground`) over hard-coded values. + +4. **Use platform fonts and icons.** SF Pro, SF Compact, SF Mono by default. New York for serif. Follow the type hierarchy at recommended sizes. Use SF Symbols for iconography. + +5. **Match platform conventions.** Align look and behavior with system standards. Provide direct, responsive manipulation and clear feedback for every action. + +6. **Respect privacy.** Request permissions only when needed, explain why clearly, provide value before asking for data. Design for minimal data collection. + +7. **Support internationalization.** Accommodate text expansion, right-to-left scripts, and varying date/number formats. Use Auto Layout for dynamic content sizing. + +8. **Use motion purposefully.** Animation should communicate meaning and spatial relationships. Honor Reduce Motion by providing crossfade alternatives. + +## Reference Index + +| Reference | Topic | Key content | +|---|---|---| +| [accessibility.md](references/accessibility.md) | Accessibility | VoiceOver, Dynamic Type, color contrast, motor accessibility, Switch Control, audio descriptions | +| [app-icons.md](references/app-icons.md) | App Icons | Icon grid, platform-specific sizes, single focal point, no transparency | +| [branding.md](references/branding.md) | Branding | Integrating brand identity within Apple's design language, subtle branding, custom tints | +| [color.md](references/color.md) | Color | System colors, Dynamic Colors, semantic colors, custom palettes, contrast ratios | +| [dark-mode.md](references/dark-mode.md) | Dark Mode | Elevated surfaces, semantic colors, adapted palettes, vibrancy, testing in both modes | +| [icons.md](references/icons.md) | Icons | Glyph icons, SF Symbols integration, custom icon design, icon weights, optical alignment | +| [images.md](references/images.md) | Images | Image resolution, @2x/@3x assets, vector assets, image accessibility | +| [immersive-experiences.md](references/immersive-experiences.md) | Immersive Experiences | AR/VR design, spatial immersion, comfort zones, progressive immersion levels | +| [inclusion.md](references/inclusion.md) | Inclusion | Diverse representation, non-gendered language, cultural sensitivity, inclusive defaults | +| [layout.md](references/layout.md) | Layout | Margins, spacing, alignment, safe areas, adaptive layouts, readable content guides | +| [materials.md](references/materials.md) | Materials | Vibrancy, blur, translucency, system materials, material thickness | +| [motion.md](references/motion.md) | Motion | Animation curves, transitions, continuity, Reduce Motion support, physics-based motion | +| [privacy.md](references/privacy.md) | Privacy | Permission requests, usage descriptions, privacy nutrition labels, minimal data collection | +| [right-to-left.md](references/right-to-left.md) | Right-to-Left | RTL layout mirroring, bidirectional text, icons that flip, exceptions | +| [sf-symbols.md](references/sf-symbols.md) | SF Symbols | Symbol categories, rendering modes, variable color, custom symbols, weight matching | +| [spatial-layout.md](references/spatial-layout.md) | Spatial Layout | visionOS window placement, depth, ergonomic zones, Z-axis design | +| [typography.md](references/typography.md) | Typography | SF Pro, Dynamic Type sizes, text styles, custom fonts, font weight hierarchy, line spacing | +| [writing.md](references/writing.md) | Writing | UI copy guidelines, tone, capitalization rules, error messages, button labels, conciseness | + +## Applying Foundations Together + +Consider how principles interact: + +1. **Color + Dark Mode + Accessibility** -- Custom palettes must work in both modes while maintaining WCAG contrast ratios. Start with system semantic colors. + +2. **Typography + Accessibility + Layout** -- Dynamic Type must scale without breaking layouts. Use text styles and Auto Layout for the full range of type sizes. + +3. **Icons + Branding + SF Symbols** -- Custom icons should match SF Symbols weight and optical sizing. Brand elements should integrate without overriding system conventions. + +4. **Motion + Accessibility + Feedback** -- Every animation must have a Reduce Motion alternative. Motion should reinforce spatial relationships, not decorate. + +5. **Privacy + Writing + Onboarding** -- Permission requests need clear, specific usage descriptions. Time them to when the user will understand the benefit. + +## Output Format + +1. **Cite the specific HIG foundation** with file and section. +2. **Note platform differences** for the user's target platforms. +3. **Provide concrete code patterns** (SwiftUI/UIKit/AppKit). +4. **Explain accessibility impact** (contrast ratios, Dynamic Type scaling, VoiceOver behavior). + +## Questions to Ask + +1. Which platforms are you targeting? +2. Do you have existing brand guidelines? +3. What accessibility level are you targeting? (WCAG AA, AAA, Apple baseline?) +4. System colors or custom? + +## Related Skills + +- **hig-platforms** -- How foundations apply per platform (e.g., type scale differences on watchOS vs macOS) +- **hig-patterns** -- Interaction patterns where foundations like writing and accessibility are critical +- **hig-components-layout** -- Structural components implementing layout principles +- **hig-components-content** -- Content display using color, typography, and images + +--- + +*Built by [Raintree Technology](https://raintree.technology) ยท [More developer tools](https://raintree.technology)* + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/accessibility.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/accessibility.md new file mode 100644 index 00000000..11d9a347 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/accessibility.md @@ -0,0 +1,291 @@ +--- +title: "Accessibility | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/accessibility + +# Accessibility + +Accessible user interfaces empower everyone to have a great experience with your app or game. + +![A sketch of the Accessibility icon. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/f7e408b21d156daa60c2e30c0bcff9e5/foundations-accessibility-intro%402x.png) + +When you design for accessibility, you reach a larger audience and create a more inclusive experience. An accessible interface allows people to experience your app or game regardless of their capabilities or how they use their devices. Accessibility makes information and interactions available to everyone. An accessible interface is: + + * **Intuitive.** Your interface uses familiar and consistent interactions that make tasks straightforward to perform. + + * **Perceivable.** Your interface doesnโ€™t rely on any single method to convey information. People can access and interact with your content, whether they use sight, hearing, speech, or touch. + + * **Adaptable.** Your interface adapts to how people want to use their device, whether by supporting system accessibility features or letting people personalize settings. + + + + +As you design your app, audit the accessibility of your interface. Use [Accessibility Inspector](https://developer.apple.com/documentation/Accessibility/accessibility-inspector) to highlight accessibility issues with your interface and to understand how your app represents itself to people using system accessibility features. You can also communicate how accessible your app is on the App Store using Accessibility Nutrition Labels. To learn more about how to evaluate and indicate accessibility feature support, see [Accessibility Nutrition Labels](https://developer.apple.com/help/app-store-connect/manage-app-accessibility/overview-of-accessibility-nutrition-labels) in App Store Connect help. + +## [Vision](https://developer.apple.com/design/human-interface-guidelines/accessibility#Vision) + +![An illustration containing five symbols associated with the topic of vision, including symbols representing text size, magnification, VoiceOver, and spoken dialogue.](https://docs-assets.developer.apple.com/published/bedd6018a62492eff46566493335ebe7/accessibility-vision-section-hero%402x.png) + +The people who use your interface may be blind, color blind, or have low vision or light sensitivity. They may also be in situations where lighting conditions and screen brightness affect their ability to interact with your interface. + +**Support larger text sizes.** Make sure people can adjust the size of your text or icons to make them more legible, visible, and comfortable to read. Ideally, give people the option to enlarge text by at least 200 percent (or 140 percent in watchOS apps). Your interface can support font size enlargement either through custom UI, or by adopting Dynamic Type. Dynamic Type is a systemwide setting that lets people adjust the size of text for comfort and legibility. For more guidance, see [Supporting Dynamic Type](https://developer.apple.com/design/human-interface-guidelines/typography#Supporting-Dynamic-Type). + +**Use recommended defaults for custom type sizes.** Each platform has different default and minimum sizes for system-defined type styles to promote readability. If youโ€™re using custom type styles, follow the recommended defaults. + +Platform| Default size| Minimum size +---|---|--- +iOS, iPadOS| 17 pt| 11 pt +macOS| 13 pt| 10 pt +tvOS| 29 pt| 23 pt +visionOS| 17 pt| 12 pt +watchOS| 16 pt| 12 pt + +**Bear in mind that font weight can also impact how easy text is to read.** If youโ€™re using a custom font with a thin weight, aim for larger than the recommended sizes to increase legibility. For more guidance, see [Typography](https://developer.apple.com/design/human-interface-guidelines/typography). + +![An illustration of a rectangular view containing the word 'Hello,' formatted bold, at a small font size.](https://docs-assets.developer.apple.com/published/b8366a96b31af036b2414243d299b011/accessibility-font-weight-small-bold%402x.png)Thicker weights are easier to read for smaller font sizes. + +![An illustration of a rectangular view containing the word 'Hello,' formatted thin, at a large font size.](https://docs-assets.developer.apple.com/published/1f164f6ff2cb994f3852340272a3df90/accessibility-font-weight-large-thin%402x.png)Consider increasing the font size when using a thin weight. + +**Strive to meet color contrast minimum standards.** To ensure all information in your app is legible, itโ€™s important that thereโ€™s enough contrast between foreground text and icons and background colors. Two popular standards of measure for color contrast are the [Web Content Accessibility Guidelines (WCAG)](https://www.w3.org/TR/WCAG/) and the Accessible Perceptual Contrast Algorithm (APCA). Use standard contrast calculators to ensure your UI meets acceptable levels. [Accessibility Inspector](https://developer.apple.com/documentation/Accessibility/accessibility-inspector) uses the following values from WCAG Level AA as guidance in determining whether your appโ€™s colors have an acceptable contrast. + +Text size| Text weight| Minimum contrast ratio +---|---|--- +Up to 17 pts| All| 4.5:1 +18 pts| All| 3:1 +All| Bold| 3:1 + +If your app doesnโ€™t provide this minimum contrast by default, ensure it at least provides a higher contrast color scheme when the system setting Increase Contrast is turned on. If your app supports [Dark Mode](https://developer.apple.com/design/human-interface-guidelines/dark-mode), make sure to check the minimum contrast in both light and dark appearances. + +![An illustration of a button that has insufficient contrast between the button's title and background.](https://docs-assets.developer.apple.com/published/7da7a46683e0b9063fb1c9db6ab59bd9/accessibilty-button-poor-color-contrast%402x.png)A button with insufficient color contrast + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration of a button that has sufficient contrast between the button's title and background.](https://docs-assets.developer.apple.com/published/7e5df7edfe62df057eef743f9a449040/accessibilty-button-good-color-contrast%402x.png)A button with sufficient color contrast + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +**Prefer system-defined colors.** These colors have their own accessible variants that automatically adapt when people adjust their color preferences, such as enabling Increase Contrast or toggling between the light and dark appearances. For guidance, see [Color](https://developer.apple.com/design/human-interface-guidelines/color). + +![An illustration demonstrating how the system-defined color red appears above a light and dark background. In the illustration, a circle is positioned above a rounded rectangle. The left side of the rounded rectangle is light in color, and the right side is dark. The left side of the circle is slightly darker than the right side.](https://docs-assets.developer.apple.com/published/9fec337c567366d81319e2daf38b6a8a/accessibility-system-red-ios-default%402x.png)The `systemRed` default color in iOS + +![An illustration demonstrating how the system-defined accessibility-specific color red appears above a light and dark background. In the illustration, a circle is positioned above a rounded rectangle. The left side of the rounded rectangle is light in color, and the right side is dark. The left side of the circle is considerably darker than the right side.](https://docs-assets.developer.apple.com/published/9e1e71f5dff34acee2faaff88ac135a0/accessibility-system-red-ios-accessible%402x.png)The `systemRed` accessible color in iOS + +**Convey information with more than color alone.** Some people have trouble differentiating between certain colors and shades. For example, people who are color blind may have particular difficulty with pairings such as red-green and blue-orange. Offer visual indicators, like distinct shapes or icons, in addition to color to help people perceive differences in function and changes in state. Consider allowing people to customize color schemes such as chart colors or game characters so they can personalize your interface in a way thatโ€™s comfortable for them. + +![An illustration of a green circle to the left of a red circle.](https://docs-assets.developer.apple.com/published/5d62d6f6c6ff1563d80847b3b29e2125/accessibility-differentiate-with-shapes-incorrect%402x.png)For someone with red-green color blindness, these indicators might appear the same. + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration of a green circle containing a checkmark to the left of a red octagon containing an X.](https://docs-assets.developer.apple.com/published/e13c9c34a780c2d2ab0e614f55a3e73e/accessibility-differentiate-with-shapes-correct%402x.png)Both visual indicators and color help differentiate between indicators. + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +**Describe your appโ€™s interface and content for VoiceOver.** VoiceOver is a screen reader that lets people experience your appโ€™s interface without needing to see the screen. For more guidance, see [VoiceOver](https://developer.apple.com/design/human-interface-guidelines/voiceover). + +## [Hearing](https://developer.apple.com/design/human-interface-guidelines/accessibility#Hearing) + +![An illustration containing five symbols associated with the topic of hearing, including symbols representing sound, waveforms, and closed captioning.](https://docs-assets.developer.apple.com/published/eef3040be22f7aa6b10dc45b2918f9f8/accessibility-hearing-section-hero%402x.png) + +The people who use your interface may be deaf or hard of hearing. They may also be in noisy or public environments. + +**Support text-based ways to enjoy audio and video.** Itโ€™s important that dialogue and crucial information about your app or game isnโ€™t communicated through audio alone. Depending on the context, give people different text-based ways to experience their media, and allow people to customize the visual presentation of that text: + + * **Captions** give people the textual equivalent of audible information in video or audio-only content. Captions are great for scenarios like game cutscenes and video clips where text synchronizes live with the media. + + * **Subtitles** allow people to read live onscreen dialogue in their preferred language. Subtitles are great for TV shows and movies. + + * **Audio descriptions** are interspersed between natural pauses in the main audio of a video and supply spoken narration of important information thatโ€™s presented only visually. + + * **Transcripts** provide a complete textual description of a video, covering both audible and visual information. Transcripts are great for longer-form media like podcasts and audiobooks where people may want to review content as a whole or highlight the transcript as media is playing. + + + + +For developer guidance, see [Selecting subtitles and alternative audio tracks](https://developer.apple.com/documentation/AVFoundation/selecting-subtitles-and-alternative-audio-tracks). + +**Use haptics in addition to audio cues.** If your interface conveys information through audio cues โ€” such as a success chime, error sound, or game feedback โ€” consider pairing that sound with matching haptics for people who canโ€™t perceive the audio or have their audio turned off. In iOS and iPadOS, you can also use [Music Haptics](https://developer.apple.com/documentation/MediaAccessibility/music-haptics) and [Audio graphs](https://developer.apple.com/documentation/Accessibility/audio-graphs) to let people experience music and infographics through vibration and texture. For guidance, see [Playing haptics](https://developer.apple.com/design/human-interface-guidelines/playing-haptics). + +![An illustration of an iPhone device vibrating as music plays from the device.](https://docs-assets.developer.apple.com/published/1bf9d6ae5c3586a5163ce6abf0cabb95/accessibility-haptic-audio-combo%402x.png) + +**Augment audio cues with visual cues.** This is especially important for games and spatial apps where important content might be taking place off screen. When using audio to guide people towards a specific action, also add in visual indicators that point to where you want people to interact. + +## [Mobility](https://developer.apple.com/design/human-interface-guidelines/accessibility#Mobility) + +![An illustration containing five symbols associated with the topic of mobility, including symbols representing the keyboard, movement, and touch.](https://docs-assets.developer.apple.com/published/f8e9d74dc994111ba0ee7fa436fc2fc1/accessibility-mobility-section-hero%402x.png) + +Ensure your interface offers a comfortable experience for people with limited dexterity or mobility. + +**Offer sufficiently sized controls.** Controls that are too small are hard for many people to interact with and select. Strive to meet the recommended minimum control size for each platform to ensure controls and menus are comfortable for all when tapping and clicking. + +Platform| Default control size| Minimum control size +---|---|--- +iOS, iPadOS| 44x44 pt| 28x28 pt +macOS| 28x28 pt| 20x20 pt +tvOS| 66x66 pt| 56x56 pt +visionOS| 60x60 pt| 28x28 pt +watchOS| 44x44 pt| 28x28 pt + +**Consider spacing between controls as important as size.** Include enough padding between elements to reduce the chance that someone taps the wrong control. In general, it works well to add about 12 points of padding around elements that include a bezel. For elements without a bezel, about 24 points of padding works well around the elementโ€™s visible edges. + +![An illustration showing three buttons: rewind, play, and fast forward. The buttons have insufficient padding between them.](https://docs-assets.developer.apple.com/published/4148fe218b3f50b66d64eeda288de5be/accessibility-controls-spacing-incorrect%402x.png)Elements with insufficient padding + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration showing three buttons: rewind, play, and fast forward. The buttons are spaced apart, with sufficient padding between them.](https://docs-assets.developer.apple.com/published/98bc500a0a2cf15620b972de1fcce3b3/accessibility-controls-spacing-correct%402x.png)Elements with sufficient padding + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +**Support simple gestures for common interactions.** For many people, with or without disabilities, complex gestures can be challenging. For interactions people do frequently in your app or game, use the simplest gesture possible โ€” avoid custom multifinger and multihand gestures โ€” so repetitive actions are both comfortable and easy to remember. + +**Offer alternatives to gestures.** Make sure your UIโ€™s core functionality is accessible through more than one type of physical interaction. Gestures can be less comfortable for people who have limited dexterity, so offer onscreen ways to achieve the same outcome. For example, if you use a swipe gesture to dismiss a view, also make a button available so people can tap or use an assistive device. + +![An illustration of a table view in edit mode. The rows of the table include delete buttons.](https://docs-assets.developer.apple.com/published/fa893cee3fa5c70e99dfefa85c0c390a/accessibility-tap-to-delete%402x.png)Edit and tap to delete + +![An illustration of a table view. One of the rows in the table is swiped to the left to reveal a delete button.](https://docs-assets.developer.apple.com/published/f6eb08c3c3a75f5b1b337b4813b4e95e/accessibility-swipe-to-delete%402x.png)Swipe to delete + +**Let people use Voice Control to give guidance and enter information verbally.** With Voice Control, people can interact with their devices entirely by speaking commands. They can perform gestures, interact with screen elements, dictate and edit text, and more. To ensure a smooth experience, label interface elements appropriately. For developer guidance, see [Voice Control](https://developer.apple.com/documentation/Accessibility/voice-control). + +**Integrate with Siri and Shortcuts to let people perform tasks using voice alone.** When your app supports Siri and Shortcuts, people can automate the important and repetitive tasks they perform regularly. They can initiate these tasks from Siri, the Action button on their iPhone or Apple Watch, and shortcuts on their Home Screen or in Control Center. For guidance, see [Siri](https://developer.apple.com/design/human-interface-guidelines/siri). + +**Support mobility-related assistive technologies.** Features like [VoiceOver](https://developer.apple.com/design/human-interface-guidelines/voiceover), AssistiveTouch, Full Keyboard Access, Pointer Control, and [Switch Control](https://developer.apple.com/documentation/Accessibility/switch-control) offer alternative ways for people with low mobility to interact with their devices. Conduct testing and verify that your app or game supports these technologies, and that your interface elements are appropriately labeled to ensure a great experience. For more information, see [Performing accessibility testing for your app](https://developer.apple.com/documentation/Accessibility/performing-accessibility-testing-for-your-app). + +## [Speech](https://developer.apple.com/design/human-interface-guidelines/accessibility#Speech) + +![An illustration containing five symbols associated with the topic of speech, including symbols representing waveforms and speech.](https://docs-assets.developer.apple.com/published/62f06a887d774ec29a27ce2be6d30444/accessibility-speech-section-hero%402x.png) + +Appleโ€™s accessibility features help people with speech disabilities and people who prefer text-based interactions to communicate effectively using their devices. + +**Let people use the keyboard alone to navigate and interact with your app.** People can turn on Full Keyboard Access to navigate apps using their physical keyboard. The system also defines accessibility keyboard shortcuts and a wide range of other [keyboard shortcuts](https://support.apple.com/en-us/102650) that many people use all the time. Avoid overriding system-defined keyboard shortcuts and evaluate your app to ensure it works well with Full Keyboard Access. For additional guidance, see [Keyboards](https://developer.apple.com/design/human-interface-guidelines/keyboards). For developer guidance, see [Support Full Keyboard Access in your iOS app](https://developer.apple.com/videos/play/wwdc2021/10120). + +**Support Switch Control.** Switch Control is an assistive technology that lets people control their devices through separate hardware, game controllers, or sounds such as a click or a pop. People can perform actions like selecting, tapping, typing, and drawing when your app or game supports the ability to navigate using Switch Control. For developer guidance, see [Switch Control](https://developer.apple.com/documentation/Accessibility/switch-control). + +## [Cognitive](https://developer.apple.com/design/human-interface-guidelines/accessibility#Cognitive) + +![An illustration containing five symbols associated with the topic of cognition, including symbols representing music, security, and information hierarchy.](https://docs-assets.developer.apple.com/published/0d837305d3c06f6ba0199cf2764df3fd/accessibility-cognitive-section-hero%402x.png) + +When you minimize complexity in your app or game, all people benefit. + +**Keep actions simple and intuitive.** Ensure that people can navigate your interface using easy-to-remember and consistent interactions. Prefer system gestures and behaviors people are already familiar with over creating custom gestures people must learn and retain. + +**Minimize use of time-boxed interface elements.** Views and controls that auto-dismiss on a timer can be problematic for people who need longer to process information, and for people who use assistive technologies that require more time to traverse the interface. Prefer dismissing views with an explicit action. + +**Consider offering difficulty accommodations in games.** Everyone has their own way of playing and enjoying games. To support a variety of cognitive abilities, consider adding the ability to customize the difficulty level of your game, such as offering options for people to reduce the criteria for successfully completing a level, adjust reaction time, or enable control assistance. + +**Let people control audio and video playback.** Avoid autoplaying audio and video content without also providing controls to start and stop it. Make sure these controls are discoverable and easy to act upon, and consider global settings that let people opt out of auto-playing all audio and video. For developer guidance, see [Animated images](https://developer.apple.com/documentation/Accessibility/animated-images) and [`isVideoAutoplayEnabled`](https://developer.apple.com/documentation/UIKit/UIAccessibility/isVideoAutoplayEnabled). + +**Allow people to opt out of flashing lights in video playback.** People might want to avoid bright, frequent flashes of light in the media they consume. A Dim Flashing Lights setting allows the system to calculate, mitigate, and inform people about flashing lights in a piece of media. If your app supports video playback, ensure that it responds appropriately to the Dim Flashing Lights setting. For developer guidance, see [Flashing lights](https://developer.apple.com/documentation/MediaAccessibility/flashing-lights). + +**Be cautious with fast-moving and blinking animations.** When you use these effects in excess, it can be distracting, cause dizziness, and in some cases even result in epileptic episodes. People who are prone to these effects can turn on the Reduce Motion accessibility setting. When this setting is active, ensure your app or game responds by reducing automatic and repetitive animations, including zooming, scaling, and peripheral motion. Other best practices for reducing motion include: + + * Tightening animation springs to reduce bounce effects + + * Tracking animations directly with peopleโ€™s gestures + + * Avoiding animating depth changes in z-axis layers + + * Replacing transitions in x-, y-, and z-axes with fades to avoid motion + + * Avoiding animating into and out of blurs + + + + +**Optimize your appโ€™s UI for Assistive Access.** Assistive Access is an accessibility feature in iOS and iPadOS that allows people with cognitive disabilities to use a streamlined version of your app. Assistive Access sets a default layout and control presentation for apps that reduces cognitive load, such as the following layout of the Camera app. + +![A screenshot of the Camera app in Assistive Access, showing an interface with three large buttons: Photo, Video, and Back.](https://docs-assets.developer.apple.com/published/186637e83d4ec29d3d20a8249be8a538/accessibility-assistive-access-camera%402x.png) + +![A screenshot of the Camera app open to the photo screen in Assistive Access, showing an interface with two large buttons: Take Photo and Back.](https://docs-assets.developer.apple.com/published/c2abc07058fc2e64295271c85c5d66eb/accessibility-assistive-access-camera-photo-mode%402x.png) + +To optimize your app for this mode, use the following guidelines when Assistive Access is turned on: + + * Identify the core functionality of your app and consider removing noncritical workflows and UI elements. + + * Break up multistep workflows so people can focus on a single interaction per screen. + + * Always ask for confirmation twice whenever people perform an action thatโ€™s difficult to recover from, such a deleting a file. + + + + +For developer guidance, see [Assistive Access](https://developer.apple.com/documentation/Accessibility/assistive-access). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/accessibility#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, or watchOS._ + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/accessibility#visionOS) + +visionOS offers a variety of accessibility features people can use to interact with their surroundings in ways that are comfortable and work best for them, including head and hand Pointer Control, and a Zoom feature. + + * Pointer Control (hand) + * Pointer Control (head) + * Zoom + + + +Video with custom controls. + +Content description: A recording of a person's hand using Pointer Control to interact with content in an app's visionOS window. A line with a pointer at the end extends from the person's hand. It changes position within the field of view as the person moves their hand. + +Play + +Video with custom controls. + +Content description: A recording of someone using Pointer Control to interact with content in an app's visionOS window. The person isn't visible in the recording. Only the pointer is visible. It's centered in the field of view, and the person uses their head movement to position content beneath the pointer. + +Play + +![A screenshot of an app's window in visionOS. A zoom lens is visible above a portion of the window, and displays a zoomed-in version of the content beneath the lens.](https://docs-assets.developer.apple.com/published/087dd22d68c54c95cd70008020f6dc1e/visionos-accessibility-zoom-lens%402x.png) + +**Prioritize comfort.** The immersive nature of visionOS means that interfaces, animations, and interactions have a greater chance of causing motion sickness, and visual and ergonomic discomfort for people. To ensure the most comfortable experience, consider these tips: + + * Keep interface elements within a personโ€™s field of view. Prefer horizontal layouts to vertical ones that might cause neck strain, and avoid demanding the viewerโ€™s attention in different locations in quick succession. + + * Reduce the speed and intensity of animated objects, particularly in someoneโ€™s peripheral vision. + + * Be gentle with camera and video motion, and avoid situations where someone may feel like the world around them is moving without their control. + + * Avoid anchoring content to the wearerโ€™s head, which may make them feel stuck and confined, and also prevent them from using assistive technologies like Pointer Control. + + * Minimize the need for large and repetitive gestures, as these can become tiresome and may be difficult depending on a personโ€™s surroundings. + + + + +For additional guidance, see [Create accessible spatial experiences](https://developer.apple.com/videos/play/wwdc2023/10034) and [Design considerations for vision and motion](https://developer.apple.com/videos/play/wwdc2023/10078). + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/accessibility#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/accessibility#Related) + +[Inclusion](https://developer.apple.com/design/human-interface-guidelines/inclusion) + +[Typography](https://developer.apple.com/design/human-interface-guidelines/typography) + +[VoiceOver](https://developer.apple.com/design/human-interface-guidelines/voiceover) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/accessibility#Developer-documentation) + +[Building accessible apps](https://developer.apple.com/accessibility/) + +[Accessibility framework](https://developer.apple.com/documentation/Accessibility) + +[Overview of Accessibility Nutrition Labels](https://devcms.apple.com/help/app-store-connect/manage-app-accessibility/overview-of-accessibility-nutrition-labels) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/accessibility#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/F5AEB5B6-FF48-4201-B110-A0EDA465F3B4/9961_wide_250x141_1x.jpg) Principles of inclusive app design ](https://developer.apple.com/videos/play/wwdc2025/316) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/163752B6-501D-4816-BA92-DBF33CCF0CD2/9917_wide_250x141_1x.jpg) Evaluate your app for Accessibility Nutrition Labels ](https://developer.apple.com/videos/play/wwdc2025/224) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/52E9AD54-DB4B-4BB0-93D9-7625A2A46A74/9166_wide_250x141_1x.jpg) Catch up on accessibility in SwiftUI ](https://developer.apple.com/videos/play/wwdc2024/10073) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/accessibility#Change-log) + +Date| Changes +---|--- +June 9, 2025| Added guidance and links for Assistive Access, Switch Control, and Accessibility Nutrition Labels. +March 7, 2025| Expanded and refined all guidance. Moved Dynamic Type guidance to the Typography page, and moved VoiceOver guidance to a new VoiceOver page. +June 10, 2024| Added a link to Appleโ€™s Unity plug-ins for supporting Dynamic Type. +December 5, 2023| Updated visionOS Zoom lens artwork. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/app-icons.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/app-icons.md new file mode 100644 index 00000000..0b4d62a4 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/app-icons.md @@ -0,0 +1,210 @@ +--- +title: "App icons | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/app-icons + +# App icons + +A unique, memorable icon expresses your appโ€™s or gameโ€™s purpose and personality and helps people recognize it at a glance. + +![A sketch of the App Store icon. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/05b8bbb4aac9f98ba8c77876fe5068b7/foundations-app-icons-intro%402x.png) + +Your app icon is a crucial aspect of your appโ€™s or gameโ€™s branding and user experience. It appears on the Home Screen and in key locations throughout the system, including search results, notifications, system settings, and share sheets. A well-designed app icon conveys your appโ€™s or gameโ€™s identity clearly and consistently across all Apple platforms. + +![An image that shows three variations of the Photos app's app icon as it appears on different platforms. The first variation is a rounded rectangle shape, and represents the iOS, iPadOS, and macOS icons. The second variation is an elongated, rounded rectangular shape, and represents the tvOS icon. The third variation is a circular shape, and represents the visionOS and watchOS icons. All variations have the same overall design over different background shapes.](https://docs-assets.developer.apple.com/published/298204fa29c2dc771deb8651963ce75a/app-icons-platform-appearance-overview%402x.png) + +## [Layer design](https://developer.apple.com/design/human-interface-guidelines/app-icons#Layer-design) + +Although you can provide a flattened image for your icon, layers give you the most control over how your icon design is represented. A layered app icon comes together to produce a sense of depth and vitality. On each platform, the system applies visual effects that respond to the environment and peopleโ€™s interactions. + +iOS, iPadOS, macOS, and watchOS app icons include a background layer and one or more foreground layers that coalesce to create dimensionality. These icons take on Liquid Glass attributes like specular highlights, frostiness, and translucency, which respond to changes in lighting and, in iOS and iPadOS, device movement. + +Video with custom controls. + +Content description: An animation of the Podcasts app icon for iOS. As the animation plays, the icon rotates to the side and expands to show how layers are separated. It then collapses and returns to its original position. + +Play + +iOS app icon + +tvOS app icons use between two and five layers to create a sense of dynamism as people bring them into focus. When focused, the app icon elevates to the foreground in response to someoneโ€™s finger movement on their remote, and gently sways while the surface illuminates. The separation between layers and the use of transparency produce a feeling of depth during the parallax effect. + +Video with custom controls. + +Content description: An animation of the Photos app icon in tvOS moving to show the parallax effect. + +Play + +tvOS app icon + +A visionOS app icon includes a background layer and one or two layers on top, producing a three-dimensional object that subtly expands when people view it. The system enhances the iconโ€™s visual dimensionality by adding shadows that convey a sense of depth between layers and by using the alpha channel of the upper layers to create an embossed appearance. + +Video with custom controls. + +Content description: An animation of the Photos app icon in visionOS moving to show the parallax effect. + +Play + +visionOS app icon + +You use your favorite design tool to craft the individual foreground layers of your app icon. For iOS, iPadOS, macOS, and watchOS icons, you then import your icon layers into Icon Composer, a design tool included with Xcode and available from the [Apple Developer website](https://developer.apple.com/icon-composer). In Icon Composer, you define the background layer for your icon, adjust your foreground layer placement, apply visual effects like transparency, define default, dark, clear, and tinted appearance variants, and export your icon for use in Xcode. For additional guidance, see [Creating your app icon using Icon Composer](https://developer.apple.com/documentation/Xcode/creating-your-app-icon-using-icon-composer). + +![A screenshot of the Photos app icon in Icon Composer.](https://docs-assets.developer.apple.com/published/3d4f8c4c6b744e77f32802201fb48fb7/app-icons-icon-composer-overview-photos%402x.png)Icon Composer + +For tvOS and visionOS app icons, you add your icon layers directly to an image stack in Xcode to form your complete icon. For developer guidance, see [Configuring your app icon using an asset catalog](https://developer.apple.com/documentation/Xcode/configuring-your-app-icon). + +**Prefer clearly defined edges in foreground layers.** To ensure system-drawn highlights and shadows look best, avoid soft and feathered edges on foreground layer shapes. + +**Vary opacity in foreground layers to increase the sense of depth and liveliness.** For example, the Photos icon separates its centerpiece into multiple layers that contain translucent pieces, bringing greater dynamism to the design. Importing fully opaque layers and adjusting transparency in Icon Composer lets you preview and make adjustments to your design based on how transparency and system effects impact one another. + +**Design a background that both stands out and emphasizes foreground content.** Subtle top-to-bottom, light-to-dark gradients tend to respond well to system lighting effects. Icon Composer supports solid colors and gradients for background layers, making it unnecessary to import custom background images in most cases. If you do import a background layer, make sure itโ€™s full-bleed and opaque. + +**Prefer vector graphics when bringing layers into Icon Composer.** Unlike raster images, vector graphics (such as SVG or PDF) scale gracefully and appear crisp at any size. Outline artwork and convert text to outline in your design. For mesh gradients and raster artwork, prefer PNG format because itโ€™s a lossless image format. + +## [Icon shape](https://developer.apple.com/design/human-interface-guidelines/app-icons#Icon-shape) + +An app iconโ€™s shape varies based on a platformโ€™s visual language. In iOS, iPadOS, and macOS, icons are square, and the system applies masking to produce rounded corners that precisely match the curvature of other rounded interface elements throughout the system and the bezel of the physical device itself. In tvOS, icons are rectangular, also with concentric edges. In visionOS and watchOS, icons are square and the system applies circular masking. + + * iOS, iPadOS, macOS + * tvOS + * visionOS, watchOS + + + +![An image of the Settings icon for iOS. The iOS, iPadOS, and macOS icon grid is overlaid on the icon to show how the icon's shape and its elements map to the grid.](https://docs-assets.developer.apple.com/published/a116649a6bdc5124779475fcd769caac/app-icons-settings-app-grid-square%402x.png) + +![An image of the Settings icon for tvOS. The tvOS icon grid is overlaid on the icon to show how the icon's shape and its elements map to the grid.](https://docs-assets.developer.apple.com/published/770ec58a9f9985410cdff8c38b8166ab/app-icons-settings-app-grid-rectangle%402x.png) + +![An image of the Settings icon for watchOS. The visionOS and watchOS icon grid is overlaid on the icon to show how the icon's shape and its elements map to the grid.](https://docs-assets.developer.apple.com/published/2ceefd0eeb7e039a43ab05fd4a5050fb/app-icons-settings-app-grid-circle%402x.png) + +**Produce appropriately shaped, unmasked layers.** The system masks all layer edges to produce an iconโ€™s final shape. For iOS, iPadOS, and macOS icons, provide square layers so the system can apply rounded corners. For visionOS and watchOS, provide square layers so the system can create the circular icon shape. For tvOS, provide rectangular layers so the system can apply rounded corners. Providing layers with pre-defined masking negatively impacts specular highlight effects and makes edges look jagged. + +**Keep primary content centered to avoid truncation when the system adjusts corners or applies masking.** Pay particular attention to centering content in visionOS and watchOS icons. To help with icon placement, use the grids in the app icon production templates, which you can find in [Apple Design Resources](https://developer.apple.com/design/resources/). + +## [Design](https://developer.apple.com/design/human-interface-guidelines/app-icons#Design) + +Embrace simplicity in your icon design. Simple icons tend to be easiest for people to understand and recognize. An icon with fine visual features might look busy when rendered with system-provided shadows and highlights, and details may be hard to discern at smaller sizes. Find a concept or element that captures the essence of your app or game, make it the core idea of your icon, and express it in a simple, unique way with a minimal number of shapes. Prefer a simple background, such as a solid color or gradient, that puts the emphasis on your primary design โ€” you donโ€™t need to fill the entire icon canvas with content. + +![An image of the Podcasts app icon.](https://docs-assets.developer.apple.com/published/58a62b07273dbbc302df7a428103a16e/app-icons-embrace-simplicity-podcasts%402x.png)The Podcasts app icon + +![An image of the Home app icon.](https://docs-assets.developer.apple.com/published/4932ee4d526fc1b112e611f610a18b08/app-icons-embrace-simplicity-home%402x.png)The Home app icon + +**Provide a visually consistent icon design across all the platforms your app supports.** A consistent design helps people quickly find your app wherever it appears and prevents people from mistaking your app for multiple apps. + +**Consider basing your icon design around filled, overlapping shapes.** Overlapping solid shapes in the foreground, particularly when paired with transparency and blurring, can give an icon a sense of depth. + +![An illustration of two circles centered above a grid. One circle encloses the other. The inner circle has a solid fill. The outer circle is larger than the inner circle, allowing some space between them. The outer circle has no fill and shows just an outline.](https://docs-assets.developer.apple.com/published/6b02e91996a97adb2dbe53a8131cc380/app-icons-element-outline-shape%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration of two circles centered above a grid. One circle encloses the other. The inner circle has a solid fill. The outer circle is larger than the inner circle, has no outline, and has a semi-transparent fill that allows the background grid to show through. Together, the two circles give the impression that the inner circle is resting upon the outer circle.](https://docs-assets.developer.apple.com/published/a8d0e9d7b802123c594cf9910fb44a50/app-icons-element-filled-shape%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +**Include text only when itโ€™s essential to your experience or brand.** Text in icons doesnโ€™t support accessibility or localization, is often too small to read easily, and can make an icon appear cluttered. In some contexts, your app name already appears nearby, making it redundant to display the name within the icon itself. Although displaying a mnemonic like the first letter of your appโ€™s name can help people recognize your app or game, avoid including nonessential words that tell people what to do with it โ€” like โ€œWatchโ€ or โ€œPlayโ€ โ€” or context-specific terms like โ€œNewโ€ or โ€œFor visionOS.โ€ If you include text in a tvOS app icon, make sure itโ€™s above other layers so itโ€™s not cropped by the parallax effect. + +**Prefer illustrations to photos and avoid replicating UI components.** Photos are full of details that donโ€™t work well when displayed in different appearances, viewed at small sizes, or split into layers. Instead of using photos, create a graphic representation of the content that emphasizes the features you want people to notice. Similarly, if your app has an interface that people recognize, donโ€™t just replicate standard UI components or use app screenshots in your icon. + +**Donโ€™t use replicas of Apple hardware products.** Apple products are copyrighted and canโ€™t be reproduced in your app icons. + +## [Visual effects](https://developer.apple.com/design/human-interface-guidelines/app-icons#Visual-effects) + +**Let the system handle blurring and other visual effects.** The system dynamically applies visual effects to your app icon layers, so thereโ€™s no need to include specular highlights, drop shadows between layers, beveled edges, blurs, glows, and other effects. In addition to interfering with system-provided effects, custom effects are static, whereas the system supplies dynamic ones. If you do include custom visual effects on your icon layers, use them intentionally and test carefully with Icon Composer, in Simulator, or on device to make sure they appear as expected and donโ€™t conflict with system effects. + +**Create layer groupings to apply effects to multiple layers at once.** System effects typically occur on individual layers. If it makes sense for your design, however, you can group several layers together in Icon Composer or your design tool so effects occur at the group level. + +## [Appearances](https://developer.apple.com/design/human-interface-guidelines/app-icons#Appearances) + +In iOS, iPadOS, and macOS, people can choose whether their Home Screen app icons are default, dark, clear, or tinted in appearance. For example, someone may want to personalize their app icon appearance to complement their wallpaper. You can design app icon variants for every appearance variant, and the system automatically generates variants you donโ€™t provide. + +![A grid showing the six different appearances of the Photos app icon in iOS. The top row shows the default, clear light, and tinted light icon variants. The bottom row shows the dark, clear dark, and tinted dark variants.](https://docs-assets.developer.apple.com/published/a91b68946df73b81596a9a29b0356a4a/app-icons-rendering-modes%402x.png) + +**Keep your iconโ€™s features consistent across appearances.** To create a seamless experience, keep your iconโ€™s core visual features the same in the default, dark, clear, and tinted appearances. Avoid creating custom icon variants that swap elements in and out with each variant, which may make it harder for people to find your app when they switch appearances. + +**Design dark and tinted icons that feel at home beside system app icons and widgets.** You can preserve the color palette of your default icon, but be mindful that dark icons are more subdued, and clear and tinted icons are even more so. A great app icon is visible, legible, and recognizable, regardless of its appearance variant. + +**Use your light app icon as the basis for your dark icon.** Choose complementary colors that reflect the default design, and avoid excessively bright images. Color backgrounds generally offer the greatest contrast in dark icons. For guidance, see [Dark Mode](https://developer.apple.com/design/human-interface-guidelines/dark-mode). + +**Consider offering alternate app icons.** In iOS, iPadOS, tvOS, and compatible apps running in visionOS, itโ€™s possible to let people visit your appโ€™s settings to choose an alternate version of your app icon. For example, a sports app might offer icons for different teams, letting someone choose their favorite. If you offer this capability, make sure each icon you design remains closely related to your content and experience. Avoid creating one someone might mistake for another app. + +Note + +Alternate app icons in iOS and iPadOS require their own dark, clear, and tinted variants. As with your default app icon, all alternate and variant icons are subject to app review and must adhere to the [App Review Guidelines](https://developer.apple.com/app-store/review/guidelines/#design). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/app-icons#Platform-considerations) + + _No additional considerations for iOS, iPadOS, or macOS._ + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/app-icons#tvOS) + +**Include a safe zone to ensure the system doesnโ€™t crop your content.** When someone focuses your app icon, the system may crop content around the edges as the icon scales and moves. To ensure that your iconโ€™s content is always visible, keep a safe zone around it. Be aware that the safe zone can vary, depending on the image size, layer depth, and motion, and the system crops foreground layers more than background layers. + +![A diagram of the Photos app icon in tvOS with a white dotted line inside the outer border, which indicates the safe zone.](https://docs-assets.developer.apple.com/published/f2f3bf70c87e53889768b64a2faf5cf5/tvos-app-icon-safe-zone%402x.png) + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/app-icons#visionOS) + +**Avoid adding a shape thatโ€™s intended to look like a hole or concave area to the background layer.** The system-added shadow and specular highlights can make such a shape stand out instead of recede. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/app-icons#watchOS) + +**Avoid using black for your iconโ€™s background.** Lighten a black background so the icon doesnโ€™t blend into the display background. + +## [Specifications](https://developer.apple.com/design/human-interface-guidelines/app-icons#Specifications) + +The layout, size, style, and appearances of app icons vary by platform. + +Platform| Layout shape| Icon shape after system masking| Layout size| Style| Appearances +---|---|---|---|---|--- +iOS, iPadOS, macOS| Square| Rounded rectangle (square)| 1024x1024 px| Layered| Default, dark, clear light, clear dark, tinted light, tinted dark +tvOS| Rectangle (landscape)| Rounded rectangle (rectangular)| 800x480 px| Layered (Parallax)| N/A +visionOS| Square| Circular| 1024x1024 px| Layered (3D)| N/A +watchOS| Square| Circular| 1088x1088 px| Layered| N/A + +The system automatically scales your icon to produce smaller variants that appear in certain locations, such as Settings and notifications. + +App icons support the following color spaces: + + * sRGB (color) + + * Gray Gamma 2.2 (grayscale) + + * Display P3 (wide-gamut color in iOS, iPadOS, macOS, tvOS, and watchOS only) + + + + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/app-icons#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/app-icons#Related) + +[Apple Design Resources](https://developer.apple.com/design/resources/) + +[Icon Composer](https://developer.apple.com/icon-composer/) + +[Icons](https://developer.apple.com/design/human-interface-guidelines/icons) + +[Images](https://developer.apple.com/design/human-interface-guidelines/images) + +[Dark Mode](https://developer.apple.com/design/human-interface-guidelines/dark-mode) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/app-icons#Developer-documentation) + +[Creating your app icon using Icon Composer](https://developer.apple.com/documentation/Xcode/creating-your-app-icon-using-icon-composer) + +[Configuring your app icon using an asset catalog](https://developer.apple.com/documentation/Xcode/configuring-your-app-icon) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/app-icons#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/2C0F206D-6728-49F7-940E-DC5BC5C51B54/9911_wide_250x141_1x.jpg) Say hello to the new look of app icons ](https://developer.apple.com/videos/play/wwdc2025/220) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/93AA149B-9ACF-4281-8BAF-5AFF7CFA1CF0/10087_wide_250x141_1x.jpg) Create icons with Icon Composer ](https://developer.apple.com/videos/play/wwdc2025/361) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/app-icons#Change-log) + +Date| Changes +---|--- +June 9, 2025| Updated guidance to reflect layered icons, consistency across platforms, and best practices for Liquid Glass. +June 10, 2024| Added guidance for creating dark and tinted app icon variants for iOS and iPadOS. +January 31, 2024| Clarified platform availability for alternate app icons. +June 21, 2023| Updated to include guidance for visionOS. +September 14, 2022| Added specifications for Apple Watch Ultra. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/branding.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/branding.md new file mode 100644 index 00000000..e8d70219 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/branding.md @@ -0,0 +1,44 @@ +--- +title: "Branding | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/branding + +# Branding + +Apps and games express their unique brand identity in ways that make them instantly recognizable while feeling at home on the platform and giving people a consistent experience. + +![A sketch of a megaphone, suggesting communication. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/8ea20e1bc15bc51d9242f39c27cbb0c6/foundations-branding-intro%402x.png) + +In addition to expressing your brand in your [app icon](https://developer.apple.com/design/human-interface-guidelines/app-icons) and throughout your experience, you have several opportunities to highlight it within the App Store. For guidance, see [App Store Marketing Guidelines](https://developer.apple.com/app-store/marketing/guidelines/). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/branding#Best-practices) + +**Use your brandโ€™s unique voice and tone in all the written communication you display.** For example, your brand might convey feelings of encouragement and optimism by using plain words, occasional exclamation marks and emoji, and simple sentence structures. + +**Consider choosing an accent color.** On most platforms, you can specify a color that the system applies to app elements like interface icons, buttons, and text. In macOS, people can also choose their own accent color that the system can use in place of the color an app specifies. For guidance, see [Color](https://developer.apple.com/design/human-interface-guidelines/color). + +**Consider using a custom font.** If your brand is strongly associated with a specific font, be sure that itโ€™s legible at all sizes and supports accessibility features like bold text and larger type. It can work well to use a custom font for headlines and subheadings while using a system font for body copy and captions, because the system fonts are designed for optimal legibility at small sizes. For guidance, see [Typography](https://developer.apple.com/design/human-interface-guidelines/typography). + +**Ensure branding always defers to content.** Using screen space for an element that does nothing but display a brand asset can mean thereโ€™s less room for the content people care about. Aim to incorporate branding in refined, unobtrusive ways that donโ€™t distract people from your experience. + +**Help people feel comfortable by using standard patterns consistently.** Even a highly stylized interface can be approachable if it maintains familiar behaviors. For example, place UI components in expected locations and use standard symbols to represent common actions. + +**Resist the temptation to display your logo throughout your app or game unless itโ€™s essential for providing context.** People seldom need to be reminded which app theyโ€™re using, and itโ€™s usually better to use the space to give people valuable information and controls. + +**Avoid using a launch screen as a branding opportunity.** Some platforms use a launch screen to minimize the startup experience, while simultaneously giving the app or game a little time to load resources (for guidance, see [Launch screens](https://developer.apple.com/design/human-interface-guidelines/launching#Launch-screens)). A launch screen disappears too quickly to convey any information, but you might consider displaying a welcome or onboarding screen that incorporates your branding content at the beginning of your experience. For guidance, see [Onboarding](https://developer.apple.com/design/human-interface-guidelines/onboarding). + +**Follow Appleโ€™s trademark guidelines.** Apple trademarks must not appear in your app name or images. See [Apple Trademark List](https://www.apple.com/legal/intellectual-property/trademark/appletmlist.html) and [Guidelines for Using Apple Trademarks](https://www.apple.com/legal/intellectual-property/guidelinesfor3rdparties.html). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/branding#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/branding#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/branding#Related) + +[Marketing resources and identity guidelines](https://developer.apple.com/app-store/marketing/guidelines/) + +[Show more with app previews](https://developer.apple.com/app-store/app-previews/) + +[Color](https://developer.apple.com/design/human-interface-guidelines/color) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/color.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/color.md new file mode 100644 index 00000000..8cfea29d --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/color.md @@ -0,0 +1,274 @@ +--- +title: "Color | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/color + +# Color + +Judicious use of color can enhance communication, evoke your brand, provide visual continuity, communicate status and feedback, and help people understand information. + +![A sketch of a paint palette, suggesting the use of color. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/10ec5551985c77cabaeaaaff016cdfd8/foundations-color-intro%402x.png) + +The system defines colors that look good on various backgrounds and appearance modes, and can automatically adapt to vibrancy and accessibility settings. Using system colors is a convenient way to make your experience feel at home on the device. + +You may also want to use custom colors to enhance the visual experience of your app or game and express its unique personality. The following guidelines can help you use color in ways that people appreciate, regardless of whether you use system-defined or custom colors. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/color#Best-practices) + +**Avoid using the same color to mean different things.** Use color consistently throughout your interface, especially when you use it to help communicate information like status or interactivity. For example, if you use your brand color to indicate that a borderless button is interactive, using the same or similar color to stylize noninteractive text is confusing. + +**Make sure all your appโ€™s colors work well in light, dark, and increased contrast contexts.** iOS, iPadOS, macOS, and tvOS offer both light and [dark](https://developer.apple.com/design/human-interface-guidelines/dark-mode) appearance settings. [System colors](https://developer.apple.com/design/human-interface-guidelines/color#System-colors) vary subtly depending on the system appearance, adjusting to ensure proper color differentiation and contrast for text, symbols, and other elements. With the Increase Contrast setting turned on, the color differences become far more apparent. When possible, use system colors, which already define variants for all these contexts. If you define a custom color, make sure to supply light and dark variants, and an increased contrast option for each variant that provides a significantly higher amount of visual differentiation. Even if your app ships in a single appearance mode, provide both light and dark colors to support Liquid Glass adaptivity in these contexts. + +![A screenshot of the Notes app in iOS with the light system appearance and default contrast. The Notes app is open to a note with the text 'Note'. The text is selected, which shows a yellow selection highlight and text editing menu. The Done button appears in the upper-right corner. The Liquid Glass background of the button is yellow, and its label, which shows a checkmark, is white. The shade of yellow is vibrant.](https://docs-assets.developer.apple.com/published/033f3f6540cc36385bc5993e2152895b/color-context-light-mode%402x.png) + +Default (light) + +![A screenshot of the Notes app in iOS with the light system appearance and increased contrast. The Notes app is open to a note with the text 'Note'. The text is selected, which shows a yellow selection highlight and text editing menu. The Done button appears in the upper-right corner. The Liquid Glass background of the button is yellow, and its label, which shows a checkmark, is black. The shade of yellow is darker to provide more contrast and differentiation against the note's white background.](https://docs-assets.developer.apple.com/published/9fa4e239f30421b0f00ee77dcace0c14/color-context-light-mode-high-contrast%402x.png) + +Increased contrast (light) + +![A screenshot of the Notes app in iOS with the dark system appearance and default contrast. The Notes app is open to a note with the text 'Note'. The text is selected, which shows a yellow selection highlight and text editing menu. The Done button appears in the upper-right corner. The Liquid Glass background of the button is yellow, and its label, which shows a checkmark, is white.](https://docs-assets.developer.apple.com/published/dc3523da3cba1dd53d3501c763335e6c/color-context-dark-mode%402x.png) + +Default (dark) + +![A screenshot of the Notes app in iOS with the dark system appearance and increased contrast. The Notes app is open to a note with the text 'Note'. The text is selected, which shows a yellow selection highlight and text editing menu. The Done button appears in the upper-right corner. The Liquid Glass background of the button is yellow, and its label, which shows a checkmark, is black.](https://docs-assets.developer.apple.com/published/95af2bc7dece914a5f870f38edac2998/color-context-dark-mode-high-contrast%402x.png) + +Increased contrast (dark) + +**Test your appโ€™s color scheme under a variety of lighting conditions.** Colors can look different when you view your app outside on a sunny day or in dim light. In bright surroundings, colors look darker and more muted. In dark environments, colors appear bright and saturated. In visionOS, colors can look different depending on the colors of a wall or object in a personโ€™s physical surroundings and how it reflects light. Adjust app colors to provide an optimal viewing experience in the majority of use cases. + +**Test your app on different devices.** For example, the True Tone display โ€” available on certain iPhone, iPad, and Mac models โ€” uses ambient light sensors to automatically adjust the white point of the display to adapt to the lighting conditions of the current environment. Apps that primarily support reading, photos, video, and gaming can strengthen or weaken this effect by specifying a white point adaptivity style (for developer guidance, see [`UIWhitePointAdaptivityStyle`](https://developer.apple.com/documentation/BundleResources/Information-Property-List/UIWhitePointAdaptivityStyle)). Test tvOS apps on multiple brands of HD and 4K TVs, and with different display settings. You can also test the appearance of your app using different color profiles on a Mac โ€” such as P3 and Standard RGB (sRGB) โ€” by choosing a profile in System Settings > Displays. For guidance, see [Color management](https://developer.apple.com/design/human-interface-guidelines/color#Color-management). + +**Consider how artwork and translucency affect nearby colors.** Variations in artwork sometimes warrant changes to nearby colors to maintain visual continuity and prevent interface elements from becoming overpowering or underwhelming. Maps, for example, displays a light color scheme when in map mode but switches to a dark color scheme when in satellite mode. Colors can also appear different when placed behind or applied to a translucent element like a toolbar. + +**If your app lets people choose colors, prefer system-provided color controls where available.** Using built-in color pickers provides a consistent user experience, in addition to letting people save a set of colors they can access from any app. For developer guidance, see [`ColorPicker`](https://developer.apple.com/documentation/SwiftUI/ColorPicker). + +## [Inclusive color](https://developer.apple.com/design/human-interface-guidelines/color#Inclusive-color) + +**Avoid relying solely on color to differentiate between objects, indicate interactivity, or communicate essential information.** When you use color to convey information, be sure to provide the same information in alternative ways so people with color blindness or other visual disabilities can understand it. For example, you can use text labels or glyph shapes to identify objects or states. + +**Avoid using colors that make it hard to perceive content in your app.** For example, insufficient contrast can cause icons and text to blend with the background and make content hard to read, and people who are color blind might not be able to distinguish some color combinations. For guidance, see [Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility). + +**Consider how the colors you use might be perceived in other countries and cultures.** For example, red communicates danger in some cultures, but has positive connotations in other cultures. Make sure the colors in your app send the message you intend. + +![An illustration of an upward-trending stock chart in the Stocks app in English. The line of the graph is green to indicate the rising value of the stock during the selected time period.](https://docs-assets.developer.apple.com/published/5969ae10a6eaca6879fb43df4f651e4d/color-inclusive-color-charts-english%402x.png)Green indicates a positive trend in the Stocks app in English. + +![An illustration of an upward-trending stock chart in the Stocks app in Chinese. The line of the graph is red to indicate the rising value of the stock during the selected time period.](https://docs-assets.developer.apple.com/published/e84b6e7089f1fb8f73712da462d66164/color-inclusive-color-charts-chinese%402x.png)Red indicates a positive trend in the Stocks app in Chinese. + +## [System colors](https://developer.apple.com/design/human-interface-guidelines/color#System-colors) + +**Avoid hard-coding system color values in your app.** Documented color values are for your reference during the app design process. The actual color values may fluctuate from release to release, based on a variety of environmental variables. Use APIs like [`Color`](https://developer.apple.com/documentation/SwiftUI/Color) to apply system colors. + +iOS, iPadOS, macOS, and visionOS also define sets of _dynamic system colors_ that match the color schemes of standard UI components and automatically adapt to both light and dark contexts. Each dynamic color is semantically defined by its purpose, rather than its appearance or color values. For example, some colors represent view backgrounds at different levels of hierarchy and other colors represent foreground content, such as labels, links, and separators. + +**Avoid redefining the semantic meanings of dynamic system colors.** To ensure a consistent experience and ensure your interface looks great when the appearance of the platform changes, use dynamic system colors as intended. For example, donโ€™t use the [separator](https://developer.apple.com/documentation/uikit/uicolor/separator) color as a text color, or [secondary text label](https://developer.apple.com/documentation/uikit/uicolor/secondarylabel) color as a background color. + +## [Liquid Glass color](https://developer.apple.com/design/human-interface-guidelines/color#Liquid-Glass-color) + +By default, [Liquid Glass](https://developer.apple.com/design/human-interface-guidelines/materials#Liquid-Glass) has no inherent color, and instead takes on colors from the content directly behind it. You can apply color to some Liquid Glass elements, giving them the appearance of colored or stained glass. This is useful for drawing emphasis to a specific control, like a primary call to action, and is the approach the system uses for prominent button styling. Symbols or text labels on Liquid Glass controls can also have color. + +![A screenshot of the Done button in iOS, which appears as a checkmark on a blue Liquid Glass background.](https://docs-assets.developer.apple.com/published/df4d0a0bca32edb16d7ff86e34d6fe2d/color-liquid-glass-overview-tinted%402x.png)Controls can use color in the Liquid Glass background, like in a primary action button. + +![A screenshot of a tab bar in iOS, with the first tab selected. The symbol and text label of the selected tab bar item are blue.](https://docs-assets.developer.apple.com/published/5a9078b2ea4baec1f15773638c9377c6/color-liquid-glass-overview-color-over-tab-bar%402x.png)Symbols and text that appear on Liquid Glass can have color, like in a selected tab bar item. + +![A screenshot of the Share button in iOS over a colorful image. The colors from the background image infuse the Liquid Glass in the button, affecting its color.](https://docs-assets.developer.apple.com/published/9cf610d972c97dee46b9e206525b2ae7/color-liquid-glass-overview-clear%402x.png)By default, Liquid Glass picks up the color from the content layer behind it. + +For smaller elements like toolbars and tab bars, the system can adapt Liquid Glass between a light and dark appearance in response to the underlying content. By default, symbols and text on these elements follow a monochromatic color scheme, becoming darker when the underlying content is light, and lighter when itโ€™s dark. Liquid Glass appears more opaque in larger elements like sidebars to preserve legibility over complex backgrounds and accommodate richer content on the materialโ€™s surface. + +**Apply color sparingly to the Liquid Glass material, and to symbols or text on the material.** If you apply color, reserve it for elements that truly benefit from emphasis, such as status indicators or primary actions. To emphasize primary actions, apply color to the background rather than to symbols or text. For example, the system applies the app accent color to the background in prominent buttons โ€” such as the Done button โ€” to draw attention and elevate their visual prominence. Refrain from adding color to the background of multiple controls. + +![A screenshot of the top half of an iPhone app that shows a toolbar with several buttons. All of the buttons in the toolbar use a blue accent color for their Liquid Glass background.](https://docs-assets.developer.apple.com/published/9b7b9adb67ee5f70839540534fdeb374/colors-liquid-glass-usage-incorrect%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![A screenshot of the top half of an iPhone app that shows a toolbar with several buttons. Only the Done button in the toolbar uses a blue accent color for its Liquid Glass background.](https://docs-assets.developer.apple.com/published/3897d0d7c8736728d130dcc820e9a688/colors-liquid-glass-usage-correct%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +**Avoid using similar colors in control labels if your app has a colorful background.** While color can make apps more visually appealing, playful, or reflective of your brand, too much color can be overwhelming and make control labels more difficult to read. If your app features colorful backgrounds or visually rich content, prefer a monochromatic appearance for toolbars and tab bars, or choose an accent color with sufficient visual differentiation. By contrast, in apps with primarily monochromatic content or backgrounds, choosing your brand color as the app accent color can be an effective way to tailor your app experience and reflect your companyโ€™s identity. + +**Be aware of the placement of color in the content layer.** Make sure your interface maintains sufficient contrast by avoiding overlap of similar colors in the content layer and controls when possible. Although colorful content might intermittently scroll underneath controls, make sure its default or resting state โ€” like the top of a screen of scrollable content โ€” maintains clear legibility. + +## [Color management](https://developer.apple.com/design/human-interface-guidelines/color#Color-management) + +A _color space_ represents the colors in a _color model_ like RGB or CMYK. Common color spaces โ€” sometimes called _gamuts_ โ€” are sRGB and Display P3. + +![Diagram showing the colors included in the sRGB space, compared to the larger number of colors included in the P3 color space.](https://docs-assets.developer.apple.com/published/c10d0ec4c78a6b824552058caac031b5/color-graphic-wide-color%402x.png) + +A _color profile_ describes the colors in a color space using, for example, mathematical formulas or tables of data that map colors to numerical representations. An image embeds its color profile so that a device can interpret the imageโ€™s colors correctly and reproduce them on a display. + +**Apply color profiles to your images.** Color profiles help ensure that your appโ€™s colors appear as intended on different displays. The sRGB color space produces accurate colors on most displays. + +**Use wide color to enhance the visual experience on compatible displays.** Wide color displays support a P3 color space, which can produce richer, more saturated colors than sRGB. As a result, photos and videos that use wide color are more lifelike, and visual data and status indicators that use wide color can be more meaningful. When appropriate, use the Display P3 color profile at 16 bits per pixel (per channel) and export images in PNG format. Note that you need to use a wide color display to design wide color images and select P3 colors. + +**Provide color spaceโ€“specific image and color variations if necessary.** In general, P3 colors and images appear fine on sRGB displays. Occasionally, it may be hard to distinguish two very similar P3 colors when viewing them on an sRGB display. Gradients that use P3 colors can also sometimes appear clipped on sRGB displays. To avoid these issues and to ensure visual fidelity on both wide color and sRGB displays, you can use the asset catalog of your Xcode project to provide different versions of images and colors for each color space. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/color#Platform-considerations) + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/color#iOS-iPadOS) + +iOS defines two sets of dynamic background colors โ€” _system_ and _grouped_ โ€” each of which contains primary, secondary, and tertiary variants that help you convey a hierarchy of information. In general, use the grouped background colors ([`systemGroupedBackground`](https://developer.apple.com/documentation/UIKit/UIColor/systemGroupedBackground), [`secondarySystemGroupedBackground`](https://developer.apple.com/documentation/UIKit/UIColor/secondarySystemGroupedBackground), and [`tertiarySystemGroupedBackground`](https://developer.apple.com/documentation/UIKit/UIColor/tertiarySystemGroupedBackground)) when you have a grouped table view; otherwise, use the system set of background colors ([`systemBackground`](https://developer.apple.com/documentation/UIKit/UIColor/systemBackground), [`secondarySystemBackground`](https://developer.apple.com/documentation/UIKit/UIColor/secondarySystemBackground), and [`tertiarySystemBackground`](https://developer.apple.com/documentation/UIKit/UIColor/tertiarySystemBackground)). + +With both sets of background colors, you generally use the variants to indicate hierarchy in the following ways: + + * Primary for the overall view + + * Secondary for grouping content or elements within the overall view + + * Tertiary for grouping content or elements within secondary elements + + + + +For foreground content, iOS defines the following dynamic colors: + +Color| Use forโ€ฆ| UIKit API +---|---|--- +Label| A text label that contains primary content.| [`label`](https://developer.apple.com/documentation/UIKit/UIColor/label) +Secondary label| A text label that contains secondary content.| [`secondaryLabel`](https://developer.apple.com/documentation/UIKit/UIColor/secondaryLabel) +Tertiary label| A text label that contains tertiary content.| [`tertiaryLabel`](https://developer.apple.com/documentation/UIKit/UIColor/tertiaryLabel) +Quaternary label| A text label that contains quaternary content.| [`quaternaryLabel`](https://developer.apple.com/documentation/UIKit/UIColor/quaternaryLabel) +Placeholder text| Placeholder text in controls or text views.| [`placeholderText`](https://developer.apple.com/documentation/UIKit/UIColor/placeholderText) +Separator| A separator that allows some underlying content to be visible.| [`separator`](https://developer.apple.com/documentation/UIKit/UIColor/separator) +Opaque separator| A separator that doesnโ€™t allow any underlying content to be visible.| [`opaqueSeparator`](https://developer.apple.com/documentation/UIKit/UIColor/opaqueSeparator) +Link| Text that functions as a link.| [`link`](https://developer.apple.com/documentation/UIKit/UIColor/link) + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/color#macOS) + +macOS defines the following dynamic system colors (you can also view them in the Developer palette of the standard Color panel): + +Color| Use forโ€ฆ| AppKit API +---|---|--- +Alternate selected control text color| The text on a selected surface in a list or table.| [`alternateSelectedControlTextColor`](https://developer.apple.com/documentation/AppKit/NSColor/alternateSelectedControlTextColor) +Alternating content background colors| The backgrounds of alternating rows or columns in a list, table, or collection view.| [`alternatingContentBackgroundColors`](https://developer.apple.com/documentation/AppKit/NSColor/alternatingContentBackgroundColors) +Control accent| The accent color people select in System Settings.| [`controlAccentColor`](https://developer.apple.com/documentation/AppKit/NSColor/controlAccentColor) +Control background color| The background of a large interface element, such as a browser or table.| [`controlBackgroundColor`](https://developer.apple.com/documentation/AppKit/NSColor/controlBackgroundColor) +Control color| The surface of a control.| [`controlColor`](https://developer.apple.com/documentation/AppKit/NSColor/controlColor) +Control text color| The text of a control that is available.| [`controlTextColor`](https://developer.apple.com/documentation/AppKit/NSColor/controlTextColor) +Current control tint| The system-defined control tint.| [`currentControlTint`](https://developer.apple.com/documentation/AppKit/NSColor/currentControlTint) +Unavailable control text color| The text of a control thatโ€™s unavailable.| [`disabledControlTextColor`](https://developer.apple.com/documentation/AppKit/NSColor/disabledControlTextColor) +Find highlight color| The color of a find indicator.| [`findHighlightColor`](https://developer.apple.com/documentation/AppKit/NSColor/findHighlightColor) +Grid color| The gridlines of an interface element, such as a table.| [`gridColor`](https://developer.apple.com/documentation/AppKit/NSColor/gridColor) +Header text color| The text of a header cell in a table.| [`headerTextColor`](https://developer.apple.com/documentation/AppKit/NSColor/headerTextColor) +Highlight color| The virtual light source onscreen.| [`highlightColor`](https://developer.apple.com/documentation/AppKit/NSColor/highlightColor) +Keyboard focus indicator color| The ring that appears around the currently focused control when using the keyboard for interface navigation.| [`keyboardFocusIndicatorColor`](https://developer.apple.com/documentation/AppKit/NSColor/keyboardFocusIndicatorColor) +Label color| The text of a label containing primary content.| [`labelColor`](https://developer.apple.com/documentation/AppKit/NSColor/labelColor) +Link color| A link to other content.| [`linkColor`](https://developer.apple.com/documentation/AppKit/NSColor/linkColor) +Placeholder text color| A placeholder string in a control or text view.| [`placeholderTextColor`](https://developer.apple.com/documentation/AppKit/NSColor/placeholderTextColor) +Quaternary label color| The text of a label of lesser importance than a tertiary label, such as watermark text.| [`quaternaryLabelColor`](https://developer.apple.com/documentation/AppKit/NSColor/quaternaryLabelColor) +Secondary label color| The text of a label of lesser importance than a primary label, such as a label used to represent a subheading or additional information.| [`secondaryLabelColor`](https://developer.apple.com/documentation/AppKit/NSColor/secondaryLabelColor) +Selected content background color| The background for selected content in a key window or view.| [`selectedContentBackgroundColor`](https://developer.apple.com/documentation/AppKit/NSColor/selectedContentBackgroundColor) +Selected control color| The surface of a selected control.| [`selectedControlColor`](https://developer.apple.com/documentation/AppKit/NSColor/selectedControlColor) +Selected control text color| The text of a selected control.| [`selectedControlTextColor`](https://developer.apple.com/documentation/AppKit/NSColor/selectedControlTextColor) +Selected menu item text color| The text of a selected menu.| [`selectedMenuItemTextColor`](https://developer.apple.com/documentation/AppKit/NSColor/selectedMenuItemTextColor) +Selected text background color| The background of selected text.| [`selectedTextBackgroundColor`](https://developer.apple.com/documentation/AppKit/NSColor/selectedTextBackgroundColor) +Selected text color| The color for selected text.| [`selectedTextColor`](https://developer.apple.com/documentation/AppKit/NSColor/selectedTextColor) +Separator color| A separator between different sections of content.| [`separatorColor`](https://developer.apple.com/documentation/AppKit/NSColor/separatorColor) +Shadow color| The virtual shadow cast by a raised object onscreen.| [`shadowColor`](https://developer.apple.com/documentation/AppKit/NSColor/shadowColor) +Tertiary label color| The text of a label of lesser importance than a secondary label.| [`tertiaryLabelColor`](https://developer.apple.com/documentation/AppKit/NSColor/tertiaryLabelColor) +Text background color| The background color behind text.| [`textBackgroundColor`](https://developer.apple.com/documentation/AppKit/NSColor/textBackgroundColor) +Text color| The text in a document.| [`textColor`](https://developer.apple.com/documentation/AppKit/NSColor/textColor) +Under page background color| The background behind a documentโ€™s content.| [`underPageBackgroundColor`](https://developer.apple.com/documentation/AppKit/NSColor/underPageBackgroundColor) +Unemphasized selected content background color| The selected content in a non-key window or view.| [`unemphasizedSelectedContentBackgroundColor`](https://developer.apple.com/documentation/AppKit/NSColor/unemphasizedSelectedContentBackgroundColor) +Unemphasized selected text background color| A background for selected text in a non-key window or view.| [`unemphasizedSelectedTextBackgroundColor`](https://developer.apple.com/documentation/AppKit/NSColor/unemphasizedSelectedTextBackgroundColor) +Unemphasized selected text color| Selected text in a non-key window or view.| [`unemphasizedSelectedTextColor`](https://developer.apple.com/documentation/AppKit/NSColor/unemphasizedSelectedTextColor) +Window background color| The background of a window.| [`windowBackgroundColor`](https://developer.apple.com/documentation/AppKit/NSColor/windowBackgroundColor) +Window frame text color| The text in the windowโ€™s title bar area.| [`windowFrameTextColor`](https://developer.apple.com/documentation/AppKit/NSColor/windowFrameTextColor) + +#### [App accent colors](https://developer.apple.com/design/human-interface-guidelines/color#App-accent-colors) + +Beginning in macOS 11, you can specify an _accent color_ to customize the appearance of your appโ€™s buttons, selection highlighting, and sidebar icons. The system applies your accent color when the current value in General > Accent color settings is _multicolor_. + +![A screenshot of the accent color picker in the System Settings app.](https://docs-assets.developer.apple.com/published/93ebe4b08af4e94a5c4479459fc7905b/colors-accent-colors-picker-multicolor%402x.png) + +If people set their accent color setting to a value other than multicolor, the system applies their chosen color to the relevant items throughout your app, replacing your accent color. The exception is a sidebar icon that uses a fixed color you specify. Because a fixed-color sidebar icon uses a specific color to provide meaning, the system doesnโ€™t override its color when people change the value of accent color settings. For guidance, see [Sidebars](https://developer.apple.com/design/human-interface-guidelines/sidebars). + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/color#tvOS) + +**Consider choosing a limited color palette that coordinates with your app logo.** Subtle use of color can help you communicate your brand while deferring to the content. + +**Avoid using only color to indicate focus.** Subtle scaling and responsive animation are the primary ways to denote interactivity when an element is in focus. + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/color#visionOS) + +**Use color sparingly, especially on glass.** Standard visionOS windows typically use the system-defined glass [material](https://developer.apple.com/design/human-interface-guidelines/materials), which lets light and objects from peopleโ€™s physical surroundings and their space show through. Because the colors in these physical and virtual objects are visible through the glass, they can affect the legibility of colorful app content in the window. Prefer using color in places where it can help call attention to important information or show the relationship between parts of the interface. + +**Prefer using color in bold text and large areas.** Color in lightweight text or small areas can make them harder to see and understand. + +**In a fully immersive experience, help people maintain visual comfort by keeping brightness levels balanced.** Although using high contrast can help direct peopleโ€™s attention to important content, it can also cause visual discomfort if peopleโ€™s eyes have adjusted to low light or darkness. Consider making content fully bright only when the rest of the visual context is also bright. For example, avoid displaying a bright object on a very dark or black background, especially if the object flashes or moves. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/color#watchOS) + +**Use background color to support existing content or supply additional information.** Background color can establish a sense of place and help people recognize key content. For example, in Activity, each infographic view for the Move, Exercise, and Stand Activity rings has a background that matches the color of the ring. Use background color when you have something to communicate, rather than as a solely visual flourish. Avoid using full-screen background color in views that are likely to remain onscreen for long periods of time, such as in a workout or audio-playing app. + +**Recognize that people might prefer graphic complications to use tinted mode instead of full color.** The system can use a single color thatโ€™s based on the wearerโ€™s selected color in a graphic complicationโ€™s images, gauges, and text. For guidance, see [Complications](https://developer.apple.com/design/human-interface-guidelines/complications). + +## [Specifications](https://developer.apple.com/design/human-interface-guidelines/color#Specifications) + +### [System colors](https://developer.apple.com/design/human-interface-guidelines/color#System-colors) + +Name| SwiftUI API| Default (light)| Default (dark)| Increased contrast (light)| Increased contrast (dark) +---|---|---|---|---|--- +Red| [`red`](https://developer.apple.com/documentation/SwiftUI/Color/red)| ![R-255,G-56,B-60](https://docs-assets.developer.apple.com/published/56ba9eebe119d2e1b3063503a2eb45b7/colors-unified-red-light%402x.png)| ![R-255,G-66,B-69](https://docs-assets.developer.apple.com/published/9d7a7df4db48b0dcbd2915724d010235/colors-unified-red-dark%402x.png)| ![R-233,G-21,B-45](https://docs-assets.developer.apple.com/published/5b3473fcd986facfdee26a24601c7082/colors-unified-accessible-red-light%402x.png)| ![R-255,G-97,B-101](https://docs-assets.developer.apple.com/published/d097760a50a181eb7f688e9d62f4e710/colors-unified-accessible-red-dark%402x.png) +Orange| [`orange`](https://developer.apple.com/documentation/SwiftUI/Color/orange)| ![R-255,G-141,B-40](https://docs-assets.developer.apple.com/published/57f431ec786e31e33f578ace3dbb8c78/colors-unified-orange-light%402x.png)| ![R-255,G-146,B-48](https://docs-assets.developer.apple.com/published/e906c25c1cadcb9cf7514d01b83f3bb7/colors-unified-orange-dark%402x.png)| ![R-197,G-83,B-0](https://docs-assets.developer.apple.com/published/2222321d0b29cad6987f0f6e26d198c1/colors-unified-accessible-orange-light%402x.png)| ![R-255,G-160,B-86](https://docs-assets.developer.apple.com/published/c82984219db600ea8396f4fd1933fc19/colors-unified-accessible-orange-dark%402x.png) +Yellow| [`yellow`](https://developer.apple.com/documentation/SwiftUI/Color/yellow)| ![R-255,G-204,B-0](https://docs-assets.developer.apple.com/published/bebac431675840fa7e0e70cce0a6eb76/colors-unified-yellow-light%402x.png)| ![R-255,G-214,B-0](https://docs-assets.developer.apple.com/published/80c02086ccc5f013058932129cf9c6d3/colors-unified-yellow-dark%402x.png)| ![R-161,G-106,B-0](https://docs-assets.developer.apple.com/published/a51b94b82d9ea46e9de2ab8da5a57bbe/colors-unified-accessible-yellow-light%402x.png)| ![R-254,G-223,B-67](https://docs-assets.developer.apple.com/published/cd06b12d9e053739b089fb102b70901e/colors-unified-accessible-yellow-dark%402x.png) +Green| [`green`](https://developer.apple.com/documentation/SwiftUI/Color/green)| ![R-52,G-199,B-89](https://docs-assets.developer.apple.com/published/b4226cfcf596812d46bd084322f47e65/colors-unified-green-light%402x.png)| ![R-48,G-209,B-88](https://docs-assets.developer.apple.com/published/7724e5dd4f60d300eaffe45c9a5e1f9d/colors-unified-green-dark%402x.png)| ![R-0,G-137,B-50](https://docs-assets.developer.apple.com/published/51471c6578d192e9dae6f40d8ace1835/colors-unified-accessible-green-light%402x.png)| ![R-74,G-217,B-104](https://docs-assets.developer.apple.com/published/aff6bca03c74050c6b78015925c8fd21/colors-unified-accessible-green-dark%402x.png) +Mint| [`mint`](https://developer.apple.com/documentation/SwiftUI/Color/mint)| ![R-0,G-200,B-179](https://docs-assets.developer.apple.com/published/5d07acb38b9d0d7098f0b92456a7d27c/colors-unified-mint-light%402x.png)| ![R-0,G-218,B-195](https://docs-assets.developer.apple.com/published/851d8c0c2bea51a9377ae31520097e8c/colors-unified-mint-dark%402x.png)| ![R-0,G-133,B-117](https://docs-assets.developer.apple.com/published/d24198fce4dd42183e7b35abc9b67c20/colors-unified-accessible-mint-light%402x.png)| ![R-84,G-223,B-203](https://docs-assets.developer.apple.com/published/72586072586bb6d91589cc4ab78177b1/colors-unified-accessible-mint-dark%402x.png) +Teal| [`teal`](https://developer.apple.com/documentation/SwiftUI/Color/teal)| ![R-0,G-195,B-208](https://docs-assets.developer.apple.com/published/6b8e5d90758cc858b4d3e20110a31f53/colors-unified-teal-light%402x.png)| ![R-0,G-210,B-224](https://docs-assets.developer.apple.com/published/d02bd29f4ba3580e84756f8c332fd677/colors-unified-teal-dark%402x.png)| ![R-0,G-129,B-152](https://docs-assets.developer.apple.com/published/f2137be89fb79e4822b633a450d6fc2c/colors-unified-accessible-teal-light%402x.png)| ![R-59,G-221,B-236](https://docs-assets.developer.apple.com/published/9a76a2333c746ded944e6610a01d4daf/colors-unified-accessible-teal-dark%402x.png) +Cyan| [`cyan`](https://developer.apple.com/documentation/SwiftUI/Color/cyan)| ![R-0,G-192,B-232](https://docs-assets.developer.apple.com/published/3eb3076ca71a16ce1bede399e815e736/colors-unified-cyan-light%402x.png)| ![R-60,G-211,B-254](https://docs-assets.developer.apple.com/published/34399c5683f58d0710a50625f2fbca64/colors-unified-cyan-dark%402x.png)| ![R-0,G-126,B-174](https://docs-assets.developer.apple.com/published/e54287c8eb8d532283dac9d646886953/colors-unified-accessible-cyan-light%402x.png)| ![R-109,G-217,B-255](https://docs-assets.developer.apple.com/published/6d3ef826eb37c61642d57f798de4d14f/colors-unified-accessible-cyan-dark%402x.png) +Blue| [`blue`](https://developer.apple.com/documentation/SwiftUI/Color/blue)| ![R-0,G-136,B-255](https://docs-assets.developer.apple.com/published/6ea9cabe180214ed99be04320df3501b/colors-unified-blue-light%402x.png)| ![R-0,G-145,B-255](https://docs-assets.developer.apple.com/published/580c321f95c59b2b4479be066d24f10f/colors-unified-blue-dark%402x.png)| ![R-30,G-110,B-244](https://docs-assets.developer.apple.com/published/f46653318bcfae105ff78fe412d64da2/colors-unified-accessible-blue-light%402x.png)| ![R-92,G-184,B-255](https://docs-assets.developer.apple.com/published/07b7bcb2d65911636342cee25db1f953/colors-unified-accessible-blue-dark%402x.png) +Indigo| [`indigo`](https://developer.apple.com/documentation/SwiftUI/Color/indigo)| ![R-97,G-85,B-245](https://docs-assets.developer.apple.com/published/2da5c45a0e483dcaac4447464da4b6a7/colors-unified-indigo-light%402x.png)| ![R-109,G-124,B-255](https://docs-assets.developer.apple.com/published/b5e1fd9a1fc2347cc7238668b2df251b/colors-unified-indigo-dark%402x.png)| ![R-86,G-74,B-222](https://docs-assets.developer.apple.com/published/e326f52473ede4e5427208f9929196d9/colors-unified-accessible-indigo-light%402x.png)| ![R-167,G-170,B-255](https://docs-assets.developer.apple.com/published/d19249c65dab279c41f16c802365df10/colors-unified-accessible-indigo-dark%402x.png) +Purple| [`purple`](https://developer.apple.com/documentation/SwiftUI/Color/purple)| ![R-203,G-48,B-224](https://docs-assets.developer.apple.com/published/2f07dfc6c397fba6d0abda5f5051a025/colors-unified-purple-light%402x.png)| ![R-219,G-52,B-242](https://docs-assets.developer.apple.com/published/04bce86fef3077014010ce6cfceb659f/colors-unified-purple-dark%402x.png)| ![R-176,G-47,B-194](https://docs-assets.developer.apple.com/published/a63779bec8a313582e11c6bbe348fc10/colors-unified-accessible-purple-light%402x.png)| ![R-234,G-141,B-255](https://docs-assets.developer.apple.com/published/82c3b96b548cbc455ef685f3e44d01d1/colors-unified-accessible-purple-dark%402x.png) +Pink| [`pink`](https://developer.apple.com/documentation/SwiftUI/Color/pink)| ![R-255,G-45,B-85](https://docs-assets.developer.apple.com/published/1486931dce50d7610a397607afc0fb4d/colors-unified-pink-light%402x.png)| ![R-255,G-55,B-95](https://docs-assets.developer.apple.com/published/d68a9dbf37bab028b011f68fdd794e9c/colors-unified-pink-dark%402x.png)| ![R-231,G-18,B-77](https://docs-assets.developer.apple.com/published/d696af68031ce91a63330e0469ff592b/colors-unified-accessible-pink-light%402x.png)| ![R-255,G-138,B-196](https://docs-assets.developer.apple.com/published/a64993da9a61253e266e411d76c2cefd/colors-unified-accessible-pink-dark%402x.png) +Brown| [`brown`](https://developer.apple.com/documentation/SwiftUI/Color/brown)| ![R-172,G-127,B-94](https://docs-assets.developer.apple.com/published/366eca06d26c2f759d6200a1e9b0a56f/colors-unified-brown-light%402x.png)| ![R-183,G-138,B-102](https://docs-assets.developer.apple.com/published/df6c5da440560b2054af5b55fe9b87f4/colors-unified-brown-dark%402x.png)| ![R-149,G-109,B-81](https://docs-assets.developer.apple.com/published/c80a760835a2bc94a68337d0208a469e/colors-unified-accessible-brown-light%402x.png)| ![R-219,G-166,B-121](https://docs-assets.developer.apple.com/published/3c6062e007c9d60e4684d063b3618786/colors-unified-accessible-brown-dark%402x.png) + +visionOS system colors use the default dark color values. + +### [iOS, iPadOS system gray colors](https://developer.apple.com/design/human-interface-guidelines/color#iOS-iPadOS-system-gray-colors) + +Name| UIKit API| Default (light)| Default (dark)| Increased contrast (light)| Increased contrast (dark) +---|---|---|---|---|--- +Gray| [`systemGray`](https://developer.apple.com/documentation/UIKit/UIColor/systemGray)| ![R-142,G-142,B-147](https://docs-assets.developer.apple.com/published/cc1289b6fd4b76c79bbeda356463232a/ios-default-systemgray%402x.png)| ![R-142,G-142,B-147](https://docs-assets.developer.apple.com/published/cc1289b6fd4b76c79bbeda356463232a/ios-default-systemgraydark%402x.png)| ![R-108,G-108,B-112](https://docs-assets.developer.apple.com/published/5d86cbc8b4ddef8b68954882b4c87a18/ios-accessible-systemgray%402x.png)| ![R-174,G-174,B-178](https://docs-assets.developer.apple.com/published/d00617ff05181a53d2cb5ddf143d502e/ios-accessible-systemgraydark%402x.png) +Gray (2)| [`systemGray2`](https://developer.apple.com/documentation/UIKit/UIColor/systemGray2)| ![R-174,G-174,B-178](https://docs-assets.developer.apple.com/published/d00617ff05181a53d2cb5ddf143d502e/ios-default-systemgray2%402x.png)| ![R-99,G-99,B-102](https://docs-assets.developer.apple.com/published/1f681e808c0f4f35a2e7642872719c8b/ios-default-systemgray2dark%402x.png)| ![R-142,G-142,B-147](https://docs-assets.developer.apple.com/published/cc1289b6fd4b76c79bbeda356463232a/ios-accessible-systemgray2%402x.png)| ![R-124,G-124,B-128](https://docs-assets.developer.apple.com/published/f941ec556140a435aa9556a993e57e63/ios-accessible-systemgray2dark%402x.png) +Gray (3)| [`systemGray3`](https://developer.apple.com/documentation/UIKit/UIColor/systemGray3)| ![R-199,G-199,B-204](https://docs-assets.developer.apple.com/published/bcbb9fb97382e52aa09de7239a6edcf7/ios-default-systemgray3%402x.png)| ![R-72,G-72,B-74](https://docs-assets.developer.apple.com/published/d99ad33dcdd426585e7107e1b130d713/ios-default-systemgray3dark%402x.png)| ![R-174,G-174,B-178](https://docs-assets.developer.apple.com/published/d00617ff05181a53d2cb5ddf143d502e/ios-accessible-systemgray3%402x.png)| ![R-84,G-84,B-86](https://docs-assets.developer.apple.com/published/693c40b65e2752b3a2b7741d61ebbb3b/ios-accessible-systemgray3dark%402x.png) +Gray (4)| [`systemGray4`](https://developer.apple.com/documentation/UIKit/UIColor/systemGray4)| ![R-209,G-209,B-214](https://docs-assets.developer.apple.com/published/5e1c546e8c78d9700b1ee58ce3a39972/ios-default-systemgray4%402x.png)| ![R-58,G-58,B-60](https://docs-assets.developer.apple.com/published/983cdcdfa9a664db0c5ff7c09905582a/ios-default-systemgray4dark%402x.png)| ![R-188,G-188,B-192](https://docs-assets.developer.apple.com/published/93644725b33daf923f7e3a146e9b2d42/ios-accessible-systemgray4%402x.png)| ![R-68,G-68,B-70](https://docs-assets.developer.apple.com/published/6439d861c1fe8a41615d5f09d3cde938/ios-accessible-systemgray4dark%402x.png) +Gray (5)| [`systemGray5`](https://developer.apple.com/documentation/UIKit/UIColor/systemGray5)| ![R-229,G-229,B-234](https://docs-assets.developer.apple.com/published/91f296b3990bfe6dcd28b1804c803581/ios-default-systemgray5%402x.png)| ![R-44,G-44,B-46](https://docs-assets.developer.apple.com/published/a8b1d65979b02865c203f18019b1084d/ios-default-systemgray5dark%402x.png)| ![R-216,G-216,B-220](https://docs-assets.developer.apple.com/published/616159815cf002c39f570affa027c298/ios-accessible-systemgray5%402x.png)| ![R-54,G-54,B-56](https://docs-assets.developer.apple.com/published/aacb35c6af213ef544f77d26df56df39/ios-accessible-systemgray5dark%402x.png) +Gray (6)| [`systemGray6`](https://developer.apple.com/documentation/UIKit/UIColor/systemGray6)| ![R-242,G-242,B-247](https://docs-assets.developer.apple.com/published/3d60e2b1bf4771610453a31de912647b/ios-default-systemgray6%402x.png)| ![R-28,G-28,B-30](https://docs-assets.developer.apple.com/published/5d86f031014f556ef2d26da001c1f639/ios-default-systemgray6dark%402x.png)| ![R-235,G-235,B-240](https://docs-assets.developer.apple.com/published/82102708ad5dc7921fc0473f6ace4613/ios-accessible-systemgray6%402x.png)| ![R-36,G-36,B-38](https://docs-assets.developer.apple.com/published/5dc6249020925c5ec09f88f8adc9bbaa/ios-accessible-systemgray6dark%402x.png) + +In SwiftUI, the equivalent of `systemGray` is [`gray`](https://developer.apple.com/documentation/SwiftUI/Color/gray). + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/color#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/color#Related) + +[Dark Mode](https://developer.apple.com/design/human-interface-guidelines/dark-mode) + +[Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility) + +[Materials](https://developer.apple.com/design/human-interface-guidelines/materials) + +[Apple Design Resources](https://developer.apple.com/design/resources/) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/color#Developer-documentation) + +[`Color`](https://developer.apple.com/documentation/SwiftUI/Color) โ€” SwiftUI + +[`UIColor`](https://developer.apple.com/documentation/UIKit/UIColor) โ€” UIKit + +[Color](https://developer.apple.com/documentation/AppKit/color) โ€” AppKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/color#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/5CD0E251-424E-490F-89CF-1E64848209A6/9910_wide_250x141_1x.jpg) Meet Liquid Glass ](https://developer.apple.com/videos/play/wwdc2025/219) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/color#Change-log) + +Date| Changes +---|--- +December 16, 2025| Updated guidance for Liquid Glass. +June 9, 2025| Updated system color values, and added guidance for Liquid Glass. +February 2, 2024| Distinguished UIKit and SwiftUI gray colors in iOS and iPadOS, and added guidance for balancing brightness levels in visionOS apps. +September 12, 2023| Enhanced guidance for using background color in watchOS views, and added color swatches for tvOS. +June 21, 2023| Updated to include guidance for visionOS. +June 5, 2023| Updated guidance for using background color in watchOS. +December 19, 2022| Corrected RGB values for system mint color (Dark Mode) in iOS and iPadOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/dark-mode.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/dark-mode.md new file mode 100644 index 00000000..a7ad3cac --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/dark-mode.md @@ -0,0 +1,116 @@ +--- +title: "Dark Mode | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/dark-mode + +# Dark Mode + +Dark Mode is a systemwide appearance setting that uses a dark color palette to provide a comfortable viewing experience tailored for low-light environments. + +![A sketch of concentric circles with half-filled areas, suggesting the presence of light and dark. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/f354bd96f1890df83e7f8e31835f80bc/foundations-dark-mode-intro%402x.png) + +In iOS, iPadOS, macOS, and tvOS, people often choose Dark Mode as their default interface style, and they generally expect all apps and games to respect their preference. In Dark Mode, the system uses a dark color palette for all screens, views, menus, and controls, and may also use greater perceptual contrast to make foreground content stand out against the darker backgrounds. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/dark-mode#Best-practices) + +**Avoid offering an app-specific appearance setting.** An app-specific appearance mode option creates more work for people because they have to adjust more than one setting to get the appearance they want. Worse, they may think your app is broken because it doesnโ€™t respond to their systemwide appearance choice. + +**Ensure that your app looks good in both appearance modes.** In addition to using one mode or the other, people can choose the Auto appearance setting, which switches between the light and dark appearances as conditions change throughout the day, potentially while your app is running. + +**Test your content to make sure that it remains comfortably legible in both appearance modes.** For example, in Dark Mode with Increase Contrast and Reduce Transparency turned on (both separately and together), you may find places where dark text is less legible when itโ€™s on a dark background. You might also find that turning on Increase Contrast in Dark Mode can result in reduced visual contrast between dark text and a dark background. Although people with strong vision might still be able to read lower contrast text, such text could be illegible for many. For guidance, see [Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility). + +**In rare cases, consider using only a dark appearance in the interface.** For example, it can make sense for an app that supports immersive media viewing to use a permanently dark appearance that lets the UI recede and helps people focus on the media. + +![A screenshot of the Stocks app on iPhone in its standard dark-only appearance, showing the Apple Inc. stock in detail. The view includes a summary of the current stock price along with a graph of its performance over the past year.](https://docs-assets.developer.apple.com/published/50e3d01e38e69e84976f7a1747321ba8/dark-mode-stocks-app-dark-only-mode%402x.png) + +The Stocks app uses a dark-only appearance + +## [Dark Mode colors](https://developer.apple.com/design/human-interface-guidelines/dark-mode#Dark-Mode-colors) + +The color palette in Dark Mode includes dimmer background colors and brighter foreground colors. Itโ€™s important to realize that these colors arenโ€™t necessarily inversions of their light counterparts: while many colors are inverted, some are not. For more information, see [Specifications](https://developer.apple.com/design/human-interface-guidelines/color#Specifications). + +**Embrace colors that adapt to the current appearance.** Semantic colors (like [`labelColor`](https://developer.apple.com/documentation/AppKit/NSColor/labelColor) and [`controlColor`](https://developer.apple.com/documentation/AppKit/NSColor/controlColor) in macOS or [`separator`](https://developer.apple.com/documentation/UIKit/UIColor/separator) in iOS and iPadOS) automatically adapt to the current appearance. When you need a custom color, add a Color Set asset to your appโ€™s asset catalog in Xcode, and specify the bright and dim variants of the color. Avoid using hard-coded color values or colors that donโ€™t adapt. + +![An illustration of a square with a light background and four color swatches representing system colors in the light appearance.](https://docs-assets.developer.apple.com/published/083d8f0f70c26b7fdea230f7da1edfeb/dark-mode-system-colors-light%402x.png)System colors in the light appearance + +![An illustration of a square with a dark background and four color swatches representing system colors in the dark appearance.](https://docs-assets.developer.apple.com/published/247df4f7b00e65cdd3827de84135fcda/dark-mode-system-colors-dark%402x.png)System colors in the dark appearance + +**Aim for sufficient color contrast in all appearances.** Using system-defined colors can help you achieve a good contrast ratio between your foreground and background content. At a minimum, make sure the contrast ratio between colors is no lower than 4.5:1. For custom foreground and background colors, strive for a contrast ratio of 7:1, especially in small text. This ratio ensures that your foreground content stands out from the background, and helps your content meet recommended accessibility guidelines. + +**Soften the color of white backgrounds.** If you display a content image that includes a white background, consider slightly darkening the image to prevent the background from glowing in the surrounding Dark Mode context. + +### [Icons and images](https://developer.apple.com/design/human-interface-guidelines/dark-mode#Icons-and-images) + +The system uses [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) (which automatically adapt to Dark Mode) and full-color images that are optimized for both the light and dark appearances. + +**Use SF Symbols wherever possible.** Symbols work well in both appearance modes when you use dynamic colors to tint them or when you add vibrancy. For guidance, see [Color](https://developer.apple.com/design/human-interface-guidelines/color). + +**Design separate interface icons for the light and dark appearances if necessary.** For example, an icon that depicts a full moon might need a subtle dark outline to contrast well with a light background, but need no outline when it displays on a dark background. Similarly, an icon that represents a drop of oil might need a slight border to make the edge visible against a dark background. + +![An illustration of a black droplet icon against a light background.](https://docs-assets.developer.apple.com/published/5377a16f9c47c32d5716a2de9e7e5ddb/dark-mode-icon-in-light-mode%402x.png)Icon in the light appearance with no border + +![An illustration of a black droplet icon against a dark background. The icon has a white border to distinguish it from the similar surrounding color.](https://docs-assets.developer.apple.com/published/a2ebe256a3e677367cc3e965e8282168/dark-mode-icon-in-dark-mode%402x.png)Icon in the dark appearance with border for better contrast + +**Make sure full-color images and icons look good in both appearances.** Use the same asset if it looks good in both the light and dark appearances. If an asset looks good in only one mode, modify the asset or create separate light and dark assets. Use asset catalogs to combine your assets into a single named image. + +![An illustration of two people sitting at a restaurant table done in a simple, abstract style. The illustration has a light background and its details are clearly visible.](https://docs-assets.developer.apple.com/published/017a90f0e42a841edec3d4238f408e9e/dark-mode-illustration-in-light-mode%402x.png)Illustration on a light background + +![An illustration of two people sitting at a restaurant table done in a simple, abstract style. The illustration has a dark background, and the darker portions of the image are hard to distinguish from the background.](https://docs-assets.developer.apple.com/published/97c07bc517069bf9175e7a3374ed95aa/dark-mode-illustration-in-dark-mode-incorrect%402x.png)On a dark background, the same illustration has poor contrast and many details are lost + +![An illustration of two people sitting at a restaurant table done in a simple, abstract style. The illustration has a dark background, and its color values are adjusted to be clearly visible in contrast to the background.](https://docs-assets.developer.apple.com/published/fa4aec31ae33aadce2ed0a0434c9c605/dark-mode-illustration-in-dark-mode-correct%402x.png)Illustration adjusted for better contrast on a dark background + +### [Text](https://developer.apple.com/design/human-interface-guidelines/dark-mode#Text) + +The system uses vibrancy and increased contrast to maintain the legibility of text on darker backgrounds. + +**Use the system-provided label colors for labels.** The primary, secondary, tertiary, and quaternary label colors adapt automatically to the light and dark appearances. + +![An illustration of a button in the light appearance with dark primary label text.](https://docs-assets.developer.apple.com/published/4dc33e45cd6cae3da766f885044174e9/dark-mode-label-in-light-mode%402x.png)Primary label in the light appearance + +![An illustration of a button in the dark appearance with light secondary label text.](https://docs-assets.developer.apple.com/published/5a2df784b29a55d1db485c30efb94009/dark-mode-label-in-dark-mode%402x.png)Secondary label in the dark appearance + +**Use system views to draw text fields and text views.** System views and controls make your appโ€™s text look good on all backgrounds, adjusting automatically for the presence or absence of vibrancy. When possible, use a system-provided view to display text instead of drawing the text yourself. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/dark-mode#Platform-considerations) + + _No additional considerations for tvOS. Dark Mode isnโ€™t supported in visionOS or watchOS._ + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/dark-mode#iOS-iPadOS) + +In Dark Mode, the system uses two sets of background colors โ€” called _base_ and _elevated_ โ€” to enhance the perception of depth when one dark interface is layered above another. The base colors are dimmer, making background interfaces appear to recede, and the elevated colors are brighter, making foreground interfaces appear to advance. + +![A diagram that shows a stack of 4 terms on top of a black background. The term at the top shows the most contrast with the background and the term at the bottom shows the least.](https://docs-assets.developer.apple.com/published/0d71ac9f5186541dce35b5f702311bd0/base-with-four-semantic-colors%402x.png)Base + +![A diagram that shows a stack of 4 terms on top of a nearly black background. The term at the top shows the most contrast with the background and the term at the bottom shows the least.](https://docs-assets.developer.apple.com/published/0dacc182adc819b08eb8cdcc897b08a4/elevated-with-four-semantic-colors%402x.png)Elevated + +![A diagram that shows a stack of 4 terms on top of a white background. The term at the top shows the most contrast with the background and the term at the bottom shows the least.](https://docs-assets.developer.apple.com/published/cbbe9a39049fd3d3d2122876de64d207/light-with-four-semantic-colors%402x.png)Light + +**Prefer the system background colors.** Dark Mode is dynamic, which means that the background color automatically changes from base to elevated when an interface is in the foreground, such as a popover or modal sheet. The system also uses the elevated background color to provide visual separation between apps in a multitasking environment and between windows in a multiple-window context. Using a custom background color can make it harder for people to perceive these system-provided visual distinctions. + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/dark-mode#macOS) + +When people choose the graphite accent color in General settings, macOS causes window backgrounds to pick up color from the current desktop picture. The result โ€” called _desktop tinting_ โ€” is a subtle effect that helps windows blend more harmoniously with their surrounding content. + +**Include some transparency in custom component backgrounds when appropriate.** Transparency lets your components pick up color from the window background when desktop tinting is active, creating a visual harmony that can persist even when the desktop picture changes. To help achieve this harmony, add transparency only to a custom component that has a visible background or bezel, and only when the component is in a neutral state, such as state that doesnโ€™t use color. You donโ€™t want to add transparency when the component is in a state that uses color, because doing so can cause the componentโ€™s color to fluctuate when the window background adjusts to a different location on the desktop or when the desktop picture changes. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/dark-mode#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/dark-mode#Related) + +[Color](https://developer.apple.com/design/human-interface-guidelines/color) + +[Materials](https://developer.apple.com/design/human-interface-guidelines/materials) + +[Typography](https://developer.apple.com/design/human-interface-guidelines/typography) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/dark-mode#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/5CD0E251-424E-490F-89CF-1E64848209A6/9910_wide_250x141_1x.jpg) Meet Liquid Glass ](https://developer.apple.com/videos/play/wwdc2025/219) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/48/174747D6-8723-4194-A932-7765179F1108/2949_wide_250x141_1x.jpg) Implementing Dark Mode on iOS ](https://developer.apple.com/videos/play/wwdc2019/214) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/dark-mode#Change-log) + +Date| Changes +---|--- +August 6, 2024| Added art contrasting the light and dark appearances. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/icons.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/icons.md new file mode 100644 index 00000000..69b46b4f --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/icons.md @@ -0,0 +1,263 @@ +--- +title: "Icons | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/icons + +# Icons + +An effective icon is a graphic asset that expresses a single concept in ways people instantly understand. + +![A sketch of the Command key icon. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/e71f139e5e50d9d10d91830b0af405c1/foundations-icons-intro%402x.png) + +Apps and games use a variety of simple icons to help people understand the items, actions, and modes they can choose. Unlike [app icons](https://developer.apple.com/design/human-interface-guidelines/app-icons), which can use rich visual details like shading, texturing, and highlighting to evoke the appโ€™s personality, an _interface icon_ typically uses streamlined shapes and touches of color to communicate a straightforward idea. + +You can design interface icons โ€” also called _glyphs_ โ€” or you can choose symbols from the SF Symbols app, using them as-is or customizing them to suit your needs. Both interface icons and symbols use black and clear colors to define their shapes; the system can apply other colors to the black areas in each image. For guidance, see [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/icons#Best-practices) + +**Create a recognizable, highly simplified design.** Too many details can make an interface icon confusing or unreadable. Strive for a simple, universal design that most people will recognize quickly. In general, icons work best when they use familiar visual metaphors that are directly related to the actions they initiate or content they represent. + +**Maintain visual consistency across all interface icons in your app.** Whether you use only custom icons or mix custom and system-provided ones, all interface icons in your app need to use a consistent size, level of detail, stroke thickness (or weight), and perspective. Depending on the visual weight of an icon, you may need to adjust its dimensions to ensure that it appears visually consistent with other icons. + +![Diagram of four glyphs in a row. From the left, the glyphs are a camera, a heart, an envelope, and an alarm clock. Two horizontal dashed lines show the bottom and top boundaries of the row and a horizontal red line shows the midpoint. All four glyphs are solid black; some include interior detail lines in white. Parts of the alarm clock extend above the top dashed line because its lighter visual weight requires greater height to achieve balance with the other glyphs.](https://docs-assets.developer.apple.com/published/f1cf8ce0ca53b7cb3bce1391a378f6ce/custom-icon-sizes%402x.png)To help achieve visual consistency, adjust individual icon sizes as necessaryโ€ฆ + +![Diagram of the same four glyphs shown above and the same horizontal dashed lines at top and bottom and horizontal red line through the middle. In this diagram, all four glyphs are solid gray; the interior detail lines are black to emphasize that all lines use the same weight.](https://docs-assets.developer.apple.com/published/91320cdd7a31574df355383d83eb1ceb/custom-icon-line-weights%402x.png)โ€ฆand use the same stroke weight in every icon. + +**In general, match the weights of interface icons and adjacent text.** Unless you want to emphasize either the icons or the text, using the same weight for both gives your content a consistent appearance and level of emphasis. + +**If necessary, add padding to a custom interface icon to achieve optical alignment.** Some icons โ€” especially asymmetric ones โ€” can look unbalanced when you center them geometrically instead of optically. For example, the download icon shown below has more visual weight on the bottom than on the top, which can make it look too low if itโ€™s geometrically centered. + +![Two images of a white arrow that points down to a white horizontal line segment within a black disk. The image on the right includes two horizontal pink bars โ€” one between the top of the glyph and the top of the disk and the other between the bottom of the glyph and the bottom of the disk โ€” that show the glyph is geometrically centered within the disk.](https://docs-assets.developer.apple.com/published/1c13eed753a1ebcfd6d35929738476c7/asymmetric-glyph%402x.png)An asymmetric icon can look off center even though itโ€™s not. + +In such cases, you can slightly adjust the position of the icon until itโ€™s optically centered. When you create an asset that includes your adjustments as padding around an interface icon (as shown below on the right), you can optically center the icon by geometrically centering the asset. + +![Two images of a white arrow that points down to a white horizontal line segment within a black disk. The image on the left includes the two horizontal pink bars in the same locations as in the previous illustration, but the glyph has been moved up by a few pixels. The image on the right includes a pink rectangle overlaid on top of the glyph to represent a padding area, which includes the extra pixels below the glyph.](https://docs-assets.developer.apple.com/published/c31bce31456820badff997c95db264c6/asymmetric-glyph-optically-centered%402x.png)Moving the icon a few pixels higher optically centers it; including the pixels in padding simplifies centering. + +Adjustments for optical centering are typically very small, but they can have a big impact on your appโ€™s appearance. + +![Two images of a white arrow that points down to a white horizontal line segment within a black disk. The glyph on the left is geometrically centered and the one on the right is optically centered.](https://docs-assets.developer.apple.com/published/5d9da37476ee3225a29ce3efbfd86cac/asymmetric-glyph-before-and-after%402x.png)Before optical centering (left) and after optical centering (right). + +**Provide a selected-state version of an interface icon only if necessary.** You donโ€™t need to provide selected and unselected appearances for an icon thatโ€™s used in standard system components such as toolbars, tab bars, and buttons. The system updates the visual appearance of the selected state automatically. + +![An image of two toolbar buttons that share a background. The left button shows the Filter icon in a selected state, using a blue tint color for its background. The right button shows the More icon in an unselected state, using the default appearance for toolbar buttons.](https://docs-assets.developer.apple.com/published/b5c874fca24c428b421c008b29709986/icons-selection-correct%402x.png)In a toolbar, a selected icon receives the appโ€™s accent color. + +**Use inclusive images.** Consider how your icons can be understandable and welcoming to everyone. Prefer depicting gender-neutral human figures and avoid images that might be hard to recognize across different cultures or languages. For guidance, see [Inclusion](https://developer.apple.com/design/human-interface-guidelines/inclusion). + +**Include text in your design only when itโ€™s essential for conveying meaning.** For example, using a character in an interface icon that represents text formatting can be the most direct way to communicate the concept. If you need to display individual characters in your icon, be sure to localize them. If you need to suggest a passage of text, design an abstract representation of it, and include a flipped version of the icon to use when the context is right-to-left. For guidance, see [Right to left](https://developer.apple.com/design/human-interface-guidelines/right-to-left). + +![A partial screenshot of the SF Symbols app showing the info panel for the character symbol, which looks like the capital letter A. Below the image, the following eight localized versions of the symbol are listed: Latin, Arabic, Hebrew, Hindi, Japanese, Korean, Thai, and Chinese.](https://docs-assets.developer.apple.com/published/1037fd04c26206ca1b1dee2e34e123af/character-in-glyph%402x.png)Create localized versions of an icon that displays individual characters. + +![A partial screenshot of the SF Symbols app showing the info panel for the text dot page symbol, which looks like three left-aligned horizontal lines inside a rounded rectangle. Below the image, the left-to-right and right-to-left localized versions are shown.](https://docs-assets.developer.apple.com/published/2edc8ff4ae7af79f32543009ba2f7084/abstract-text-in-glyph%402x.png)Create a flipped version of an icon that suggests reading direction. + +**If you create a custom interface icon, use a vector format like PDF or SVG.** The system automatically scales a vector-based interface icon for high-resolution displays, so you donโ€™t need to provide high-resolution versions of it. In contrast, PNG โ€” used for app icons and other images that include effects like shading, textures, and highlighting โ€” doesnโ€™t support scaling, so you have to supply multiple versions for each PNG-based interface icon. Alternatively, you can create a custom SF Symbol and specify a scale that ensures the symbolโ€™s emphasis matches adjacent text. For guidance, see [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols). + +**Provide alternative text labels for custom interface icons.** Alternative text labels โ€” or accessibility descriptions โ€” arenโ€™t visible, but they let VoiceOver audibly describe whatโ€™s onscreen, simplifying navigation for people with visual disabilities. For guidance, see [VoiceOver](https://developer.apple.com/design/human-interface-guidelines/voiceover). + +**Avoid using replicas of Apple hardware products.** Hardware designs tend to change frequently and can make your interface icons and other content appear dated. If you must display Apple hardware, use only the images available in [Apple Design Resources](https://developer.apple.com/design/resources/) or the SF Symbols that represent various Apple products. + +## [Standard icons](https://developer.apple.com/design/human-interface-guidelines/icons#Standard-icons) + +For icons to represent common actions in [menus](https://developer.apple.com/design/human-interface-guidelines/menus), [toolbars](https://developer.apple.com/design/human-interface-guidelines/toolbars), [buttons](https://developer.apple.com/design/human-interface-guidelines/buttons), and other places in interfaces across Apple platforms, you can use these [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols). + +### [Editing](https://developer.apple.com/design/human-interface-guidelines/icons#Editing) + +Action| Icon| Symbol name +---|---|--- +Cut| ![An icon showing a pair of scissors.](https://docs-assets.developer.apple.com/published/16c5fe84ae743e06cf2d388fc64e0cf4/icons-symbols-meaning-cut%402x.png)| `scissors` +Copy| ![An icon showing two copies of a document.](https://docs-assets.developer.apple.com/published/a88919c55265efbadeac0df5e16ffd05/icons-symbols-meaning-copy%402x.png)| `document.on.document` +Paste| ![An icon showing a document in front of a clipboard.](https://docs-assets.developer.apple.com/published/20e32bbb2a3a94eb35d01ddfa9c630e0/icons-symbols-meaning-paste%402x.png)| `document.on.clipboard` +Done| ![An icon showing a checkmark.](https://docs-assets.developer.apple.com/published/833bd3b8ccdf0e2fee0893b3858ddae5/icons-symbols-meaning-done-save%402x.png)| `checkmark ` +Save +Cancel| ![An icon showing an X.](https://docs-assets.developer.apple.com/published/b834206c8d155bc1b0d9d17c206c6da3/icons-symbols-meaning-close-cancel%402x.png)| `xmark` +Close +Delete| ![An icon showing a trash can.](https://docs-assets.developer.apple.com/published/61f8368d02b05af22d3253a892ced7f3/icons-symbols-meaning-delete%402x.png)| `trash` +Undo| ![An icon showing an arrow curving toward the top left.](https://docs-assets.developer.apple.com/published/e3e973d07e4cfa983c92e37da5b3e104/icons-symbols-meaning-undo%402x.png)| `arrow.uturn.backward` +Redo| ![An icon showing an arrow curving toward the top right.](https://docs-assets.developer.apple.com/published/0f263db97ca2b7c31bbbd3cd5682d071/icons-symbols-meaning-redo%402x.png)| `arrow.uturn.forward` +Compose| ![An icon showing a pencil positioned over a square.](https://docs-assets.developer.apple.com/published/cfac914468b7fa2f287495f8644f3937/icons-symbols-meaning-compose%402x.png)| `square.and.pencil` +Duplicate| ![An icon showing a square with a plus sign on top of another square.](https://docs-assets.developer.apple.com/published/96323f746d3c67172648745420a15c27/icons-symbols-meaning-duplicate%402x.png)| `plus.square.on.square` +Rename| ![An icon showing a pencil.](https://docs-assets.developer.apple.com/published/8d3692b6e29cf0cdcb7885af414b2693/icons-symbols-meaning-rename%402x.png)| `pencil` +Move to| ![An icon showing a folder.](https://docs-assets.developer.apple.com/published/77c3e45c395bf2d2225c85979eca53a8/icons-symbols-meaning-move-to-folder%402x.png)| `folder` +Folder +Attach| ![An icon showing a paperclip.](https://docs-assets.developer.apple.com/published/e493eab83f8cc2a6f0aaa2ced386dcff/icons-symbols-meaning-attach%402x.png)| `paperclip` +Add| ![An icon showing a plus sign.](https://docs-assets.developer.apple.com/published/e0a7d36fc7aecfd6e49a4d0c0cb196af/icons-symbols-meaning-add%402x.png)| `plus` +More| ![An icon showing an ellipsis.](https://docs-assets.developer.apple.com/published/92e0b17a3881b62008563deb4a2aca40/icons-symbols-meaning-more%402x.png)| `ellipsis` + +### [Selection](https://developer.apple.com/design/human-interface-guidelines/icons#Selection) + +Action| Icon| Symbol name +---|---|--- +Select| ![An icon showing a checkmark in a circle.](https://docs-assets.developer.apple.com/published/7eac493b5a3896062a90328117d43e90/icons-symbols-meaning-select-all%402x.png)| `checkmark.circle` +Deselect| ![An icon showing an X.](https://docs-assets.developer.apple.com/published/b834206c8d155bc1b0d9d17c206c6da3/icons-symbols-meaning-deselect-close%402x.png)| `xmark` +Close +Delete| ![An icon showing a trash can.](https://docs-assets.developer.apple.com/published/61f8368d02b05af22d3253a892ced7f3/icons-symbols-meaning-delete%402x.png)| `trash` + +### [Text formatting](https://developer.apple.com/design/human-interface-guidelines/icons#Text-formatting) + +Action| Icon| Symbol name +---|---|--- +Superscript| ![An icon showing the capital letter A with the number 1 in the upper right corner.](https://docs-assets.developer.apple.com/published/7e5e3d9b1b0eb6f340f531841d6b27f9/icons-symbols-meaning-superscript%402x.png)| `textformat.superscript` +Subscript| ![An icon showing the capital letter A with the number 1 in the lower right corner.](https://docs-assets.developer.apple.com/published/aac330c124cac37a78e02d6049943e53/icons-symbols-meaning-subscript%402x.png)| `textformat.subscript` +Bold| ![An icon showing the capital letter B in bold.](https://docs-assets.developer.apple.com/published/c8695e06d6461e79c145e9b6627f70ac/icons-symbols-meaning-bold%402x.png)| `bold` +Italic| ![An icon showing the capital letter I in italics.](https://docs-assets.developer.apple.com/published/9f560283ff88d8d1d4b48f278a831b7b/icons-symbols-meaning-italic%402x.png)| `italic` +Underline| ![An icon showing the capital letter U with an underline.](https://docs-assets.developer.apple.com/published/3b0d371f10bde381bfa1c9a8999797ec/icons-symbols-meaning-underline%402x.png)| `underline` +โ€‹โ€‹Align Left| ![An icon showing a stack of four horizontal lines of varying widths that align at the left edge.](https://docs-assets.developer.apple.com/published/68c0ff42aa0ac813b57b663562198e15/icons-symbols-meaning-align-left%402x.png)| `text.alignleft` +Center| ![An icon showing a stack of four horizontal lines of varying widths that align in the center.](https://docs-assets.developer.apple.com/published/a10b48c850a047efd4a72cc2919c30da/icons-symbols-meaning-align-center%402x.png)| `text.aligncenter` +Justified| ![An icon showing a stack of four horizontal lines of identical widths.](https://docs-assets.developer.apple.com/published/d71f35b4f71149b0b908dd1ff8cfe01e/icons-symbols-meaning-align-justified%402x.png)| `text.justify` +Align Right| ![An icon showing a stack of four horizontal lines of varying widths that align at the right edge.](https://docs-assets.developer.apple.com/published/8af1da29f8f3173510521492642a82be/icons-symbols-meaning-align-right%402x.png)| `text.alignright` + +### [Search](https://developer.apple.com/design/human-interface-guidelines/icons#Search) + +Action| Icon| Symbol name +---|---|--- +Search| ![An icon showing a magnifying glass.](https://docs-assets.developer.apple.com/published/585f5454757731f942979247bf886ecb/icons-symbols-meaning-search%402x.png)| `magnifyingglass` +Find| ![An icon showing a magnifying glass above a document.](https://docs-assets.developer.apple.com/published/646c6a152822dde685e52a2791ff672f/icons-symbols-meaning-find%402x.png)| `text.page.badge.magnifyingglass` +Find and Replace +Find Next +Find Previous +Use Selection for Find +Filter| ![An icon showing a stack of three horizontal lines decreasing in width from top to bottom.](https://docs-assets.developer.apple.com/published/e0924492d663dac952860673a61f4f96/icons-symbols-meaning-filter%402x.png)| `line.3.horizontal.decrease` + +### [Sharing and exporting](https://developer.apple.com/design/human-interface-guidelines/icons#Sharing-and-exporting) + +Action| Icon| Symbol name +---|---|--- +Share| ![An icon showing an arrow pointing up from the middle of square.](https://docs-assets.developer.apple.com/published/b5fa28be3d82955fc380f44783befd32/icons-symbols-meaning-sharing%402x.png)| `square.and.arrow.up` +Export +Print| ![An icon showing a printer.](https://docs-assets.developer.apple.com/published/9de4d23e30e6fd8331215dd6dab12878/icons-symbols-meaning-print%402x.png)| `printer` + +### [Users and accounts](https://developer.apple.com/design/human-interface-guidelines/icons#Users-and-accounts) + +Action| Icon| Symbol name +---|---|--- +Account| ![An icon showing an abstract representation of a personโ€™s head and shoulders in a circular outline.](https://docs-assets.developer.apple.com/published/512c86a1c2c99bc09991c89c1e535441/icons-symbols-meaning-account-user%402x.png)| `person.crop.circle` +User +Profile + +### [Ratings](https://developer.apple.com/design/human-interface-guidelines/icons#Ratings) + +Action| Icon| Symbol name +---|---|--- +Dislike| ![An icon showing a hand giving a thumbs down gesture.](https://docs-assets.developer.apple.com/published/189b97655ff655985deec03d0466b898/icons-symbols-meaning-dislike%402x.png)| `hand.thumbsdown` +Like| ![An icon showing a hand giving a thumbs up gesture.](https://docs-assets.developer.apple.com/published/6f38eef523ffbb4f1d6de22a6a022309/icons-symbols-meaning-like%402x.png)| `hand.thumbsup` + +### [Layer ordering](https://developer.apple.com/design/human-interface-guidelines/icons#Layer-ordering) + +Action| Icon| Symbol name +---|---|--- +Bring to Front| ![An icon showing a stack of three squares overlapping each other, with the top square using a solid fill style while the other squares are outlines.](https://docs-assets.developer.apple.com/published/c5da334738c9baf5ddaa0d6ed9de0af6/icons-symbols-meaning-bring-to-front%402x.png)| `square.3.layers.3d.top.filled` +Send to Back| ![An icon showing a stack of three squares overlapping each other, with the bottom square using a solid fill style while the other squares are outlines.](https://docs-assets.developer.apple.com/published/1006037b6fa15950ca7ceb933dbb4805/icons-symbols-meaning-send-to-back%402x.png)| `square.3.layers.3d.bottom.filled` +Bring Forward| ![An icon showing a stack of two squares overlapping each other, with the top square using a solid fill style while the other square is an outline.](https://docs-assets.developer.apple.com/published/88b18e0384bca2cd93151169bd507aa3/icons-symbols-meaning-bring-forward%402x.png)| `square.2.layers.3d.top.filled` +Send Backward| ![An icon showing a stack of two squares overlapping each other, with the bottom square using a solid fill style while the other square is an outline.](https://docs-assets.developer.apple.com/published/49eb0ed5381249d763d30d4e515bc57b/icons-symbols-meaning-send-backwards%402x.png)| `square.2.layers.3d.bottom.filled` + +### [Other](https://developer.apple.com/design/human-interface-guidelines/icons#Other) + +Action| Icon| Symbol name +---|---|--- +Alarm| ![An icon showing an alarm clock.](https://docs-assets.developer.apple.com/published/b777cb6bcc99642b037824c78a7efb0e/icons-symbols-meaning-alarm%402x.png)| `alarm` +Archive| ![An icon showing a file box.](https://docs-assets.developer.apple.com/published/50a3b677d72b3d031ea66d10198bfb59/icons-symbols-meaning-archive%402x.png)| `archivebox` +Calendar| ![An icon showing a calendar.](https://docs-assets.developer.apple.com/published/4b14bf07cf562405caebedb2e200e3dc/icons-symbols-meaning-calendar%402x.png)| `calendar` + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/icons#Platform-considerations) + + _No additional considerations for iOS, iPadOS, tvOS, visionOS, or watchOS._ + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/icons#macOS) + +#### [Document icons](https://developer.apple.com/design/human-interface-guidelines/icons#Document-icons) + +If your macOS app can use a custom document type, you can create a document icon to represent it. Traditionally, a document icon looks like a piece of paper with its top-right corner folded down. This distinctive appearance helps people distinguish documents from apps and other content, even when icon sizes are small. + +If you donโ€™t supply a document icon for a file type you support, macOS creates one for you by compositing your app icon and the fileโ€™s extension onto the canvas. For example, Preview uses a system-generated document icon to represent JPG files. + +![An image of the Preview document icon for a JPG file.](https://docs-assets.developer.apple.com/published/bfe462604c63811cb542e7c0fc46185e/doc-icon-generated%402x.png) + +In some cases, it can make sense to create a set of document icons to represent a range of file types your app handles. For example, Xcode uses custom document icons to help people distinguish projects, AR objects, and Swift code files. + +![Image of an Xcode project document icon.](https://docs-assets.developer.apple.com/published/8cd56a7291cd6b41fe391958f704c823/doc-icon-custom-1%402x.png) + +![Image of a document icon for an AR object.](https://docs-assets.developer.apple.com/published/a1449177968f693c1bd68c2b146df7c3/doc-icon-custom-2%402x.png) + +![Image of a document icon for a Swift file.](https://docs-assets.developer.apple.com/published/495bd043bf65349ec96f6728941386f8/doc-icon-custom-3%402x.png) + +To create a custom document icon, you can supply any combination of background fill, center image, and text. The system layers, positions, and masks these elements as needed and composites them onto the familiar folded-corner icon shape. + +![A square canvas that contains a grid of pink lines and a jagged white EKG line that runs horizontally across the middle. The pink grid gets lighter in color toward the bottom edge.](https://docs-assets.developer.apple.com/published/2aed446834a2dc6e8275b6bd7a797ca9/doc-icon-parts-background-fill%402x.png)Background fill + +![A solid pink heart.](https://docs-assets.developer.apple.com/published/b59c836903d1b409ab9e21f81762df3e/doc-icon-parts-center-image%402x.png)Center image + +![The word heart in all caps.](https://docs-assets.developer.apple.com/published/56c5adedc0c08a167a4a03e706924ee6/doc-icon-parts-text%402x.png)Text + +![A custom document icon that displays the pink heart and the word heart on top of the pink grid and white EKG line.](https://docs-assets.developer.apple.com/published/d5da9148d27f60891780ab1a9546a111/doc-icon-parts%402x.png)macOS composites the elements you supply to produce your custom document icon. + +[Apple Design Resources](https://developer.apple.com/design/resources/#macos-apps) provides a template you can use to create a custom background fill and center image for a document icon. As you use this template, follow the guidelines below. + +**Design simple images that clearly communicate the document type.** Whether you use a background fill, a center image, or both, prefer uncomplicated shapes and a reduced palette of distinct colors. Your document icon can display as small as 16x16 px, so you want to create designs that remain recognizable at every size. + +**Designing a single, expressive image for the background fill can be a great way to help people understand and recognize a document type.** For example, Xcode and TextEdit both use rich background images that donโ€™t include a center image. + +![Image of an Xcode project document icon.](https://docs-assets.developer.apple.com/published/8cd56a7291cd6b41fe391958f704c823/doc-icon-custom-1%402x.png) + +![Image of a TextEdit rich text document icon.](https://docs-assets.developer.apple.com/published/f32709a5ff5742e79fd03a58ae1dd9c6/doc-icon-fill-only%402x.png) + +**Consider reducing complexity in the small versions of your document icon.** Icon details that are clear in large versions can look blurry and be hard to recognize in small versions. For example, to ensure that the grid lines in the custom heart document icon remain clear in intermediate sizes, you might use fewer lines and thicken them by aligning them to the reduced pixel grid. In the 16x16 px size, you might remove the lines altogether. + +![Pixelated image of the heart document icon. The grid, the EKG line, the heart shape, and the word heart are visible but blurry.](https://docs-assets.developer.apple.com/published/1f8bc7946a75363224f373924b557a3a/doc-icon-fewer-details-1%402x.png)The 32x32 px icon has fewer grid lines and a thicker EKG line. + +![Pixelated image of the heart document icon, in which only the blurry heart shape and EKG line are visible.](https://docs-assets.developer.apple.com/published/e46ac887801d9a16393948c3f2098715/doc-icon-fewer-details-2%402x.png)The 16x16 px @2x icon retains the EKG line but has no grid lines. + +![Pixelated image of the heart document icon, in which only the blurry heart shape is visible.](https://docs-assets.developer.apple.com/published/fd0d2afcd76a9b25c1217ef2ffb1ad0e/doc-icon-fewer-details-3%402x.png)The 16x16 px @1x icon has no EKG line and no grid lines. + +**Avoid placing important content in the top-right corner of your background fill.** The system automatically masks your image to fit the document icon shape and draws the white folded corner on top of the fill. Create a set of background images in the sizes listed below. + + * 512x512 px @1x, 1024x1024 px @2x + + * 256x256 px @1x, 512x512 px @2x + + * 128x128 px @1x, 256x256 px @2x + + * 32x32 px @1x, 64x64 px @2x + + * 16x16 px @1x, 32x32 px @2x + + + + +**If a familiar object can convey a documentโ€™s type or its connection with your app, consider creating a center image that depicts it.** Design a simple, unambiguous image thatโ€™s clear and recognizable at every size. The center image measures half the size of the overall document icon canvas. For example, to create a center image for a 32x32 px document icon, use an image canvas that measures 16x16 px. You can provide center images in the following sizes: + + * 256x256 px @1x, 512x512 px @2x + + * 128x128 px @1x, 256x256 px @2x + + * 32x32 px @1x, 64x64 px @2x + + * 16x16 px @1x, 32x32 px @2x + + + + +**Define a margin that measures about 10% of the image canvas and keep most of the image within it.** Although parts of the image can extend into this margin for optical alignment, itโ€™s best when the image occupies about 80% of the image canvas. For example, most of the center image in a 256x256 px canvas would fit in an area that measures 205x205 px. + +![Diagram of the solid pink heart shape within blue margins that measure 10 percent of the canvas width.](https://docs-assets.developer.apple.com/published/7cc19b2ae1e99d26ba69e1351683ede1/doc-icon-parts-margins%402x.png) + +**Specify a succinct term if it helps people understand your document type.** By default, the system displays a documentโ€™s extension at the bottom edge of the document icon, but if the extension is unfamiliar you can supply a more descriptive term. For example, the document icon for a SceneKit scene file uses the term _scene_ instead of the file extension _scn_. The system automatically scales the extension text to fit in the document icon, so be sure to use a term thatโ€™s short enough to be legible at small sizes. By default, the system capitalizes every letter in the text. + +![Image of a SceneKit scene document icon.](https://docs-assets.developer.apple.com/published/3b4bb7de9edb5870d3a162aae8153163/doc-icon-custom-extension%402x.png) + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/icons#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/icons#Related) + +[App icons](https://developer.apple.com/design/human-interface-guidelines/app-icons) + +[SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/icons#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/7/597D59A1-F123-4B08-BEE1-6D79A4C22268/1914_wide_250x141_1x.jpg) Designing Glyphs ](https://developer.apple.com/videos/play/wwdc2017/823) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/icons#Change-log) + +Date| Changes +---|--- +June 9, 2025| Added a table of SF Symbols that represent common actions. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/images.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/images.md new file mode 100644 index 00000000..6b841670 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/images.md @@ -0,0 +1,176 @@ +--- +title: "Images | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/images + +# Images + +To make sure your artwork looks great on all devices you support, learn how the system displays content and how to deliver art at the appropriate scale factors. + +![A sketch of a photo, suggesting imagery. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/b0a68ac859ddb098858e92609997d307/foundations-images-intro%402x.png) + +## [Resolution](https://developer.apple.com/design/human-interface-guidelines/images#Resolution) + +Different devices can display images at different resolutions. For example, a 2D device displays images according to the resolution of its screen. + +A _point_ is an abstract unit of measurement that helps visual content remain consistent regardless of how itโ€™s displayed. In 2D platforms, a point maps to a number of pixels that can vary according to the resolution of the display; in visionOS, a point is an angular value that allows visual content to scale according to its distance from the viewer. + +When creating bitmap images, you specify a _scale factor_ which determines the resolution of an image. You can visualize scale factor by considering the density of pixels per point in 2D displays of various resolutions. For example, a scale factor of 1 (also called @1x) describes a 1:1 pixel density, where one pixel is equal to one point. High-resolution 2D displays have higher pixel densities, such as 2:1 or 3:1. A 2:1 density (called @2x) has a scale factor of 2, and a 3:1 density (called @3x) has a scale factor of 3. Because of higher pixel densities, high-resolution displays demand images with more pixels. + +![Image of a circle that's in standard resolution at scale factor of @1x, and is 10 by 10 pixels.](https://docs-assets.developer.apple.com/published/a9b04545f30aff45ca503e263c02d464/image-resolution-1x%402x.png)1x (10x10 px) + +![Image of a circle that's in high resolution at a scale factor of @2x, and is 20 by 20 pixels.](https://docs-assets.developer.apple.com/published/cf203acc0ee6299833fb2e5b5c4a7348/image-resolution-2x%402x.png)2x (20x20 px) + +![Image of a circle that's in high resolution at a scale factor of @3x, and is 30 by 30 pixels.](https://docs-assets.developer.apple.com/published/0de9ee5144fc2278682eb211ac8f571d/image-resolution-3x%402x.png)3x (30x30 px) + +**Provide high-resolution assets for all bitmap images in your app, for every device you support.** As you add each image to your projectโ€™s asset catalog, identify its scale factor by appending โ€œ@1x,โ€ โ€œ@2x,โ€ or โ€œ@3xโ€ to its filename. Use the following values for guidance; for additional scale factors, see [Layout](https://developer.apple.com/design/human-interface-guidelines/layout). + +Platform| Scale factors +---|--- +iPadOS, watchOS| @2x +iOS| @2x and @3x +visionOS| @2x or higher (see [visionOS](https://developer.apple.com/design/human-interface-guidelines/images#visionOS)) +macOS, tvOS| @1x and @2x + +**In general, design images at the lowest resolution and scale them up to create high-resolution assets.** When you use resizable vectorized shapes, you might want to position control points at whole values so that theyโ€™re cleanly aligned at 1x. This positioning allows the points to remain cleanly aligned to the raster grid at higher resolutions, because 2x and 3x are multiples of 1x. + +## [Formats](https://developer.apple.com/design/human-interface-guidelines/images#Formats) + +As you create different types of images, consider the following recommendations. + +Image type| Format +---|--- +Bitmap or raster work| De-interlaced PNG files +PNG graphics that donโ€™t require full 24-bit color| An 8-bit color palette +Photos| JPEG files, optimized as necessary, or HEIC files +Stereo or spatial photos| Stereo HEIC +Flat icons, interface icons, and other flat artwork that requires high-resolution scaling| PDF or SVG files + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/images#Best-practices) + +**Include a color profile with each image.** Color profiles help ensure that your appโ€™s colors appear as intended on different displays. For guidance, see [Color management](https://developer.apple.com/design/human-interface-guidelines/color#Color-management). + +**Always test images on a range of actual devices.** An image that looks great at design time may appear pixelated, stretched, or compressed when viewed on various devices. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/images#Platform-considerations) + + _No additional considerations for iOS, iPadOS, or macOS._ + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/images#tvOS) + +Layered images are at the heart of the Apple TV user experience. The system combines layered images, transparency, scaling, and motion to produce a sense of realism and vigor that evokes a personal connection as people interact with onscreen content. + +#### [Parallax effect](https://developer.apple.com/design/human-interface-guidelines/images#Parallax-effect) + + _Parallax_ is a subtle visual effect the system uses to convey depth and dynamism when an element is in focus. As an element comes into focus, the system elevates it to the foreground, gently swaying it while applying illumination that makes the elementโ€™s surface appear to shine. After a period of inactivity, out-of-focus content dims and the focused element expands. + +Layered images are required to support the parallax effect. + +Video with custom controls. + +Content description: An animation of a tvOS app icon moving to show the parallax effect. + +Play + +#### [Layered images](https://developer.apple.com/design/human-interface-guidelines/images#Layered-images) + +A _layered image_ consists of two to five distinct layers that come together to form a single image. The separation between layers, along with use of transparency, creates a feeling of depth. As someone interacts with an image, layers closer to the surface elevate and scale, overlapping lower layers farther back and producing a 3D effect. + +Important + +Your tvOS [app icon](https://developer.apple.com/design/human-interface-guidelines/app-icons#tvOS) must use a layered image. For other focusable images in your app, including [Top Shelf](https://developer.apple.com/design/human-interface-guidelines/top-shelf) images, layered images are strongly encouraged, but optional. + +You can embed layered images in your app or retrieve them from a content server at runtime. For guidance on adding layered images to your app, see the [Parallax Previewer User Guide](https://help.apple.com/itc/parallaxpreviewer/). + +Developer note + +If your app retrieves layered images from a content server at runtime, you must provide runtime layered images (`.lcr`). You can generate them from LSR files or Photoshop files using the `layerutil` command-line tool that Xcode provides. Runtime layered images are intended to be downloaded โ€” donโ€™t embed them in your app. + +**Use standard interface elements to display layered images.** If you use standard views and system-provided focus APIs โ€” such as [`FocusState`](https://developer.apple.com/documentation/SwiftUI/FocusState) โ€” layered images automatically get the parallax treatment when people bring them into focus. + +**Identify logical foreground, middle, and background elements.** In foreground layers, display prominent elements like a character in a game, or text on an album cover or movie poster. Middle layers are perfect for secondary content and effects like shadows. Background layers are opaque backdrops that showcase the foreground and middle layers without upstaging them. + +**Generally, keep text in the foreground.** Unless you want to obscure text, bring it to the foreground layer for clarity. + +**Keep the background layer opaque.** Using varying levels of opacity to let content shine through higher layers is fine, but your background layer must be opaque โ€” youโ€™ll get an error if itโ€™s not. An opaque background layer ensures your artwork looks great with parallax, drop shadows, and system backgrounds. + +**Keep layering simple and subtle.** Parallax is designed to be almost unnoticeable. Excessive 3D effects can appear unrealistic and jarring. Keep depth simple to bring your content to life and add delight. + +**Leave a safe zone around the foreground layers of your image.** When focused, content on some layers may be cropped as the layered image scales and moves. To ensure that essential content is always visible, keep it within a safe zone. For guidance, see [App icons](https://developer.apple.com/design/human-interface-guidelines/app-icons). + +**Always preview layered images.** To ensure your layered images look great on Apple TV, preview them throughout your design process using Xcode, the Parallax Previewer app for macOS, or the Parallax Exporter plug-in for Adobe Photoshop. Pay special attention as scaling and clipping occur, and readjust your images as needed to keep important content safe. After your layered images are final, preview them on an actual TV for the most accurate representation of what people will see. To download Parallax Previewer and Parallax Exporter, see [Resources](https://developer.apple.com/design/resources/#parallax-previewer). + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/images#visionOS) + +In visionOS, people can view images at a much larger range of sizes than in any other platform, and the system dynamically scales the image resolution to match the current size. Because you can position images at specific angles within someoneโ€™s surroundings, image pixels may not line up 1:1 with screen pixels. + +**Create a layered app icon.** App icons in visionOS are composed of two to three layers that provide the appearance of depth by moving at subtly different rates when the icon is in focus. For guidance, see [Layer design](https://developer.apple.com/design/human-interface-guidelines/app-icons#Layer-design). + +**Prefer vector-based art for 2D images.** Avoid bitmap content because it might not look good when the system scales it up. If you use Core Animation layers, see [Drawing sharp layer-based content in visionOS](https://developer.apple.com/documentation/visionOS/drawing-sharp-layer-based-content) for developer guidance. + +**If you need to use rasterized images, balance quality with performance as you choose a resolution.** Although a @2x image looks fine at common viewing distances, its fixed resolution means that the system doesnโ€™t dynamically scale it and it might not look sharp from close up. To help a rasterized image look sharp when people view it from a wide range of distances, you can use a higher resolution, but each increase in resolution results in a larger file size and may impact your appโ€™s runtime performance, especially for resolutions over @6x. If you use images that have resolutions higher than @2x, be sure to also apply high-quality image filtering to help balance quality and performance (for developer guidance, see [`filters`](https://developer.apple.com/documentation/QuartzCore/CALayer/filters)). + +#### [Spatial photos and spatial scenes](https://developer.apple.com/design/human-interface-guidelines/images#Spatial-photos-and-spatial-scenes) + +In addition to 2D and stereoscopic images, visionOS apps and games can use RealityKit to display spatial photos and spatial scenes. A _spatial photo_ is a stereoscopic photo with additional spatial metadata, as captured on iPhone 15 Pro or later, Apple Vision Pro, or other compatible camera. A _spatial scene_ is a 3D image generated from a 2D image to add a parallax effect that responds to head movement. For developer guidance, see [`ImagePresentationComponent`](https://developer.apple.com/documentation/RealityKit/ImagePresentationComponent). + +**Make sure spatial photos render correctly in your app.** Use the stereo High-Efficiency Image Codec (HEIC) format to display a spatial photo in your app. When you add spatial metadata to a stereo HEIC, visionOS recognizes the photo as spatial and includes visual treatments that help minimize common causes of stereo-viewing discomfort. + +**Prefer the feathered glass background effect to display text over spatial photos.** If you need to place text over a spatial photo in your app or game, use the feathered glass background effect. The effect adds contrast to make the text readable, and it blurs out detail to help reduce visual discomfort when people view text over spatial photos. For developer guidance, see [`GlassBackgroundEffect`](https://developer.apple.com/documentation/SwiftUI/GlassBackgroundEffect). + +**Take visual comfort into consideration when you make spatial photos from existing 2D content.** When adjusting the spatial metadata of a photo for your app or game, consider how you want people to view your content. Metadata like disparity adjustment can alter how people perceive the 3D scene, and can cause visual discomfort from certain viewing positions. For developer guidance, see [Creating spatial photos and videos with spatial metadata](https://developer.apple.com/documentation/ImageIO/Creating-spatial-photos-and-videos-with-spatial-metadata). + +**Display spatial photos and spatial scenes in standalone views.** Avoid displaying spatial photos inline with other content, as this can cause visual discomfort. Instead, showcase spatial photos or spatial scenes in a separate view, like a sheet or window. If you must display stereoscopic images inline, provide generous spacing between the image and any inline content to help peopleโ€™s eyes adjust to the depth changes. + +**Use spatial scenes in your app for specific moments.** Each spatial scene can take up to several seconds to generate from an existing image. Design experiences with this limitation in mind. For instance, the Photos app offers an explicit action to create a spatial scene while immersed in a single photo. Avoid displaying too many spatial scenes at once. Instead, use scroll views, pagination, or explicit actions to move to new photos and keep the visual information hierarchy simple. + +**When displaying immersively, prefer minimal UI.** For example, the Spatial Gallery app displays a single piece of content with a small caption and a single Back button, relying on swipe gestures to navigate between items. + +**Prefer displaying larger spatial scenes that you center in someoneโ€™s field of view.** When people view a spatial scene, they may move their head laterally to view the parallax effect. Smaller spatial scenes provide less of a parallax effect and may not be as impactful to viewers. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/images#watchOS) + +**In general, avoid transparency to keep image files small.** If you always composite an image on the same solid background color, itโ€™s more efficient to include the background in the image. However, transparency is necessary in complication images, menu icons, and other interface icons that serve as template images, because the system uses it to determine where to apply color. + +**Use autoscaling PDFs to let you provide a single asset for all screen sizes.** Design your image for the 40mm and 42mm screens at 2x. When you load the PDF, WatchKit automatically scales the image based on the deviceโ€™s screen size, using the values shown below: + +Screen size| Image scale +---|--- +38mm| 90% +40mm| 100% +41mm| 106% +42mm| 100% +44mm| 110% +45mm| 119% +49mm| 119% + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/images#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/images#Related) + +[Apple Design Resources](https://developer.apple.com/design/resources/) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/images#Developer-documentation) + +[Drawing sharp layer-based content in visionOS](https://developer.apple.com/documentation/visionOS/drawing-sharp-layer-based-content) โ€” visionOS + +[Images](https://developer.apple.com/documentation/SwiftUI/Images) โ€” SwiftUI + +[`UIImageView`](https://developer.apple.com/documentation/UIKit/UIImageView) โ€” UIKit + +[`NSImageView`](https://developer.apple.com/documentation/AppKit/NSImageView) โ€” AppKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/images#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/8226C70F-64DC-4FF1-9956-2DC0751A2143/8241_wide_250x141_1x.jpg) Support HDR images in your app ](https://developer.apple.com/videos/play/wwdc2023/10181) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/7/09438FDD-3E8B-42A3-A364-78E1A7F2CE6B/1915_wide_250x141_1x.jpg) Get Started with Display P3 ](https://developer.apple.com/videos/play/wwdc2017/821) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/images#Change-log) + +Date| Changes +---|--- +December 16, 2025| Added guidance for spatial photos and spatial scenes in visionOS. +December 5, 2023| Clarified guidance on choosing a resolution for a rasterized image in a visionOS app. +June 21, 2023| Updated to include guidance for visionOS. +September 14, 2022| Added specifications for Apple Watch Ultra. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/immersive-experiences.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/immersive-experiences.md new file mode 100644 index 00000000..580f8c47 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/immersive-experiences.md @@ -0,0 +1,174 @@ +--- +title: "Immersive experiences | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/immersive-experiences + +# Immersive experiences + +In visionOS, you can design apps and games that extend beyond windows and volumes, immersing people in your content. + +![A sketch that suggests Apple Vision Pro. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/63fd96e56c2b19f4451f688728f0b013/foundations-immersive-experiences-intro%402x.png) + +You can choose whether your visionOS app or game launches in the Shared Space or in a Full Space. In the _Shared Space_ , your software runs alongside other experiences, and people can switch between them much as they do on a Mac; in a _Full Space_ , your app or game runs alone, hiding other experiences and helping people immerse themselves in your content. Apps and games can support different types of immersion, and can transition fluidly between the Shared Space and a Full Space at any time. + +## [Immersion and passthrough](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences#Immersion-and-passthrough) + +In visionOS, people use passthrough to see their physical surroundings. _Passthrough_ provides real-time video from the deviceโ€™s external cameras, helping people feel comfortable and connected to their physical context. + +People can also use the [Digital Crown](https://developer.apple.com/design/human-interface-guidelines/digital-crown) at any time to manage app or game content or adjust passthrough. For example, people can press and hold the Digital Crown to recenter content in their field of view or double-click it to briefly hide all content and show passthrough for a clear view of their surroundings. + +The system also helps people remain comfortable by automatically changing the opacity of content in certain situations. For example, if someone gets too close to a physical object in `mixed` immersion, the content in front of them dims briefly so they can see their immediate physical surroundings more clearly. In more immersive experiences โ€” such as in the `progressive` and `full` styles described below โ€” the system defines a boundary that extends about 1.5 meters from the initial position of the wearerโ€™s head. As their head gets close to this boundary, the entire experience begins to fade and passthrough increases. When their head moves beyond this boundary, the immersive visuals are replaced in space by the appโ€™s icon, and are restored when the wearer returns to their original location or recenters their view using the Digital Crown. + +### [Immersion styles](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences#Immersion-styles) + +When your app or game transitions to a Full Space, the system hides other apps so people can focus on yours. In a Full Space, you can display 3D content that isnโ€™t bound by a window, in addition to content in standard windows and volumes. For developer guidance, see [`automatic`](https://developer.apple.com/documentation/SwiftUI/ImmersionStyle/automatic). + +visionOS offers several ways to immerse people in your content in the Shared Space as well as when you transition to a Full Space. For example, you can: + + * **Use dimmed passthrough to bring attention to your content.** You can subtly dim or tint passthrough and other visible content to bring attention to your app in the Shared Space without hiding other apps and games, or create a more focused experience in a Full Space. While passthrough is tinted black by default, you can apply a custom tint color to create a dynamic experience in your app. For developer guidance, see [`SurroundingsEffect`](https://developer.apple.com/documentation/SwiftUI/SurroundingsEffect). + + + + + * Without dimmed passthrough + * With dimmed passthrough + + + +![A screenshot of a window in the Shared Space.](https://docs-assets.developer.apple.com/published/3aa6d97e5947c39a73cfd8dd7e9c4dff/immersive-spaces-shared-space-regular-content%402x.png) + +![A screenshot of a highlighted window in the Shared Space with dimmed passthrough.](https://docs-assets.developer.apple.com/published/d6645a2853d8dc87e99062f5f575222b/immersive-spaces-shared-space-dimmed-content%402x.png) + + * **Create unbounded 3D experiences.** Use the `mixed` immersion style in a Full Space to blend your content with passthrough. When your app or game runs in a Full Space, you can request access to information about nearby physical objects and room layout, helping you display virtual content in a personโ€™s surroundings. The `mixed` immersion style doesnโ€™t define a boundary. Instead, when a person gets too close to a physical object, the system automatically makes nearby content semi-opaque to help them remain aware of their surroundings. For developer guidance, see [`mixed`](https://developer.apple.com/documentation/SwiftUI/ImmersionStyle/mixed) and [ARKit](https://developer.apple.com/documentation/ARKit). + + * **Use`progressive` immersion to blend your custom environment with a personโ€™s surroundings.** You can use the `progressive` style in a Full Space to display a custom environment that partially replaces passthrough. You can also define a specific range of immersion that works best with your app or game, and display content in portrait or landscape orientation. While in your immersive experience, people can use the Digital Crown to adjust the amount of immersion within either the default range of 120- to 360-degrees or a custom range, if you specify one. The system automatically defines an approximately 1.5-meter boundary when an experience transitions to the `progressive` style. For developer guidance, see [`progressive`](https://developer.apple.com/documentation/SwiftUI/ImmersionStyle/progressive). + + + + +Video with custom controls. + +Content description: A recording of a fully immersive experience in which a video player window appears on top of an Environment. As the viewer adjusts the Digital Crown, passthrough increases to reveal more of the person's physical surroundings. + +Play + + * **Use`full` immersion to create a fully immersive experience.** You can use the `full` style in a Full Space to display a 360-degree custom environment that completely replaces passthrough and transports people to a new place. As with the `progressive` style, the system defines an approximately 1.5-meter boundary when a fully immersive experience starts. For developer guidance, see [`full`](https://developer.apple.com/documentation/SwiftUI/ImmersionStyle/full). + + + + + * Full Space (Mixed) + * Full Space (Progressive) + * Full Space (Immersive) + + + +![A screenshot of an app running in a Full Space using the mixed immersion style in visionOS.](https://docs-assets.developer.apple.com/published/bb7e4d2d5f14673af8223f16b8ef8367/immersive-spaces-full-space-mixed-style%402x.png)Mixed immersion style in a Full Space blending in-app objects with real-world surroundings + +![A screenshot of an app running in a Full Space using the progressive immersion style in visionOS.](https://docs-assets.developer.apple.com/published/7c6bd28f709239805551dfe4db2f4f0e/immersive-spaces-full-space-progressive-style%402x.png)Progressive immersion style in a Full Space blending the appโ€™s custom environment with real-world surroundings + +![A screenshot of an app running in a Full Space using the full immersion style in visionOS.](https://docs-assets.developer.apple.com/published/3e8f31614811987239868bca745cd798/immersive-spaces-full-space-full-style%402x.png)Full immersion style in a Full Space showing a 360-degree custom environment + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences#Best-practices) + +**Offer multiple ways to use your app or game.** In addition to giving people the freedom to choose their experiences, itโ€™s essential to design your software to support the accessibility features people use to personalize the ways they interact with their devices. For guidance, see [Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility). + +**Prefer launching your app or game in the Shared Space or using the`mixed` immersion style.** Launching in the Shared Space lets people reference your app or game while using other running software, and enables seamless switching between them. If your app or game provides a fully immersive or `progressive` style experience, launching in the `mixed` immersion style or with a window-based experience in the Shared Space gives people more control, letting them choose when to increase immersion. + +**Reserve immersion for meaningful moments and content.** Not every task benefits from immersion, and not every immersive task needs to be fully immersive. Although people sometimes want to enter a different world, they often want to stay grounded in their surroundings while theyโ€™re using your app or game, and they can appreciate being able to use other software and system features at the same time. Instead of assuming that your app or game needs to be fully immersive most of the time, design ways for people to immerse themselves in the individual tasks and content that make your experience unique. For example, people can browse their albums in Photos using a familiar app window in the Shared Space, but when they want to examine a single photo, they can temporarily transition to a more immersive experience in a Full Space where they can expand the photo and appreciate its details. + +**Help people engage with key moments in your app or game, regardless of the level of immersion.** Cues like dimming, tinting, [motion](https://developer.apple.com/design/human-interface-guidelines/motion), [scale](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Scale), and [Spatial Audio](https://developer.apple.com/design/human-interface-guidelines/playing-audio#visionOS) can help draw peopleโ€™s attention to a specific area of content, whether itโ€™s in a window in the Shared Space or in a completely immersive experience in a Full Space. Start with subtle cues that gently guide peopleโ€™s attention, strengthening the cues only when thereโ€™s a good reason to do so. + +**Prefer subtle tint colors for passthrough.** In visionOS 2 and later, you can tint passthrough to help a personโ€™s surroundings visually coordinate with your content, while also making their hands look like they belong in your experience. Avoid bright or dramatic tints that can distract people and diminish the sense of immersion. For developer guidance, see [`SurroundingsEffect`](https://developer.apple.com/documentation/SwiftUI/SurroundingsEffect). + +## [Promoting comfort](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences#Promoting-comfort) + +**Be mindful of peopleโ€™s visual comfort.** For example, although you can place 3D content anywhere while your app or game is running in a Full Space, prefer placing it within peopleโ€™s [field of view](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Field-of-view). Also, make sure you display motion in comfortable ways while your software runs in a Full Space to avoid causing distraction, confusion, or discomfort. For guidance, see [Motion](https://developer.apple.com/design/human-interface-guidelines/motion). + +**Choose a style of immersion that supports the movements people might make while theyโ€™re in your app or game.** Itโ€™s essential to choose the right style for your immersive experience because it allows the system to respond appropriately when people move. Although people can make minor physical movements while in an immersive experience โ€” such as shifting their weight, turning around, or switching between sitting and standing โ€” making excessive movements can cause the system to interrupt some experiences. In particular, avoid using the `progressive` or `full` immersion styles or transition back to the `mixed` immersion style if you think people might need to move beyond the 1.5-meter boundary. + +**Avoid encouraging people to move while theyโ€™re in a progressive or fully immersive experience.** Some people may not want to move, or are unable to move because of a disability or their physical surroundings. Design ways for people to interact with content without moving. For example, let people bring a virtual object closer to them instead of expecting them to move closer to the object. + +**If you use the`mixed` immersion style, avoid obscuring passthrough too much.** People use passthrough to help them understand and navigate their physical surroundings, so itโ€™s important to avoid displaying virtual objects that block too much of their view. If your app or game displays virtual objects that could substantially obscure passthrough, use the `full` or `progressive` immersion styles instead of `mixed`. + +**Adopt ARKit if you want to blend custom content with someoneโ€™s surroundings.** For example, you might want to integrate virtual content into someoneโ€™s surroundings or use the wearerโ€™s hand positions to inform your experience. If you need access to these types of sensitive data, you must request peopleโ€™s permission. For guidance, see [Privacy](https://developer.apple.com/design/human-interface-guidelines/privacy); for developer guidance, see [`SceneReconstructionProvider`](https://developer.apple.com/documentation/ARKit/SceneReconstructionProvider). + +## [Transitioning between immersive styles](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences#Transitioning-between-immersive-styles) + +**Design smooth, predictable transitions when changing immersion.** Help people prepare for different experiences by providing gentle transitions that let people visually track changes. Avoid sudden, jarring transitions that might be disorienting or uncomfortable. For developer guidance, see [`CoordinateSpaceProtocol`](https://developer.apple.com/documentation/SwiftUI/CoordinateSpaceProtocol). + +**Let people choose when to enter or exit a more immersive experience.** It can be disorienting for someone to suddenly enter a more immersive experience when theyโ€™re not expecting it. Instead, provide a clear action to enter or exit immersion so people can decide when to be more immersed in your content, and when to leave. For example, Keynote provides a prominent Exit button in its fully immersive Rehearsal environment to help people return to the slide-viewing window. Avoid requiring people to use system controls to reduce immersion in your experience. + +**Indicate the purpose of an exit control.** Make sure your button clarifies whether it returns people to a previous, less immersive context or quits an experience altogether. If exiting your immersive experience also quits your app or game, consider providing controls that let people pause or return to a place where they can save their progress before quitting. + +## [Displaying virtual hands](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences#Displaying-virtual-hands) + +When your immersive app or game transitions to a Full Space, it can ask permission to hide a personโ€™s hands and instead show virtual hands that represent them. + +**Prefer virtual hands that match familiar characteristics.** For example, match the positions and gestures of the viewerโ€™s hands so they can continue to interact with your app or game in ways that feel natural. Hands that work in familiar ways help people stay immersed in the experience when in fully virtual worlds. + +**Use caution if you create virtual hands that are larger than the viewerโ€™s hands.** Virtual hands that are significantly bigger than human hands can prevent people from seeing the content theyโ€™re interested in and can make interactions feel clumsy. Also, large virtual hands can seem out of proportion with the space, appearing to be too close to the viewerโ€™s face. + +**If thereโ€™s an interruption in hand-tracking data, fade out virtual hands and reveal the viewerโ€™s own hands.** Donโ€™t let the virtual hands become unresponsive and appear frozen. When hand-tracking data returns, fade the virtual hands back in. + +## [Creating an environment](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences#Creating-an-environment) + +When your app or game transitions to a Full Space, you can replace passthrough with a custom environment that partially or completely surrounds a person, transporting them to a new place. The following guidelines can help you design a beautiful environment that people appreciate. + +**Minimize distracting content.** To help immerse people in a primary task like watching a video, avoid displaying a lot of movement or high-contrast details in your environment. Alternatively, when you want to draw peopleโ€™s attention to certain areas of your environment, consider techniques like using the highest quality textures and shapes in the important area while using lower quality assets and dimming in less important areas. + +**Help people distinguish interactive objects in your environment.** People often use an objectโ€™s proximity to help them decide if they can interact with it. For example, when you place a 3D object far away from people, they often donโ€™t try to touch or move toward it, but when you place a 3D object close to people, theyโ€™re more likely to try interacting with it. + +**Keep animation subtle.** Small, gentle movements, like clouds drifting or transforming, can enrich your custom environment without distracting people or making them uncomfortable. Always avoid displaying too much movement near the edges of a personโ€™s field of view. For guidance, see [Motion](https://developer.apple.com/design/human-interface-guidelines/motion). + +**Create an expansive environment, regardless of the place it depicts.** A small, restrictive environment can make people feel uncomfortable and even claustrophobic. + +**Use Spatial Audio to create atmosphere.** In visionOS, you use Spatial Audio to play sound that people can perceive as coming from specific locations in space, not just from speakers (for guidance, see [Playing audio](https://developer.apple.com/design/human-interface-guidelines/playing-audio)). As you design a soundscape that enhances your custom environment, keep the experience fresh and captivating by avoiding too much repetition or looping. If people can play other audio while theyโ€™re in your environment โ€” for example, while watching a movie โ€” be sure to lower the volume of the soundscape or stop it completely. + +**In general, avoid using a flat 360-degree image to create your environment.** A 360-degree image doesnโ€™t tend to give people a sense of scale when they view it in an environment, so it can reduce the immersiveness of the experience. Prefer creating object meshes that include lighting, and use shaders to implement subtle animations like the movements of clouds or leaves or the reflections of objects. + +**Help people feel grounded.** Always provide a ground plane mesh so people donโ€™t feel like theyโ€™re floating. If you must use a flat 360-degree image in your environment, adding a ground plane mesh can help it feel more realistic. + +**Minimize asset redundancy.** Using the same assets or models too frequently tends to make an environment feel less realistic. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences#Platform-considerations) + + _Not supported in iOS, iPadOS, macOS, tvOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences#Related) + +[Spatial layout](https://developer.apple.com/design/human-interface-guidelines/spatial-layout) + +[Motion](https://developer.apple.com/design/human-interface-guidelines/motion) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences#Developer-documentation) + +[Creating fully immersive experiences in your app](https://developer.apple.com/documentation/visionOS/creating-fully-immersive-experiences) โ€” visionOS + +[Incorporating real-world surroundings in an immersive experience](https://developer.apple.com/documentation/visionOS/incorporating-real-world-surroundings-in-an-immersive-experience) โ€” visionOS + +[`ImmersionStyle`](https://developer.apple.com/documentation/SwiftUI/ImmersionStyle) โ€” visionOS + +[Immersive spaces](https://developer.apple.com/documentation/SwiftUI/Immersive-spaces) โ€” SwiftUI + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/15489B11-8744-483D-AD38-EF78D8962FF4/8126_wide_250x141_1x.jpg) Principles of spatial design ](https://developer.apple.com/videos/play/wwdc2023/10072) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/942191E7-9B98-487D-AE81-400D58285B31/8129_wide_250x141_1x.jpg) Design spatial SharePlay experiences ](https://developer.apple.com/videos/play/wwdc2023/10075) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/BEBF6FDD-D987-4A45-AF6F-6D4C4575E69F/9198_wide_250x141_1x.jpg) Create custom environments for your immersive apps in visionOS ](https://developer.apple.com/videos/play/wwdc2024/10087) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences#Change-log) + +Date| Changes +---|--- +June 9, 2025| Clarified guidance and noted the availability of portrait-oriented progressive immersion. +November 19, 2024| Refined immersion style guidance and added artwork. +June 10, 2024| Added guidance for tinting passthrough and specifying initial, minimum, and maximum immersion levels. +May 7, 2024| Added guidance for creating an environment. +February 2, 2024| Clarified guidance for choosing an immersion style that matches the experience your app provides. +October 24, 2023| Updated artwork. +June 21, 2023| New page. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/inclusion.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/inclusion.md new file mode 100644 index 00000000..7946182b --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/inclusion.md @@ -0,0 +1,189 @@ +--- +title: "Inclusion | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/inclusion + +# Inclusion + +Inclusive apps and games put people first by prioritizing respectful communication and presenting content and functionality in ways that everyone can access and understand. + +![A sketch of two people, suggesting inclusion. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/498c87708887321ec79abcf0c45abc66/foundations-inclusion-intro%402x.png) + +To help you design an inclusive app or game, consider the following goals as you review the words and images you use and the experiences you offer. + +As with all design, designing an inclusive app is an iterative process that takes time to get right. Throughout the process, be prepared to examine your assumptions about how other people think and feel and be open to evolving knowledge and understanding. + +## [Inclusive by design](https://developer.apple.com/design/human-interface-guidelines/inclusion#Inclusive-by-design) + +Simple, intuitive experiences are at the core of well-designed apps and games. To design an intuitive experience, you start by investigating peopleโ€™s goals and perspectives so you can present content that resonates with them. + +Empathy is an important tool in this investigation because it helps you understand how people with different perspectives might respond to the content and experiences you create. For example, you might discover that from some perspectives a word or image is incomprehensible or has a meaning you donโ€™t intend. + +Although each personโ€™s perspective comprises a unique intersection of human qualities thatโ€™s both distinct and dynamic, all perspectives arise from human characteristics and experiences that everyone shares, including: + + * Age + + * Gender and gender identity + + * Race and ethnicity + + * Sexuality + + * Physical attributes + + * Cognitive attributes + + * Permanent, temporary, and situational disabilities + + * Language and culture + + * Religion + + * Education + + * Political or philosophical opinions + + * Social and economic context + + + + +As you examine your app or game through different perspectives, avoid framing the work as merely a search for content that might give offense. Although no design should contain offensive material or experiences, an inoffensive app or game isnโ€™t necessarily an inclusive one. Focusing on inclusion can help you avoid potentially offensive content while also helping you create a welcoming experience that everyone can enjoy. + +## [Welcoming language](https://developer.apple.com/design/human-interface-guidelines/inclusion#Welcoming-language) + +Using plain, inclusive language welcomes everyone and helps them understand your app or game. Carefully review the writing in your experience to make sure that your tone and words donโ€™t exclude people. Here are a few tips for writing text โ€” also known as _copy_ โ€” thatโ€™s direct, easy to understand, and inclusive. + +**Consider the tone of your copy from different perspectives.** The style of your writing communicates almost as much as the words you use. Although different apps use different communication styles, make sure the tone you use doesnโ€™t send messages you donโ€™t intend. For example, an academic tone can make an app or game seem like it welcomes only high levels of education. As you seek the style thatโ€™s right for your experience, be clear, direct, and respectful. + +**Pay attention to how you refer to people.** It typically works well to use _you_ and _your_ to address people directly. Referring to people indirectly as _the user_ or _the player_ can make your experience feel distant and unwelcoming. Also, consider reserving words like _we_ and _our_ to represent your software or company; otherwise, these terms can suggest a personal relationship with people that might be interpreted as insulting or condescending. + +**Avoid using specialized or technical terms without defining them.** Using specialized or technical terms can make your writing more succinct, but doing so excludes people who donโ€™t know what the terms mean. If you must use such terms, be sure to define them first and make the definitions easy for people to look up. Even when people know the definition of a specialized or technical term in a sentence, the sentence is easier to read โ€” and translate โ€” when it uses plain language instead. + +**Replace colloquial expressions with plain language.** Colloquial expressions are often culture-specific and can be difficult to translate. Worse, some colloquial phrases have exclusionary meanings you might not know. For example, the phrases _peanut gallery_ and _grandfathered in_ both arose from oppressive contexts and continue to exclude people. Even when a colloquial phrase doesnโ€™t have an exclusionary meaning, it can still exclude everyone who doesnโ€™t understand it. + +**Consider carefully before including humor.** Humor is highly subjective and โ€” similar to colloquial expressions โ€” difficult to translate from one culture to another. Including humor in your experience risks confusing people who donสผt understand it, irritating people who tire of repeatedly encountering it, and insulting people who interpret it differently. For additional writing guidance, see [Writing inclusively](https://help.apple.com/applestyleguide/#/apdcb2a65d68). + +## [Being approachable](https://developer.apple.com/design/human-interface-guidelines/inclusion#Being-approachable) + +An approachable app or game doesnโ€™t require people to have particular skills or knowledge before they can use it, and it gives people a clear path toward deepening their understanding over time. Here are two ways to help make an experience approachable. + + * Present a clear, straightforward interface. To help you design a simple interface that fits in with other experiences on each platform, see [Designing for iOS](https://developer.apple.com/design/human-interface-guidelines/designing-for-ios), [Designing for iPadOS](https://developer.apple.com/design/human-interface-guidelines/designing-for-ipados), [Designing for macOS](https://developer.apple.com/design/human-interface-guidelines/designing-for-macos), [Designing for tvOS](https://developer.apple.com/design/human-interface-guidelines/designing-for-tvos), [Designing for visionOS](https://developer.apple.com/design/human-interface-guidelines/designing-for-visionos), [Designing for watchOS](https://developer.apple.com/design/human-interface-guidelines/designing-for-watchos), and [Designing for games](https://developer.apple.com/design/human-interface-guidelines/designing-for-games). + + * Build in ways to learn how to use your app or game. Consider designing an onboarding flow that helps people who are new to your experience take a step-by-step approach while letting others skip straight to the content they want. For guidance, see [Onboarding](https://developer.apple.com/design/human-interface-guidelines/onboarding). + + + + +## [Gender identity](https://developer.apple.com/design/human-interface-guidelines/inclusion#Gender-identity) + +Throughout history, cultures around the world have recognized a spectrum of self-identity and expression that expands beyond the binary variants of woman and man. + +You can help everyone feel welcome in your app or game by avoiding unnecessary references to specific genders. For example, a recipe-sharing app that uses copy like โ€œYou can let a subscriber post his or her recipes to your shared folderโ€ could avoid unnecessary gender references by using an alternative like โ€œSubscribers can post recipes to your shared folder.โ€ In addition to using the gender-neutral noun โ€œsubscribers,โ€ the revised copy avoids the unnecessary singular pronouns โ€œhisโ€ and โ€œher,โ€ helping the sentence remain inclusive when itโ€™s localized for languages that use gendered pronouns. + +In addition, you can often avoid referencing a specific gender in an avatar, emoji, glyph, or game character. To welcome everyone to your app or game, prefer giving people the tools they need to customize such items as they choose. + +If you need to depict a generic person or people, use a nongendered human image to reinforce the message that _generic person_ means _human_ , not _man_ or _woman_. SF Symbols provides many nongendered glyphs you can use, such as the figure and person symbols shown here: + +![A solid silhouette of a person from the shoulders up, within a circle.](https://docs-assets.developer.apple.com/published/22f3909c1b433ca2181d2fdcf193fff7/person-crop-circle%402x.png)person.crop.circle + +![Solid silhouettes of three people, with the left silhouette in the foreground and the other two in the background, all from the shoulders up.](https://docs-assets.developer.apple.com/published/5edbc84a409deb59e72f4d780b8e7b94/person-3-fill%402x.png)person.3.fill + +![A solid silhouette of a person standing with an arm raised high on the left side of the image.](https://docs-assets.developer.apple.com/published/ea7ebde0ec424a8dc74961a3670724b2/figure-wave%402x.png)figure.wave + +Most apps and games donโ€™t need to know a personโ€™s gender, but if you require this information โ€” such as for health or legal reasons โ€” consider providing inclusive options, such as _nonbinary_ , _self-identify_ , and _decline to state_. In this situation, you could also let people specify the pronouns they use so you can address them properly when necessary. + +## [People and settings](https://developer.apple.com/design/human-interface-guidelines/inclusion#People-and-settings) + +Portraying human diversity is one of the most noticeable ways your app or game can welcome everyone. When people recognize others like themselves within an experience and its related materials, theyโ€™re less likely to feel excluded and can be more likely to think theyโ€™ll benefit from it. + +As you create copy and images that represent people, portray a range of human characteristics and activities. For example, a fitness app could feature exercise moves demonstrated by people with different racial backgrounds, body types, ages, and physical capabilities. If you need to depict occupations or behaviors, avoid stereotypical representations, such as showing only male doctors, female nurses, or heroes and villains that may perpetuate real-world racial or gender stereotypes. + +Also review the settings and objects you show. For example, showing high levels of affluence might make sense in some scenarios, but in other cases it can be unwelcoming and make an experience seem out of touch. When it makes sense in your app or game, prefer showing places, homes, activities, and items that are familiar and relatable to most people. + +## [Avoiding stereotypes](https://developer.apple.com/design/human-interface-guidelines/inclusion#Avoiding-stereotypes) + +Everyone holds biases and stereotypes โ€” often unconsciously โ€” and it can be challenging to discover how they affect your thoughts. A goal of inclusive design is to become aware of your biases and generalizations so you can recognize where they might influence your design decisions. + +For example, consider an app that helps people manage account access for various family members. If this app uses a stereotypical definition of _family_ โ€” such as a woman, a man, and their biological children โ€” itโ€™s likely to communicate this perspective in its copy and images. Because the app assumes that peopleโ€™s families fit this narrow definition, it excludes everyone whose family is different. + +Although the assumption made in the account-access app might seem like an obvious mistake, itโ€™s important to realize that not all assumptions are so easy to spot. For example, consider an app or game that requires people to choose security questions they can answer for future identity confirmation, such as: + + * What was your favorite subject in college? + + * What was the make of your first car? + + * How did you feel when you first saw a rainbow? + + + + +From some perspectives these questions refer to commonplace events, but all are based on experiences that not everyone has. Using a context-specific experience to communicate something is useless for everyone who doesnโ€™t share that context and effectively excludes them. To create alternatives to the culture- and capability-specific questions above, you might reference more universal human experiences like: + + * Whatโ€™s your favorite activity? + + * What was the name of your first friend? + + * What quality describes you best? + + + + +Basing design decisions on stereotypes or assumptions inevitably leads to exclusion because generalizations canโ€™t reflect the diversity of human perspectives. Avoiding assumptions and instead concentrating on inclusion can help you craft experiences that benefit everyone. + +## [Accessibility](https://developer.apple.com/design/human-interface-guidelines/inclusion#Accessibility) + +An inclusive app or game is accessible to everyone. People rely on Appleโ€™s accessibility features โ€” such as VoiceOver, Display Accommodations, closed captioning, Switch Control, and Speak Screen โ€” to customize their devices for their individual needs, so itโ€™s essential to support these features. + +Itโ€™s also essential to avoid assuming that any disability might prevent someone from wanting to enjoy the experience your software provides. Making an assumption like this can result in designs that limit the potential audience for your app or game. In contrast, when you make each experience accessible, you give everyone the opportunity to benefit from your app or game in ways that work for them. + +To help you design an app or game that everyone can enjoy, remember that: + + * Each disability is a spectrum. For example, visual disabilities range from low vision to complete blindness, and include things like color blindness, blurry vision, light sensitivity, and peripheral vision loss. + + * Everyone can experience disabilities. In addition to disabilities that most people experience as they age, there are _temporary disabilities_ โ€” like short-term hearing loss due to an infection โ€” and _situational disabilities_ โ€” like being unable to hear while on a noisy train โ€” that can affect everyone at various times. + + + + +As you design content that welcomes people of all abilities, consider the following tips. + +**Avoid images and language that exclude people with disabilities.** For example, include people with disabilities when you represent a variety of people, and avoid language that uses a disability to express a negative quality. + +**Take a people-first approach when writing about people with disabilities.** For example, you could describe an individualโ€™s accomplishments and goals before mentioning a disability they may have. If youโ€™re writing about a specific person or community, find out how they self-identify; for more guidance, see [Writing about disability](https://help.apple.com/applestyleguide/#/apd49cbb2b06). + +**Prioritize simplicity and perceivability.** Prefer familiar, consistent interactions that make tasks simple to perform, and ensure that everyone can perceive your content, whether they use sight, hearing, or touch. + +To learn more about making your app or game accessible, see [Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility). + +## [Languages](https://developer.apple.com/design/human-interface-guidelines/inclusion#Languages) + +People expect to customize their device by choosing a language for text and a region for formatting values like date, time, and money. To welcome a global audience, first prepare your software to handle languages and regions other than your own โ€” a process called _internationalization_ โ€” and provide translated text and resources for specific locales. For an overview of internationalization, see [Expanding your app to new markets](https://developer.apple.com/localization/); for developer guidance on localization, see [Localization](https://developer.apple.com/documentation/Xcode/localization). + +Creating an inclusive experience can also help you prepare for localization. For example, using plain language, avoiding unnecessary gender references, representing a variety of people, and avoiding stereotypes and culture-specific content, can put you in a good position to create versions of your software localized into more languages. Using [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) for the glyphs in your app or game can also help streamline localization. In addition to providing many language-specific glyphs, SF Symbols includes glyphs you can use in both left-to-right and right-to-left contexts; for guidance, see [Right to left](https://developer.apple.com/design/human-interface-guidelines/right-to-left). + +As you localize your app or game and related content, also be aware of the ways you use color. Colors often have strong culture-specific meanings, so itโ€™s essential to discover how people respond to specific colors in each locale you support. In some places, for example, white is associated with death or grief, whereas in other places, itโ€™s associated with purity or peace. If you use color as a way to communicate, make sure your color choices communicate the same thing in each version of your software. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/inclusion#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/inclusion#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/inclusion#Related) + +[Writing inclusively](https://help.apple.com/applestyleguide/#/apdcb2a65d68) + +[Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/inclusion#Developer-documentation) + +[Localization](https://developer.apple.com/documentation/Xcode/localization) โ€” Xcode + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/inclusion#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/F5AEB5B6-FF48-4201-B110-A0EDA465F3B4/9961_wide_250x141_1x.jpg) Principles of inclusive app design ](https://developer.apple.com/videos/play/wwdc2025/316) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/90B67086-3A99-49A5-965A-D35DB6552AE0/5206_wide_250x141_1x.jpg) The practice of inclusive design ](https://developer.apple.com/videos/play/wwdc2021/10275) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/30932E3E-C589-4804-B16E-D0003DEF0F02/5247_wide_250x141_1x.jpg) The process of inclusive design ](https://developer.apple.com/videos/play/wwdc2021/10304) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/layout.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/layout.md new file mode 100644 index 00000000..ddd54a29 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/layout.md @@ -0,0 +1,425 @@ +--- +title: "Layout | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/layout + +# Layout + +A consistent layout that adapts to various contexts makes your experience more approachable and helps people enjoy their favorite apps and games on all their devices. + +![A sketch of a small rectangle in the upper-left quadrant of a larger rectangle, suggesting the position of a user interface element within a window. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/fe3e14f290a6986d2490634a9e2fab0c/foundations-layout-intro%402x.png) + +Your appโ€™s layout helps ground people in your content from the moment they open it. People expect familiar relationships between controls and content to help them use and discover your appโ€™s features, and designing the layout to take advantage of this makes your app feel at home on the platform. + +Apple provides templates, guides, and other resources that can help you integrate Apple technologies and design your apps and games to run on all Apple platforms. See [Apple Design Resources](https://developer.apple.com/design/resources/). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/layout#Best-practices) + +**Group related items to help people find the information they want.** For example, you might use negative space, background shapes, colors, materials, or separator lines to show when elements are related and to separate information into distinct areas. When you do so, ensure that content and controls remain clearly distinct. + +**Make essential information easy to find by giving it sufficient space.** People want to view the most important information right away, so donโ€™t obscure it by crowding it with nonessential details. You can make secondary information available in other parts of the window, or include it in an additional view. + +**Extend content to fill the screen or window.** Make sure backgrounds and full-screen artwork extend to the edges of the display. Also ensure that scrollable layouts continue all the way to the bottom and the sides of the device screen. Controls and navigation components like sidebars and tab bars appear on top of content rather than on the same plane, so itโ€™s important for your layout to take this into account. + +When your content doesnโ€™t span the full window, use a background extension view to provide the appearance of content behind the control layer on either side of the screen, such as beneath the sidebar or inspector. For developer guidance, see [`backgroundExtensionEffect()`](https://developer.apple.com/documentation/SwiftUI/View/backgroundExtensionEffect\(\)) and [`UIBackgroundExtensionView`](https://developer.apple.com/documentation/UIKit/UIBackgroundExtensionView). + +![A screenshot of a full screen iPad app with a sidebar on the leading edge. A photo of Mount Fuji fills the top half of the content area. The photo subtly blurs as it reaches the top of the screen, where toolbar items float above it grouped on the trailing edge. Where the photo meets the sidebar, the image flips, blurs, and extends fully beneath the sidebar to the edge of the screen.](https://docs-assets.developer.apple.com/published/ffacfee843cc378d0af09d8926f2408b/layout-background-extention-view%402x.png) + +## [Visual hierarchy](https://developer.apple.com/design/human-interface-guidelines/layout#Visual-hierarchy) + +**Differentiate controls from content.** Take advantage of the Liquid Glass material to provide a distinct appearance for controls thatโ€™s consistent across iOS, iPadOS, and macOS. Instead of a background, use a scroll edge effect to provide a transition between content and the control area. For guidance, see [Scroll views](https://developer.apple.com/design/human-interface-guidelines/scroll-views). + +**Place items to convey their relative importance.** People often start by viewing items in reading order โ€” that is, from top to bottom and from the leading to trailing side โ€” so it generally works well to place the most important items near the top and leading side of the window, display, or [field of view](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Field-of-view). Be aware that reading order varies by language, and take [right to left](https://developer.apple.com/design/human-interface-guidelines/right-to-left) languages into account as you design. + +**Align components with one another to make them easier to scan and to communicate organization and hierarchy.** Alignment makes an app look neat and organized and can help people track content while scrolling or moving their eyes, making it easier to find information. Along with indentation, alignment can also help people understand an information hierarchy. + +**Take advantage of progressive disclosure to help people discover content thatโ€™s currently hidden.** For example, if you canโ€™t display all the items in a large collection at once, you need to indicate that there are additional items that arenโ€™t currently visible. Depending on the platform, you might use a [disclosure control](https://developer.apple.com/design/human-interface-guidelines/disclosure-controls), or display parts of items to hint that people can reveal additional content by interacting with the view, such as by scrolling. + +**Make controls easier to use by providing enough space around them and grouping them in logical sections.** If unrelated controls are too close together โ€” or if other content crowds them โ€” they can be difficult for people to tell apart or understand what they do, which can make your app or game hard to use. For guidance, see [Toolbars](https://developer.apple.com/design/human-interface-guidelines/toolbars). + +## [Adaptability](https://developer.apple.com/design/human-interface-guidelines/layout#Adaptability) + +Every app and game needs to adapt when the device or system context changes. In iOS, iPadOS, tvOS, and visionOS, the system defines a collection of _traits_ that characterize variations in the device environment that can affect the way your app or game looks. Using SwiftUI or Auto Layout can help you ensure that your interface adapts dynamically to these traits and other context changes; if you donโ€™t use these tools, you need to use alternative methods to do the work. + +Here are some of the most common device and system variations you need to handle: + + * Different device screen sizes, resolutions, and color spaces + + * Different device orientations (portrait/landscape) + + * System features like Dynamic Island and camera controls + + * External display support, Display Zoom, and resizable windows on iPad + + * Dynamic Type text-size changes + + * Locale-based internationalization features like left-to-right/right-to-left layout direction, date/time/number formatting, font variation, and text length + + + + +**Design a layout that adapts gracefully to context changes while remaining recognizably consistent.** People expect your experience to work well and remain familiar when they rotate their device, resize a window, add another display, or switch to a different device. You can help ensure an adaptable interface by respecting system-defined safe areas, margins, and guides (where available) and specifying layout modifiers to fine-tune the placement of views in your interface. + +**Be prepared for text-size changes.** People appreciate apps and games that respond when they choose a different text size. When you support [Dynamic Type](https://developer.apple.com/design/human-interface-guidelines/typography#Supporting-Dynamic-Type) โ€” a feature that lets people choose the size of visible text in iOS, iPadOS, tvOS, visionOS, and watchOS โ€” your app or game can respond appropriately when people adjust text size. To support Dynamic Type in your Unity-based game, use Appleโ€™s accessibility plug-in (for developer guidance, see [Apple โ€“ Accessibility](https://github.com/apple/unityplugins/blob/main/plug-ins/Apple.Accessibility/Apple.Accessibility_Unity/Assets/Apple.Accessibility/Documentation~/Apple.Accessibility.md)). For guidance on displaying text in your app, see [Typography](https://developer.apple.com/design/human-interface-guidelines/typography). + +**Preview your app on multiple devices, using different orientations, localizations, and text sizes.** You can streamline the testing process by first testing versions of your experience that use the largest and the smallest layouts. Although itโ€™s generally best to preview features like wide-gamut color on actual devices, you can use Xcode Simulator to check for clipping and other layout issues. For example, if your iOS app or game supports landscape mode, you can use Simulator to make sure your layouts look great whether the device rotates left or right. + +**When necessary, scale artwork in response to display changes.** For example, viewing your app or game in a different context โ€” such as on a screen with a different aspect ratio โ€” might make your artwork appear cropped, letterboxed, or pillarboxed. If this happens, donโ€™t change the aspect ratio of the artwork; instead, scale it so that important visual content remains visible. In visionOS, the system automatically [scales](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Scale) a window when it moves along the z-axis. + +## [Guides and safe areas](https://developer.apple.com/design/human-interface-guidelines/layout#Guides-and-safe-areas) + +A _layout guide_ defines a rectangular region that helps you position, align, and space your content on the screen. The system includes predefined layout guides that make it easy to apply standard margins around content and restrict the width of text for optimal readability. You can also define custom layout guides. For developer guidance, see [`UILayoutGuide`](https://developer.apple.com/documentation/UIKit/UILayoutGuide) and [`NSLayoutGuide`](https://developer.apple.com/documentation/AppKit/NSLayoutGuide). + +A _safe area_ defines the area within a view that isnโ€™t covered by a toolbar, tab bar, or other views a window might provide. Safe areas are essential for avoiding a deviceโ€™s interactive and display features, like Dynamic Island on iPhone or the camera housing on some Mac models. For developer guidance, see [`SafeAreaRegions`](https://developer.apple.com/documentation/SwiftUI/SafeAreaRegions) and [Positioning content relative to the safe area](https://developer.apple.com/documentation/UIKit/positioning-content-relative-to-the-safe-area). + +**Respect key display and system features in each platform.** When an app or game doesnโ€™t accommodate such features, it doesnโ€™t feel at home in the platform and may be harder for people to use. In addition to helping you avoid display and system features, safe areas can also help you account for interactive components like bars, dynamically repositioning content when sizes change. + +For templates that include the guides and safe areas for each platform, see [Apple Design Resources](https://developer.apple.com/design/resources/). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/layout#Platform-considerations) + +### [iOS](https://developer.apple.com/design/human-interface-guidelines/layout#iOS) + +**Aim to support both portrait and landscape orientations.** People appreciate apps and games that work well in different device orientations, but sometimes your experience needs to run in only portrait or only landscape. When this is the case, you can rely on people trying both orientations before settling on the one you support โ€” thereโ€™s no need to tell people to rotate their device. If your app or game is landscape-only, make sure it runs equally well whether people rotate their device to the left or the right. + +**Prefer a full-bleed interface for your game.** Give players a beautiful interface that fills the screen while accommodating the corner radius, sensor housing, and features like Dynamic Island. If necessary, consider giving players the option to view your game using a letterboxed or pillarboxed appearance. + +**Avoid full-width buttons.** Buttons feel at home in iOS when they respect system-defined margins and are inset from the edges of the screen. If you need to include a full-width button, make sure it harmonizes with the curvature of the hardware and aligns with adjacent safe areas. + +**Hide the status bar only when it adds value or enhances your experience.** The status bar displays information people find useful and it occupies an area of the screen most apps donโ€™t fully use, so itโ€™s generally a good idea to keep it visible. The exception is if you offer an in-depth experience like playing a game or viewing media, where it might make sense to hide the status bar. + +### [iPadOS](https://developer.apple.com/design/human-interface-guidelines/layout#iPadOS) + +People can freely resize windows down to a minimum width and height, similar to window behavior in macOS. Itโ€™s important to account for this resizing behavior and the full range of possible window sizes when designing your layout. For guidance, see [Multitasking](https://developer.apple.com/design/human-interface-guidelines/multitasking#iPadOS) and [Windows](https://developer.apple.com/design/human-interface-guidelines/windows#iPadOS). + +**As someone resizes a window, defer switching to a compact view for as long as possible.** Design for a full-screen view first, and only switch to a compact view when a version of the full layout no longer fits. This helps the UI feel more stable and familiar in as many situations as possible. For more complex layouts such as [split views](https://developer.apple.com/design/human-interface-guidelines/split-views), prefer hiding tertiary columns such as inspectors as the view narrows. + +**Test your layout at common system-provided sizes, and provide smooth transitions.** Window controls provide the option to arrange windows to fill halves, thirds, and quadrants of the screen, so itโ€™s important to check your layout at each of these sizes on a variety of devices. Be sure to minimize unexpected UI changes as people adjust down to the minimum and up to the maximum window size. + +**Consider a convertible tab bar for adaptive navigation.** For many apps, you donโ€™t need to choose between a tab bar or sidebar for navigation; instead, you can adopt a style of tab bar that provides both. The app first launches with your choice of a sidebar or a tab bar, and then people can tap to switch between them. As the view resizes, the presentation style changes to fit the width of the view. For guidance, see [Tab bars](https://developer.apple.com/design/human-interface-guidelines/tab-bars). For developer guidance, see [`sidebarAdaptable`](https://developer.apple.com/documentation/SwiftUI/TabViewStyle/sidebarAdaptable). + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/layout#macOS) + +**Avoid placing controls or critical information at the bottom of a window.** People often move windows so that the bottom edge is below the bottom of the screen. + +**Avoid displaying content within the camera housing at the top edge of the window.** For developer guidance, see [`NSPrefersDisplaySafeAreaCompatibilityMode`](https://developer.apple.com/documentation/BundleResources/Information-Property-List/NSPrefersDisplaySafeAreaCompatibilityMode). + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/layout#tvOS) + +**Be prepared for a wide range of TV sizes.** On Apple TV, layouts donโ€™t automatically adapt to the size of the screen like they do on iPhone or iPad. Instead, apps and games show the same interface on every display. Take extra care in designing your layout so that it looks great in a variety of screen sizes. + +**Adhere to the screenโ€™s safe area.** Inset primary content 60 points from the top and bottom of the screen, and 80 points from the sides. It can be difficult for people to see content that close to the edges, and unintended cropping can occur due to overscanning on older TVs. Allow only partially displayed offscreen content and elements that deliberately flow offscreen to appear outside this zone. + +![An illustration of a TV with a safe zone border on all sides. In width, the top and bottom borders measure 60 points, and the side borders both measure 80 points.](https://docs-assets.developer.apple.com/published/1be425edd08beb67cba3c1000983581f/visual-design-safe-zone%402x.png) + +**Include appropriate padding between focusable elements.** When you use UIKit and the focus APIs, an element gets bigger when it comes into focus. Consider how elements look when theyโ€™re focused, and make sure you donโ€™t let them overlap important information. For developer guidance, see [About focus interactions for Apple TV](https://developer.apple.com/documentation/UIKit/about-focus-interactions-for-apple-tv). + +![An illustration that uses vertical shaded rectangles to show padding between focusable items.](https://docs-assets.developer.apple.com/published/1cfcdddb80150197945945a6884a9ade/visual-design-padding%402x.png) + +#### [Grids](https://developer.apple.com/design/human-interface-guidelines/layout#Grids) + +The following grid layouts provide an optimal viewing experience. Be sure to use appropriate spacing between unfocused rows and columns to prevent overlap when an item comes into focus. + +If you use the UIKit collection view flow element, the number of columns in a grid is automatically determined based on the width and spacing of your content. For developer guidance, see [`UICollectionViewFlowLayout`](https://developer.apple.com/documentation/UIKit/UICollectionViewFlowLayout). + + * Two-column + * Three-column + * Four-column + * Five-column + * Six-column + * Seven-column + * Eight-column + * Nine-column + + + +![An illustration of Apple TV, displaying a two-column grid of media items. Additional media items are partially visible on the right side and bottom edge of the screen.](https://docs-assets.developer.apple.com/published/29cbd7ef913d834c991bd303816e410d/visual-design-grid-2-column%402x.png) + +#### [Two-column grid](https://developer.apple.com/design/human-interface-guidelines/layout#Two-column-grid) + +Attribute| Value +---|--- +Unfocused content width| 860 pt +Horizontal spacing| 40 pt +Minimum vertical spacing| 100 pt + +![An illustration of Apple TV, displaying a three-column grid of media items. Additional media items are partially visible on the right side and bottom edge of the screen.](https://docs-assets.developer.apple.com/published/efc27c2f40d150e6350f03d8709527d8/visual-design-grid-3-column%402x.png) + +#### [Three-column grid](https://developer.apple.com/design/human-interface-guidelines/layout#Three-column-grid) + +Attribute| Value +---|--- +Unfocused content width| 560 pt +Horizontal spacing| 40 pt +Minimum vertical spacing| 100 pt + +![An illustration of Apple TV, displaying a four-column grid of media items. Additional media items are partially visible on the right side of the screen.](https://docs-assets.developer.apple.com/published/b02a182e769f7a89201719f46547dabf/visual-design-grid-4-column%402x.png) + +#### [Four-column grid](https://developer.apple.com/design/human-interface-guidelines/layout#Four-column-grid) + +Attribute| Value +---|--- +Unfocused content width| 410 pt +Horizontal spacing| 40 pt +Minimum vertical spacing| 100 pt + +![An illustration of Apple TV, displaying a five-column grid of media items. Additional media items are partially visible on the right side and bottom edge of the screen.](https://docs-assets.developer.apple.com/published/6eebe97a166aceb55ed18304ac46be8d/visual-design-grid-5-column%402x.png) + +#### [Five-column grid](https://developer.apple.com/design/human-interface-guidelines/layout#Five-column-grid) + +Attribute| Value +---|--- +Unfocused content width| 320 pt +Horizontal spacing| 40 pt +Minimum vertical spacing| 100 pt + +![An illustration of Apple TV, displaying a six-column grid of media items. Additional media items are partially visible on the right side and bottom edge of the screen.](https://docs-assets.developer.apple.com/published/a2a7efa8dc58b3615082ba7e62e81437/visual-design-grid-6-column%402x.png) + +#### [Six-column grid](https://developer.apple.com/design/human-interface-guidelines/layout#Six-column-grid) + +Attribute| Value +---|--- +Unfocused content width| 260 pt +Horizontal spacing| 40 pt +Minimum vertical spacing| 100 pt + +![An illustration of Apple TV, displaying a seven-column grid of media items. Additional media items are partially visible on the right side of the screen.](https://docs-assets.developer.apple.com/published/3e625b746a4a31f083020cfa91674bd6/visual-design-grid-7-column%402x.png) + +#### [Seven-column grid](https://developer.apple.com/design/human-interface-guidelines/layout#Seven-column-grid) + +Attribute| Value +---|--- +Unfocused content width| 217 pt +Horizontal spacing| 40 pt +Minimum vertical spacing| 100 pt + +![An illustration of Apple TV, displaying an eight-column grid of media items. Additional media items are partially visible on the right side and bottom edge of the screen.](https://docs-assets.developer.apple.com/published/71f872111291a6f1b465ddfd4f4dc246/visual-design-grid-8-column%402x.png) + +#### [Eight-column grid](https://developer.apple.com/design/human-interface-guidelines/layout#Eight-column-grid) + +Attribute| Value +---|--- +Unfocused content width| 184 pt +Horizontal spacing| 40 pt +Minimum vertical spacing| 100 pt + +![An illustration of Apple TV, displaying a nine-column grid of media items.](https://docs-assets.developer.apple.com/published/19125b211b45864b26f33d8f54a98a87/visual-design-grid-9-column%402x.png) + +#### [Nine-column grid](https://developer.apple.com/design/human-interface-guidelines/layout#Nine-column-grid) + +Attribute| Value +---|--- +Unfocused content width| 160 pt +Horizontal spacing| 40 pt +Minimum vertical spacing| 100 pt + +**Include additional vertical spacing for titled rows.** If a row has a title, provide enough spacing between the bottom of the previous unfocused row and the center of the title to avoid crowding. Also provide spacing between the bottom of the title and the top of the unfocused items in the row. + +**Use consistent spacing.** When content isnโ€™t consistently spaced, it no longer looks like a grid and itโ€™s harder for people to scan. + +**Make partially hidden content look symmetrical.** To help direct attention to the fully visible content, keep partially hidden offscreen content the same width on each side of the screen. + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/layout#visionOS) + +The guidance below can help you lay out content within the windows of your visionOS app or game, making it feel familiar and easy to use. For guidance on displaying windows in space and best practices for using depth, scale, and field of view in your visionOS app, see [Spatial layout](https://developer.apple.com/design/human-interface-guidelines/spatial-layout). To learn more about visionOS window components, see [Windows > visionOS](https://developer.apple.com/design/human-interface-guidelines/windows#visionOS). + +Note + +When you add depth to content in a standard window, the content extends beyond the windowโ€™s bounds along the z-axis. If content extends too far along the z-axis, the system clips it. + +**Consider centering the most important content and controls in your app or game.** Often, people can more easily discover and interact with content when itโ€™s near the middle of a window, especially when the window is large. + +**Keep a windowโ€™s content within its bounds.** In visionOS, the system displays window controls just outside a windowโ€™s bounds in the XY plane. For example, the Share menu appears above the window and the controls for resizing, moving, and closing the window appear below it. Letting 2D or 3D content encroach on these areas can make the system-provided controls, especially those below the window, difficult for people to use. + +**If you need to display additional controls that donโ€™t belong within a window, use an ornament.** An ornament lets you offer app controls that remain visually associated with a window without interfering with the system-provided controls. For example, a windowโ€™s toolbar and tab bar appear as ornaments. For guidance, see [Ornaments](https://developer.apple.com/design/human-interface-guidelines/ornaments). + +**Make a windowโ€™s interactive components easy for people to look at.** You need to include enough space around an interactive component so that visually identifying it is easy and comfortable, and to prevent the system-provided hover effect from obscuring other content. For example, place buttons so their centers are at least 60 points apart. For guidance, see [Eyes](https://developer.apple.com/design/human-interface-guidelines/eyes), [Spatial layout](https://developer.apple.com/design/human-interface-guidelines/spatial-layout), and [Buttons > visionOS](https://developer.apple.com/design/human-interface-guidelines/buttons#visionOS). + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/layout#watchOS) + +**Design your content to extend from one edge of the screen to the other.** The Apple Watch bezel provides a natural visual padding around your content. To avoid wasting valuable space, consider minimizing the padding between elements. + +![An illustration of the Workout appโ€™s main list of workouts on Apple Watch. A callout indicates that the currently focused workout item spans the full width of the available screen area.](https://docs-assets.developer.apple.com/published/9b9b27a4e9e752fc4ed6be98f5eb5b0d/layout-full-width%402x.png) + +**Avoid placing more than two or three controls side by side in your interface.** As a general rule, display no more than three buttons that contain glyphs โ€” or two buttons that contain text โ€” in a row. Although itโ€™s usually better to let text buttons span the full width of the screen, two side-by-side buttons with short text labels can also work well, as long as the screen doesnโ€™t scroll. + +![A diagram of an Apple Watch screen showing two side-by-side buttons beneath three lines of text.](https://docs-assets.developer.apple.com/published/25c5882538789bded5a9953eb5e2001f/layout-controls%402x.png) + +**Support autorotation in views people might want to show others.** When people flip their wrist away, apps typically respond to the motion by sleeping the display, but in some cases it makes sense to autorotate the content. For example, a wearer might want to show an image to a friend or display a QR code to a reader. For developer guidance, see [`isAutorotating`](https://developer.apple.com/documentation/WatchKit/WKExtension/isAutorotating). + +## [Specifications](https://developer.apple.com/design/human-interface-guidelines/layout#Specifications) + +### [iOS, iPadOS device screen dimensions](https://developer.apple.com/design/human-interface-guidelines/layout#iOS-iPadOS-device-screen-dimensions) + +Model| Dimensions (portrait) +---|--- +iPad Pro 12.9-inch| 1024x1366 pt (2048x2732 px @2x) +iPad Pro 11-inch| 834x1194 pt (1668x2388 px @2x) +iPad Pro 10.5-inch| 834x1194 pt (1668x2388 px @2x) +iPad Pro 9.7-inch| 768x1024 pt (1536x2048 px @2x) +iPad Air 13-inch| 1024x1366 pt (2048x2732 px @2x) +iPad Air 11-inch| 820x1180 pt (1640x2360 px @2x) +iPad Air 10.9-inch| 820x1180 pt (1640x2360 px @2x) +iPad Air 10.5-inch| 834x1112 pt (1668x2224 px @2x) +iPad Air 9.7-inch| 768x1024 pt (1536x2048 px @2x) +iPad 11-inch| 820x1180 pt (1640x2360 px @2x) +iPad 10.2-inch| 810x1080 pt (1620x2160 px @2x) +iPad 9.7-inch| 768x1024 pt (1536x2048 px @2x) +iPad mini 8.3-inch| 744x1133 pt (1488x2266 px @2x) +iPad mini 7.9-inch| 768x1024 pt (1536x2048 px @2x) +iPhone 17 Pro Max| 440x956 pt (1320x2868 px @3x) +iPhone 17 Pro| 402x874 pt (1206x2622 px @3x) +iPhone Air| 420x912 pt (1260x2736 px @3x) +iPhone 17| 402x874 pt (1206x2622 px @3x) +iPhone 16 Pro Max| 440x956 pt (1320x2868 px @3x) +iPhone 16 Pro| 402x874 pt (1206x2622 px @3x) +iPhone 16 Plus| 430x932 pt (1290x2796 px @3x) +iPhone 16| 393x852 pt (1179x2556 px @3x) +iPhone 16e| 390x844 pt (1170x2532 px @3x) +iPhone 15 Pro Max| 430x932 pt (1290x2796 px @3x) +iPhone 15 Pro| 393x852 pt (1179x2556 px @3x) +iPhone 15 Plus| 430x932 pt (1290x2796 px @3x) +iPhone 15| 393x852 pt (1179x2556 px @3x) +iPhone 14 Pro Max| 430x932 pt (1290x2796 px @3x) +iPhone 14 Pro| 393x852 pt (1179x2556 px @3x) +iPhone 14 Plus| 428x926 pt (1284x2778 px @3x) +iPhone 14| 390x844 pt (1170x2532 px @3x) +iPhone 13 Pro Max| 428x926 pt (1284x2778 px @3x) +iPhone 13 Pro| 390x844 pt (1170x2532 px @3x) +iPhone 13| 390x844 pt (1170x2532 px @3x) +iPhone 13 mini| 375x812 pt (1125x2436 px @3x) +iPhone 12 Pro Max| 428x926 pt (1284x2778 px @3x) +iPhone 12 Pro| 390x844 pt (1170x2532 px @3x) +iPhone 12| 390x844 pt (1170x2532 px @3x) +iPhone 12 mini| 375x812 pt (1125x2436 px @3x) +iPhone 11 Pro Max| 414x896 pt (1242x2688 px @3x) +iPhone 11 Pro| 375x812 pt (1125x2436 px @3x) +iPhone 11| 414x896 pt (828x1792 px @2x) +iPhone XS Max| 414x896 pt (1242x2688 px @3x) +iPhone XS| 375x812 pt (1125x2436 px @3x) +iPhone XR| 414x896 pt (828x1792 px @2x) +iPhone X| 375x812 pt (1125x2436 px @3x) +iPhone 8 Plus| 414x736 pt (1080x1920 px @3x) +iPhone 8| 375x667 pt (750x1334 px @2x) +iPhone 7 Plus| 414x736 pt (1080x1920 px @3x) +iPhone 7| 375x667 pt (750x1334 px @2x) +iPhone 6s Plus| 414x736 pt (1080x1920 px @3x) +iPhone 6s| 375x667 pt (750x1334 px @2x) +iPhone 6 Plus| 414x736 pt (1080x1920 px @3x) +iPhone 6| 375x667 pt (750x1334 px @2x) +iPhone SE 4.7-inch| 375x667 pt (750x1334 px @2x) +iPhone SE 4-inch| 320x568 pt (640x1136 px @2x) +iPod touch 5th generation and later| 320x568 pt (640x1136 px @2x) + +Note + +All scale factors in the table above are UIKit scale factors, which may differ from native scale factors. For developer guidance, see [`scale`](https://developer.apple.com/documentation/UIKit/UIScreen/scale) and [`nativeScale`](https://developer.apple.com/documentation/UIKit/UIScreen/nativeScale). + +### [iOS, iPadOS device size classes](https://developer.apple.com/design/human-interface-guidelines/layout#iOS-iPadOS-device-size-classes) + +A size class is a value thatโ€™s either regular or compact, where _regular_ refers to a larger screen or a screen in landscape orientation and _compact_ refers to a smaller screen or a screen in portrait orientation. For developer guidance, see [`UserInterfaceSizeClass`](https://developer.apple.com/documentation/SwiftUI/UserInterfaceSizeClass). + +Different size class combinations apply to the full-screen experience on different devices, based on screen size. + +Model| Portrait orientation| Landscape orientation +---|---|--- +iPad Pro 12.9-inch| Regular width, regular height| Regular width, regular height +iPad Pro 11-inch| Regular width, regular height| Regular width, regular height +iPad Pro 10.5-inch| Regular width, regular height| Regular width, regular height +iPad Air 13-inch| Regular width, regular height| Regular width, regular height +iPad Air 11-inch| Regular width, regular height| Regular width, regular height +iPad 11-inch| Regular width, regular height| Regular width, regular height +iPad 9.7-inch| Regular width, regular height| Regular width, regular height +iPad mini 7.9-inch| Regular width, regular height| Regular width, regular height +iPhone 17 Pro Max| Compact width, regular height| Regular width, compact height +iPhone 17 Pro| Compact width, regular height| Compact width, compact height +iPhone Air| Compact width, regular height| Regular width, compact height +iPhone 17| Compact width, regular height| Compact width, compact height +iPhone 16 Pro Max| Compact width, regular height| Regular width, compact height +iPhone 16 Pro| Compact width, regular height| Compact width, compact height +iPhone 16 Plus| Compact width, regular height| Regular width, compact height +iPhone 16| Compact width, regular height| Compact width, compact height +iPhone 16e| Compact width, regular height| Compact width, compact height +iPhone 15 Pro Max| Compact width, regular height| Regular width, compact height +iPhone 15 Pro| Compact width, regular height| Compact width, compact height +iPhone 15 Plus| Compact width, regular height| Regular width, compact height +iPhone 15| Compact width, regular height| Compact width, compact height +iPhone 14 Pro Max| Compact width, regular height| Regular width, compact height +iPhone 14 Pro| Compact width, regular height| Compact width, compact height +iPhone 14 Plus| Compact width, regular height| Regular width, compact height +iPhone 14| Compact width, regular height| Compact width, compact height +iPhone 13 Pro Max| Compact width, regular height| Regular width, compact height +iPhone 13 Pro| Compact width, regular height| Compact width, compact height +iPhone 13| Compact width, regular height| Compact width, compact height +iPhone 13 mini| Compact width, regular height| Compact width, compact height +iPhone 12 Pro Max| Compact width, regular height| Regular width, compact height +iPhone 12 Pro| Compact width, regular height| Compact width, compact height +iPhone 12| Compact width, regular height| Compact width, compact height +iPhone 12 mini| Compact width, regular height| Compact width, compact height +iPhone 11 Pro Max| Compact width, regular height| Regular width, compact height +iPhone 11 Pro| Compact width, regular height| Compact width, compact height +iPhone 11| Compact width, regular height| Regular width, compact height +iPhone XS Max| Compact width, regular height| Regular width, compact height +iPhone XS| Compact width, regular height| Compact width, compact height +iPhone XR| Compact width, regular height| Regular width, compact height +iPhone X| Compact width, regular height| Compact width, compact height +iPhone 8 Plus| Compact width, regular height| Regular width, compact height +iPhone 8| Compact width, regular height| Compact width, compact height +iPhone 7 Plus| Compact width, regular height| Regular width, compact height +iPhone 7| Compact width, regular height| Compact width, compact height +iPhone 6s Plus| Compact width, regular height| Regular width, compact height +iPhone 6s| Compact width, regular height| Compact width, compact height +iPhone SE| Compact width, regular height| Compact width, compact height +iPod touch 5th generation and later| Compact width, regular height| Compact width, compact height + +### [watchOS device screen dimensions](https://developer.apple.com/design/human-interface-guidelines/layout#watchOS-device-screen-dimensions) + +Series| Size| Width (pixels)| Height (pixels) +---|---|---|--- +Apple Watch Ultra (3rd generation)| 49mm| 422| 514 +10, 11| 42mm| 374| 446 +10, 11| 46mm| 416| 496 +Apple Watch Ultra (1st and 2nd generations)| 49mm| 410| 502 +7, 8, and 9| 41mm| 352| 430 +7, 8, and 9| 45mm| 396| 484 +4, 5, 6, and SE (all generations)| 40mm| 324| 394 +4, 5, 6, and SE (all generations)| 44mm| 368| 448 +1, 2, and 3| 38mm| 272| 340 +1, 2, and 3| 42mm| 312| 390 + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/layout#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/layout#Related) + +[Right to left](https://developer.apple.com/design/human-interface-guidelines/right-to-left) + +[Spatial layout](https://developer.apple.com/design/human-interface-guidelines/spatial-layout) + +[Layout and organization](https://developer.apple.com/design/human-interface-guidelines/layout-and-organization) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/layout#Developer-documentation) + +[Composing custom layouts with SwiftUI](https://developer.apple.com/documentation/SwiftUI/composing-custom-layouts-with-swiftui) โ€” SwiftUI + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/layout#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/1AAA030E-2ECA-47D8-AE09-6D7B72A840F6/10044_wide_250x141_1x.jpg) Get to know the new design system ](https://developer.apple.com/videos/play/wwdc2025/356) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/124/1E740BE0-AF55-430B-B8C2-346CA2982476/6549_wide_250x141_1x.jpg) Compose custom layouts with SwiftUI ](https://developer.apple.com/videos/play/wwdc2022/10056) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/7/2546ECBD-6443-41EC-921D-6429026F8B67/1700_wide_250x141_1x.jpg) Essential Design Principles ](https://developer.apple.com/videos/play/wwdc2017/802) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/layout#Change-log) + +Date| Changes +---|--- +September 9, 2025| Added specifications for iPhone 17, iPhone Air, iPhone 17 Pro, iPhone 17 Pro Max, Apple Watch SE 3, Apple Watch Series 11, and Apple Watch Ultra 3. +June 9, 2025| Added guidance for Liquid Glass. +March 7, 2025| Added specifications for iPhone 16e, iPad 11-inch, iPad Air 11-inch, and iPad Air 13-inch. +September 9, 2024| Added specifications for iPhone 16, iPhone 16 Plus, iPhone 16 Pro, iPhone 16 Pro Max, and Apple Watch Series 10. +June 10, 2024| Made minor corrections and organizational updates. +February 2, 2024| Enhanced guidance for avoiding system controls in iPadOS app layouts, and added specifications for 10.9-inch iPad Air and 8.3-inch iPad mini. +December 5, 2023| Clarified guidance on centering content in a visionOS window. +September 15, 2023| Added specifications for iPhone 15 Pro Max, iPhone 15 Pro, iPhone 15 Plus, iPhone 15, Apple Watch Ultra 2, and Apple Watch SE. +June 21, 2023| Updated to include guidance for visionOS. +September 14, 2022| Added specifications for iPhone 14 Pro Max, iPhone 14 Pro, iPhone 14 Plus, iPhone 14, and Apple Watch Ultra. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/materials.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/materials.md new file mode 100644 index 00000000..0452bf57 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/materials.md @@ -0,0 +1,238 @@ +--- +title: "Materials | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/materials + +# Materials + +A material is a visual effect that creates a sense of depth, layering, and hierarchy between foreground and background elements. + +![A sketch of overlapping squares, suggesting the use of transparency to hint at background content. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/7dbd8b65138bed71acdeb36135193681/foundations-materials-intro%402x.png) + +Materials help visually separate foreground elements, such as text and controls, from background elements, such as content and solid colors. By allowing color to pass through from background to foreground, a material establishes visual hierarchy to help people more easily retain a sense of place. + +Apple platforms feature two types of materials: Liquid Glass, and standard materials. [Liquid Glass](https://developer.apple.com/design/human-interface-guidelines/materials#Liquid-Glass) is a dynamic material that unifies the design language across Apple platforms, allowing you to present controls and navigation without obscuring underlying content. In contrast to Liquid Glass, the [standard materials](https://developer.apple.com/design/human-interface-guidelines/materials#Standard-materials) help with visual differentiation within the content layer. + +## [Liquid Glass](https://developer.apple.com/design/human-interface-guidelines/materials#Liquid-Glass) + +Liquid Glass forms a distinct functional layer for controls and navigation elements โ€” like tab bars and sidebars โ€” that floats above the content layer, establishing a clear visual hierarchy between functional elements and content. Liquid Glass allows content to scroll and peek through from beneath these elements to give the interface a sense of dynamism and depth, all while maintaining legibility for controls and navigation. + +**Donโ€™t use Liquid Glass in the content layer.** Liquid Glass works best when it provides a clear distinction between interactive elements and content, and including it in the content layer can result in unnecessary complexity and a confusing visual hierarchy. Instead, use [standard materials](https://developer.apple.com/design/human-interface-guidelines/materials#Standard-materials) for elements in the content layer, such as app backgrounds. An exception to this is for controls in the content layer with a transient interactive element like [sliders](https://developer.apple.com/design/human-interface-guidelines/sliders) and [toggles](https://developer.apple.com/design/human-interface-guidelines/toggles); in these cases, the element takes on a Liquid Glass appearance to emphasize its interactivity when a person activates it. + +**Use Liquid Glass effects sparingly.** Standard components from system frameworks pick up the appearance and behavior of this material automatically. If you apply Liquid Glass effects to a custom control, do so sparingly. Liquid Glass seeks to bring attention to the underlying content, and overusing this material in multiple custom controls can provide a subpar user experience by distracting from that content. Limit these effects to the most important functional elements in your app. For developer guidance, see [Applying Liquid Glass to custom views](https://developer.apple.com/documentation/SwiftUI/Applying-Liquid-Glass-to-custom-views). + +**Only use clear Liquid Glass for components that appear over visually rich backgrounds.** Liquid Glass provides two variants โ€” [`regular`](https://developer.apple.com/documentation/SwiftUI/Glass/regular) and [`clear`](https://developer.apple.com/documentation/SwiftUI/Glass/clear) โ€” that you can choose when building custom components or styling some system components. The appearance of these variants can differ in response to certain system settings, like if people choose a preferred look for Liquid Glass in their deviceโ€™s display settings, or turn on accessibility settings that reduce transparency or increase contrast in the interface. + +The _regular_ variant blurs and adjusts the luminosity of background content to maintain legibility of text and other foreground elements. Scroll edge effects further enhance legibility by blurring and reducing the opacity of background content. Most system components use this variant. Use the regular variant when background content might create legibility issues, or when components have a significant amount of text, such as alerts, sidebars, or popovers. + +![A visual example of the regular variant of Liquid Glass, which appears darker when there is a dark background beneath it.](https://docs-assets.developer.apple.com/published/91bd48556358ab3deb6720c982aa8503/materials-ios-liquid-glass-regular-on-dark%402x.png)On dark background + +![A visual example of the regular variant of Liquid Glass, which appears lighter when there is a light background beneath it.](https://docs-assets.developer.apple.com/published/07aee30876315c8b2985a59a3ac1df31/materials-ios-liquid-glass-regular-on-light%402x.png)On light background + +The _clear_ variant is highly translucent, which is ideal for prioritizing the visibility of the underlying content and ensuring visually rich background elements remain prominent. Use this variant for components that float above media backgrounds โ€” such as photos and videos โ€” to create a more immersive content experience. + +![A visual example of the clear variant of Liquid Glass, which allows the visual detail of the background beneath it to show through.](https://docs-assets.developer.apple.com/published/fe0cd9171626ada19f9ea7343f60a426/materials-ios-liquid-glass-clear%402x.png) + +For optimal contrast and legibility, determine whether to add a dimming layer behind components with clear Liquid Glass: + + * If the underlying content is bright, consider adding a dark dimming layer of 35% opacity. For developer guidance, see [`clear`](https://developer.apple.com/documentation/SwiftUI/Glass/clear). + + * If the underlying content is sufficiently dark, or if you use standard media playback controls from AVKit that provide their own dimming layer, you donโ€™t need to apply a dimming layer. + + + + +For guidance about the use of color, see [Liquid Glass color](https://developer.apple.com/design/human-interface-guidelines/color#Liquid-Glass-color). + +## [Standard materials](https://developer.apple.com/design/human-interface-guidelines/materials#Standard-materials) + +Use standard materials and effects โ€” such as [blur](https://developer.apple.com/documentation/UIKit/UIBlurEffect), [vibrancy](https://developer.apple.com/documentation/UIKit/UIVibrancyEffect), and [blending modes](https://developer.apple.com/documentation/AppKit/NSVisualEffectView/BlendingMode-swift.enum) โ€” to convey a sense of structure in the content beneath Liquid Glass. + +**Choose materials and effects based on semantic meaning and recommended usage.** Avoid selecting a material or effect based on the apparent color it imparts to your interface, because system settings can change its appearance and behavior. Instead, match the material or vibrancy style to your specific use case. + +**Help ensure legibility by using vibrant colors on top of materials.** When you use system-defined vibrant colors, you donโ€™t need to worry about colors seeming too dark, bright, saturated, or low contrast in different contexts. Regardless of the material you choose, use vibrant colors on top of it. For guidance, see [System colors](https://developer.apple.com/design/human-interface-guidelines/color#System-colors). + +![An illustration of a Share button with a translucent background material and a symbol. The symbol uses the systemGray3 color and is difficult to see against the background material.](https://docs-assets.developer.apple.com/published/8a395765f911660a5e16b3bdb30ddd2f/materials-legibility-non-vibrant-label%402x.png)Poor contrast between the material and `systemGray3` label + +![An X in a circle to indicate incorrect usage](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration of a Share button with a translucent background material and a symbol. The symbol uses vibrant color and is clearly visible against the background material.](https://docs-assets.developer.apple.com/published/7495cfbce7d79a1f5635ea2a729dfc24/materials-legibility-primary-label%402x.png)Good contrast between the material and vibrant color label + +![A checkmark in a circle to indicate correct usage](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +**Consider contrast and visual separation when choosing a material to combine with blur and vibrancy effects.** For example, consider that: + + * Thicker materials, which are more opaque, can provide better contrast for text and other elements with fine features. + + * Thinner materials, which are more translucent, can help people retain their context by providing a visible reminder of the content thatโ€™s in the background. + + + + +For developer guidance, see [`Material`](https://developer.apple.com/documentation/SwiftUI/Material). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/materials#Platform-considerations) + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/materials#iOS-iPadOS) + +In addition to Liquid Glass, iOS and iPadOS continue to provide four standard materials โ€” ultra-thin, thin, regular (default), and thick โ€” which you can use in the content layer to help create visual distinction. + +![An illustration of the iOS and iPadOS ultraThin material above a colorful background. Where the material overlaps the background, it provides a diffuse gradient of the background colors.](https://docs-assets.developer.apple.com/published/2ad0598be0bf67fb23e479f102e16b59/materials-ios-material-background-ultrathin%402x.png)`ultraThin` + +![An illustration of the iOS and iPadOS thin material above a colorful background. Where the material overlaps the background, it provides a diffuse and slightly darkened gradient of the background colors.](https://docs-assets.developer.apple.com/published/d298de701d98a146b1436fdf21d0b7ce/materials-ios-material-background-thin%402x.png)`thin` + +![An illustration of the iOS and iPadOS regular material above a colorful background. Where the material overlaps the background, it provides a diffuse and darkened gradient of the background colors.](https://docs-assets.developer.apple.com/published/93a77ac4cfc0786664563a0691498b05/materials-ios-material-background-regular%402x.png)`regular` + +![An illustration of the iOS and iPadOS thick material above a colorful background. Where the material overlaps the background, it provides a dark, muted gradient of the background colors.](https://docs-assets.developer.apple.com/published/2532ddf965d0effa12f528ac10b5a0b3/materials-ios-material-background-thick%402x.png)`thick` + +iOS and iPadOS also define vibrant colors for labels, fills, and separators that are specifically designed to work with each material. Labels and fills both have several levels of vibrancy; separators have one level. The name of a level indicates the relative amount of contrast between an element and the background: The default level has the highest contrast, whereas quaternary (when it exists) has the lowest contrast. + +Except for quaternary, you can use the following vibrancy values for labels on any material. In general, avoid using quaternary on top of the [`thin`](https://developer.apple.com/documentation/SwiftUI/Material/thin) and [`ultraThin`](https://developer.apple.com/documentation/SwiftUI/Material/ultraThin) materials, because the contrast is too low. + + * [`UIVibrancyEffectStyle.label`](https://developer.apple.com/documentation/UIKit/UIVibrancyEffectStyle/label) (default) + + * [`UIVibrancyEffectStyle.secondaryLabel`](https://developer.apple.com/documentation/UIKit/UIVibrancyEffectStyle/secondaryLabel) + + * [`UIVibrancyEffectStyle.tertiaryLabel`](https://developer.apple.com/documentation/UIKit/UIVibrancyEffectStyle/tertiaryLabel) + + * [`UIVibrancyEffectStyle.quaternaryLabel`](https://developer.apple.com/documentation/UIKit/UIVibrancyEffectStyle/quaternaryLabel) + + + + +You can use the following vibrancy values for fills on all materials. + + * [`UIVibrancyEffectStyle.fill`](https://developer.apple.com/documentation/UIKit/UIVibrancyEffectStyle/fill) (default) + + * [`UIVibrancyEffectStyle.secondaryFill`](https://developer.apple.com/documentation/UIKit/UIVibrancyEffectStyle/secondaryFill) + + * [`UIVibrancyEffectStyle.tertiaryFill`](https://developer.apple.com/documentation/UIKit/UIVibrancyEffectStyle/tertiaryFill) + + + + +The system provides a single, default vibrancy value for a [separator](https://developer.apple.com/documentation/UIKit/UIVibrancyEffectStyle/separator), which works well on all materials. + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/materials#macOS) + +macOS provides several standard materials with designated purposes, and vibrant versions of all [system colors](https://developer.apple.com/design/human-interface-guidelines/color#Specifications). For developer guidance, see [`NSVisualEffectView.Material`](https://developer.apple.com/documentation/AppKit/NSVisualEffectView/Material-swift.enum). + +**Choose when to allow vibrancy in custom views and controls.** Depending on configuration and system settings, system views and controls use vibrancy to make foreground content stand out against any background. Test your interface in a variety of contexts to discover when vibrancy enhances the appearance and improves communication. + +**Choose a background blending mode that complements your interface design.** macOS defines two modes that blend background content: behind window and within window. For developer guidance, see [`NSVisualEffectView.BlendingMode`](https://developer.apple.com/documentation/AppKit/NSVisualEffectView/BlendingMode-swift.enum). + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/materials#tvOS) + +In tvOS, Liquid Glass appears throughout navigation elements and system experiences such as Top Shelf and Control Center. Certain interface elements, like image views and buttons, adopt Liquid Glass when they gain focus. + +![A screenshot of the Destination Video app running in tvOS. The app shows a screen with details about a video called A BOT-anist Adventure. The background is a colorful image of the main character in a scene from the video. The interface elements floating above the background adopt a Liquid Glass appearance to allow background color to show through and create a more immersive media experience.](https://docs-assets.developer.apple.com/published/fd83bb7f079cac7b59cb692d8e1c6707/materials-tvos-media-player%402x.png) + +In addition to Liquid Glass, tvOS continues to provide standard materials, which you can use to help define structure in the content layer. The thickness of a standard material affects how prominently the underlying content shows through. For example, consider using standard materials in the following ways: + +Material| Recommended for +---|--- +[`ultraThin`](https://developer.apple.com/documentation/SwiftUI/Material/ultraThin)| Full-screen views that require a light color scheme +[`thin`](https://developer.apple.com/documentation/SwiftUI/Material/thin)| Overlay views that partially obscure onscreen content and require a light color scheme +[`regular`](https://developer.apple.com/documentation/SwiftUI/Material/regular)| Overlay views that partially obscure onscreen content +[`thick`](https://developer.apple.com/documentation/SwiftUI/Material/thick)| Overlay views that partially obscure onscreen content and require a dark color scheme + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/materials#visionOS) + +In visionOS, windows generally use an unmodifiable system-defined material called _glass_ that helps people stay grounded by letting light, the current Environment, virtual content, and objects in peopleโ€™s surroundings show through. Glass is an adaptive material that limits the range of background color information so a window can continue to provide contrast for app content while becoming brighter or darker depending on peopleโ€™s physical surroundings and other virtual content. + +Video with custom controls. + +Content description: A recording of the Music app window in visionOS. The window uses the glass material and adapts as the viewing angle and lighting change. + +Play + +Note + +visionOS doesnโ€™t have a distinct Dark Mode setting. Instead, glass automatically adapts to the luminance of the objects and colors behind it. + +**Prefer translucency to opaque colors in windows.** Areas of opacity can block peopleโ€™s view, making them feel constricted and reducing their awareness of the virtual and physical objects around them. + +![An illustration of a field of view in visionOS with a window in the center. The window has an opaque background that obstructs its surroundings.](https://docs-assets.developer.apple.com/published/137ceb38a96227aa8a9d2021ee82a8e2/materials-visionos-opaque-window-incorrect%402x.png) + +![An X in a circle to indicate incorrect usage](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration of a field of view in visionOS with a window in the center. The window has a translucent material background that allows its surroundings to pass through.](https://docs-assets.developer.apple.com/published/3f23b3476f6cf8cc77fdcb91a0c15063/materials-visionos-glass-window%402x.png) + +![A checkmark in a circle to indicate correct usage](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +**If necessary, choose materials that help you create visual separations or indicate interactivity in your app.** If you need to create a custom component, you may need to specify a system material for it. Use the following examples for guidance. + + * The [`thin`](https://developer.apple.com/documentation/SwiftUI/Material/thin) material brings attention to interactive elements like buttons and selected items. + + * The [`regular`](https://developer.apple.com/documentation/SwiftUI/Material/regular) material can help you visually separate sections of your app, like a sidebar or a grouped table view. + + * The [`thick`](https://developer.apple.com/documentation/SwiftUI/Material/thick) material lets you create a dark element that remains visually distinct when itโ€™s on top of an area that uses a `regular` background. + + + + +![An illustration of a field of view in visionOS with a window in the center. The window is composed of a sidebar on the left and a content area on the right, with a text field at the top and a button in the lower-right corner. The sidebar uses regular material, while the text field uses thick material and the button uses thin material.](https://docs-assets.developer.apple.com/published/c3577aa1e00689431e49973173a151f9/visionos-materials-window-example%402x.png) + +To ensure foreground content remains legible when it displays on top of a material, visionOS applies vibrancy to text, symbols, and fills. Vibrancy enhances the sense of depth by pulling light and color forward from both virtual and physical surroundings. + +visionOS defines three vibrancy values that help you communicate a hierarchy of text, symbols, and fills. + + * Use [`UIVibrancyEffectStyle.label`](https://developer.apple.com/documentation/UIKit/UIVibrancyEffectStyle/label) for standard text. + + * Use [`UIVibrancyEffectStyle.secondaryLabel`](https://developer.apple.com/documentation/UIKit/UIVibrancyEffectStyle/secondaryLabel) for descriptive text like footnotes and subtitles. + + * Use [`UIVibrancyEffectStyle.tertiaryLabel`](https://developer.apple.com/documentation/UIKit/UIVibrancyEffectStyle/tertiaryLabel) for inactive elements, and only when text doesnโ€™t need high legibility. + + + + +![An illustration of a Share button with a translucent background material and a symbol. The symbol uses the default vibrant label color and has very high contrast against the background material.](https://docs-assets.developer.apple.com/published/8f850521ecc2e3953e8e693fe7b4887b/materials-visionos-label-vibrant-primary%402x.png)`label` + +![An illustration of a Share button with a translucent background material and a symbol. The symbol uses the secondary vibrant label color and has high contrast against the background material.](https://docs-assets.developer.apple.com/published/876503f2b2b5fd1783e359128ffd2482/materials-visionos-label-vibrant-secondary%402x.png)`secondaryLabel` + +![An illustration of a Share button with a translucent background material and a symbol. The symbol uses the tertiary vibrant label color and has muted contrast against the background material.](https://docs-assets.developer.apple.com/published/b3b80e5f23b286f6c7897780676e6dfe/materials-visionos-label-vibrant-tertiary%402x.png)`tertiaryLabel` + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/materials#watchOS) + +**Use materials to provide context in a full-screen modal view.** Because full-screen modal views are common in watchOS, the contrast provided by material layers can help orient people in your app and distinguish controls and system elements from other content. Avoid removing or replacing material backgrounds for modal sheets when theyโ€™re provided by default. + +![An illustration of a modal view in watchOS with an example title, descriptive text, and a single action button. The modal completely covers the screen with a transparent material, and uses a thinner material for the button along with vibrant label text.](https://docs-assets.developer.apple.com/published/b9bdbaa947d461e98681c9fbb87a7052/watchos-modal-view-material-background%402x.png) + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/materials#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/materials#Related) + +[Color](https://developer.apple.com/design/human-interface-guidelines/color) + +[Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility) + +[Dark Mode](https://developer.apple.com/design/human-interface-guidelines/dark-mode) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/materials#Developer-documentation) + +[Adopting Liquid Glass](https://developer.apple.com/documentation/TechnologyOverviews/adopting-liquid-glass) + +[`glassEffect(_:in:)`](https://developer.apple.com/documentation/SwiftUI/View/glassEffect\(_:in:\)) โ€” SwiftUI + +[`Material`](https://developer.apple.com/documentation/SwiftUI/Material) โ€” SwiftUI + +[`UIVisualEffectView`](https://developer.apple.com/documentation/UIKit/UIVisualEffectView) โ€” UIKit + +[`NSVisualEffectView`](https://developer.apple.com/documentation/AppKit/NSVisualEffectView) โ€” AppKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/materials#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/5CD0E251-424E-490F-89CF-1E64848209A6/9910_wide_250x141_1x.jpg) Meet Liquid Glass ](https://developer.apple.com/videos/play/wwdc2025/219) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/1AAA030E-2ECA-47D8-AE09-6D7B72A840F6/10044_wide_250x141_1x.jpg) Get to know the new design system ](https://developer.apple.com/videos/play/wwdc2025/356) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/materials#Change-log) + +Date| Changes +---|--- +September 9, 2025| Updated guidance for Liquid Glass. +June 9, 2025| Added guidance for Liquid Glass. +August 6, 2024| Added platform-specific art. +December 5, 2023| Updated descriptions of the various material types, and clarified terms related to vibrancy and material thickness. +June 21, 2023| Updated to include guidance for visionOS. +June 5, 2023| Added guidance on using materials to provide context and orientation in watchOS apps. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/motion.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/motion.md new file mode 100644 index 00000000..262e739c --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/motion.md @@ -0,0 +1,103 @@ +--- +title: "Motion | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/motion + +# Motion + +Beautiful, fluid motions bring the interface to life, conveying status, providing feedback and instruction, and enriching the visual experience of your app or game. + +![A sketch of three overlapping diamonds, suggesting the movement of an element from left to right. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/1a0efd7807cfcba7a5821be86b20bafc/foundations-motion-intro%402x.png) + +Many system components automatically include motion, letting you offer familiar and consistent experiences throughout your app or game. System components might also adjust their motion in response to factors like accessibility settings or different input methods. For example, the movement of [Liquid Glass](https://developer.apple.com/design/human-interface-guidelines/materials#Liquid-Glass) responds to direct touch interaction with greater emphasis to reinforce the feeling of a tactile experience, but produces a more subdued effect when a person interacts using a trackpad. + +If you design custom motion, follow the guidelines below. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/motion#Best-practices) + +**Add motion purposefully, supporting the experience without overshadowing it.** Donโ€™t add motion for the sake of adding motion. Gratuitous or excessive animation can distract people and may make them feel disconnected or physically uncomfortable. + +**Make motion optional.** Not everyone can or wants to experience the motion in your app or game, so itโ€™s essential to avoid using it as the only way to communicate important information. To help everyone enjoy your app or game, supplement visual feedback by also using alternatives like [haptics](https://developer.apple.com/design/human-interface-guidelines/playing-haptics) and [audio](https://developer.apple.com/design/human-interface-guidelines/playing-audio) to communicate. + +## [Providing feedback](https://developer.apple.com/design/human-interface-guidelines/motion#Providing-feedback) + +**Strive for realistic feedback motion that follows peopleโ€™s gestures and expectations.** In nongame apps, accurate, realistic motion can help people understand how something works, but feedback motion that doesnโ€™t make sense can make them feel disoriented. For example, if someone reveals a view by sliding it down from the top, they donโ€™t expect to dismiss the view by sliding it to the side. + +**Aim for brevity and precision in feedback animations.** When animated feedback is brief and precise, it tends to feel lightweight and unobtrusive, and it can often convey information more effectively than prominent animation. For example, when a game displays a succinct animation thatโ€™s precisely tied to a successful action, players can instantly get the message without being distracted from their gameplay. Another example is in visionOS: When people tap a panorama in Photos, it quickly and smoothly expands to fill the space in front of them, helping them track the transition without making them wait to enjoy the content. + +**In apps, generally avoid adding motion to UI interactions that occur frequently.** The system already provides subtle animations for interactions with standard interface elements. For a custom element, you generally want to avoid making people spend extra time paying attention to unnecessary motion every time they interact with it. + +**Let people cancel motion.** As much as possible, donโ€™t make people wait for an animation to complete before they can do anything, especially if they have to experience the animation more than once. + +**Consider using animated symbols where it makes sense.** When you use SF Symbols 5 or later, you can apply animations to SF Symbols or custom symbols. For guidance, see [Animations](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Animations). + +## [Leveraging platform capabilities](https://developer.apple.com/design/human-interface-guidelines/motion#Leveraging-platform-capabilities) + +**Make sure your gameโ€™s motion looks great by default on each platform you support.** In most games, maintaining a consistent frame rate of 30 to 60 fps typically results in a smooth, visually appealing experience. For each platform you support, use the deviceโ€™s graphics capabilities to enable default settings that let people enjoy your game without first having to change those settings. + +**Let people customize the visual experience of your game to optimize performance or battery life.** For example, consider letting people switch between power modes when the system detects the presence of an external power source. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/motion#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, or tvOS._ + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/motion#visionOS) + +In addition to subtly communicating context, drawing attention to information, and enriching immersive experiences, motion in visionOS can combine with [depth](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Depth) to provide essential feedback when people look at interactive elements. Because motion is likely to be a large part of your visionOS experience, itโ€™s crucial to avoid causing distraction, confusion, or discomfort. + +**As much as possible, avoid displaying motion at the edges of a personโ€™s field of view.** People can be particularly sensitive to motion that occurs in their peripheral vision: in addition to being distracting, such motion can even cause discomfort because it can make people feel like they or their surroundings are moving. If you need to show an object moving in the periphery during an immersive experience, make sure the objectโ€™s brightness level is similar to the rest of the visible content. + +**Help people remain comfortable when showing the movement of large virtual objects.** If an object is large enough to fill a lot of the [field of view](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Field-of-view), occluding most or all of [passthrough](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences#Immersion-and-passthrough), people can naturally perceive it as being part of their surroundings. To help people perceive the objectโ€™s movement without making them think that they or their surroundings are moving, you can increase the objectโ€™s translucency, helping people see through it, or lower its contrast to make its motion less noticeable. + +Note + +People can experience discomfort even when theyโ€™re the ones moving a large virtual object, such as a window. Although adjusting translucency and contrast can help in this scenario, consider also keeping a windowโ€™s size fairly small. + +**Consider using fades when you need to relocate an object.** When an object moves from one location to another, people naturally watch the movement. If such movement doesnโ€™t communicate anything useful to people, you can fade the object out before moving it and fade it back in after itโ€™s in the new location. + +**In general, avoid letting people rotate a virtual world.** When a virtual world rotates, the experience typically upsets peopleโ€™s sense of stability, even when they control the rotation and the movement is subtle. Instead, consider using instantaneous directional changes during a quick fade-out. + +**Consider giving people a stationary frame of reference.** It can be easier for people to handle visual movement when itโ€™s contained within an area that doesnโ€™t move. In contrast, if the entire surrounding area appears to move โ€” for example, in a game that automatically moves a player through space โ€” people can feel unwell. + +**Avoid showing objects that oscillate in a sustained way.** In particular, you want to avoid showing an oscillation that has a frequency of around 0.2 Hz because people can be very sensitive to this frequency. If you need to show objects oscillating, aim to keep the amplitude low and consider making the content translucent. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/motion#watchOS) + +SwiftUI provides a powerful and streamlined way to add motion to your app. If you need to use WatchKit to animate layout and appearance changes โ€” or create animated image sequences โ€” see [`WKInterfaceImage`](https://developer.apple.com/documentation/WatchKit/WKInterfaceImage#1652345). + +Note + +All layout- and appearance-based animations automatically include built-in easing that plays at the start and end of the animation. You canโ€™t turn off or customize easing. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/motion#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/motion#Related) + +[Feedback](https://developer.apple.com/design/human-interface-guidelines/feedback) + +[Accessibility](https://www.apple.com/accessibility/) + +[Spatial layout](https://developer.apple.com/design/human-interface-guidelines/spatial-layout) + +[Immersive experiences](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/motion#Developer-documentation) + +[Animating views and transitions](https://developer.apple.com/tutorials/SwiftUI/animating-views-and-transitions) โ€” SwiftUI + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/motion#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/B38CC217-7635-48EF-B8C9-F7954F390CCE/9273_wide_250x141_1x.jpg) Enhance your UI animations and transitions ](https://developer.apple.com/videos/play/wwdc2024/10145) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/A8C0D750-CF09-48F3-982B-0D1B870F273F/9279_wide_250x141_1x.jpg) Create custom visual effects with SwiftUI ](https://developer.apple.com/videos/play/wwdc2024/10151) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/2C47B638-090D-4CBB-9E9E-EBE8114536D9/8132_wide_250x141_1x.jpg) Design considerations for vision and motion ](https://developer.apple.com/videos/play/wwdc2023/10078) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/motion#Change-log) + +Date| Changes +---|--- +September 9, 2025| Added guidance for Liquid Glass. +June 10, 2024| Added game-specific examples and enhanced guidance for using motion in games. +February 2, 2024| Enhanced guidance for minimizing peripheral motion in visionOS apps. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/privacy.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/privacy.md new file mode 100644 index 00000000..4d0811d6 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/privacy.md @@ -0,0 +1,231 @@ +--- +title: "Privacy | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/privacy + +# Privacy + +Privacy is paramount: itโ€™s critical to be transparent about the privacy-related data and resources you require and essential to protect the data people allow you to access. + +![A sketch of an upright hand, suggesting protection. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/161fec1d77c705ccf076fb4c67d32f5c/foundations-privacy-intro%402x.png) + +People use their devices in very personal ways and they expect apps to help them preserve their privacy. + +When you submit a new or updated app, you must provide details about your privacy practices and the privacy-relevant data you collect so the App Store can display the information on your product page. (You can manage this information at any time in [App Store Connect](https://help.apple.com/app-store-connect/#/dev1b4647c5b).) People use the privacy details on your product page to make an informed decision before they download your app. To learn more, see [App privacy details on the App Store](https://developer.apple.com/app-store/app-privacy-details/). + +![A screenshot of the App Privacy screen in an appโ€™s App Store product page. The top card in the screen is titled Data Used to Track You and lists contact info, other data, and identifiers. The bottom card is titled Data Linked to You and lists health and fitness, financial info, contact info, purchases, location, and contacts.](https://docs-assets.developer.apple.com/published/50727e3a2229fda1e6fa93ca9677cc7f/privacy-social-media-app-store-nutrition-labels%402x.png) + +An appโ€™s App Store product page helps people understand the appโ€™s privacy practices before they download it. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/privacy#Best-practices) + +**Request access only to data that you actually need.** Asking for more data than a feature needs โ€” or asking for data before a person shows interest in the feature โ€” can make it hard for people to trust your app. Give people precise control over their data by making your permission requests as specific as possible. + +**Be transparent about how your app collects and uses peopleโ€™s data.** People are less likely to be comfortable sharing data with your app if they donโ€™t understand exactly how you plan to use it. Always respect peopleโ€™s choices to use system features like Hide My Email and Mail Privacy Protection, and be sure you understand your obligations with regard to app tracking. To learn more about Apple privacy features, see [Privacy](https://www.apple.com/privacy/); for developer guidance, see [User privacy and data use](https://developer.apple.com/app-store/user-privacy-and-data-use/). + +**Process data on the device where possible.** In iOS, for example, you can take advantage of the Apple Neural Engine and custom CreateML models to process the data right on the device, helping you avoid lengthy and potentially risky round trips to a remote server. + +**Adopt system-defined privacy protections and follow security best practices.** For example, in iOS 15 and later, you can rely on CloudKit to provide encryption and key management for additional data types, like strings, numbers, and dates. + +## [Requesting permission](https://developer.apple.com/design/human-interface-guidelines/privacy#Requesting-permission) + +Here are several examples of the things you must request permission to access: + + * Personal data, including location, health, financial, contact, and other personally identifying information + + * User-generated content like emails, messages, calendar data, contacts, gameplay information, Apple Music activity, HomeKit data, and audio, video, and photo content + + * Protected resources like Bluetooth peripherals, home automation features, Wi-Fi connections, and local networks + + * Device capabilities like camera and microphone + + * In a visionOS app running in a Full Space, ARKit data, such as hand tracking, plane estimation, image anchoring, and world tracking + + * The deviceโ€™s advertising identifier, which supports app tracking + + + + +The system provides a standard alert that lets people view each request you make. You supply copy that describes why your app needs access, and the system displays your description in the alert. People can also view the description โ€” and update their choice โ€” in Settings > Privacy. + +**Request permission only when your app clearly needs access to the data or resource.** Itโ€™s natural for people to be suspicious of a request for personal information or access to a device capability, especially if thereโ€™s no obvious need for it. Ideally, wait to request permission until people actually use an app feature that requires access. For example, you can use the [location button](https://developer.apple.com/design/human-interface-guidelines/privacy#Location-button) to give people a way to share their location after they indicate interest in a feature that needs that information. + +**Avoid requesting permission at launch unless the data or resource is required for your app to function.** People are less likely to be bothered by a launch-time request when itโ€™s obvious why youโ€™re making it. For example, people understand that a navigation app needs access to their location before they can benefit from it. Similarly, before people can play a visionOS game that lets them bounce virtual objects off walls in their surroundings, they need to permit the game to access information about their surroundings. + +**Write copy that clearly describes how your app uses the ability, data, or resource youโ€™re requesting.** The standard alert displays your copy (called a _purpose string_ or _usage description string_) after your app name and before the buttons people use to grant or deny their permission. Aim for a brief, complete sentence thatโ€™s straightforward, specific, and easy to understand. Use sentence case, avoid passive voice, and include a period at the end. For developer guidance, see [Requesting access to protected resources](https://developer.apple.com/documentation/UIKit/requesting-access-to-protected-resources) and [App Tracking Transparency](https://developer.apple.com/documentation/AppTrackingTransparency). + +| Example purpose string| Notes +---|---|--- +![A checkmark in a circle to indicate a correct example.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png)| The app records during the night to detect snoring sounds.| An active sentence that clearly describes how and why the app collects the data. +![An X in a circle to indicate an incorrect example.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png)| Microphone access is needed for a better experience.| A passive sentence that provides a vague, undefined justification. +![An X in a circle to indicate an incorrect example.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png)| Turn on microphone access.| An imperative sentence that doesnโ€™t provide any justification. + +Here are several examples of the standard system alert: + + * Example 1 + * Example 2 + * Example 3 + + + +![A screenshot of a permission alert for a social media app displaying a purpose string that reads Allow Social Media to access your location? Turning on location will allow us to show you nearby post locations. Below the string is a small map image containing the Precise On notice and below the map are three buttons in a stack. From the top, the buttons are titled Allow Once, Allow While Using App, and Donโ€™t Allow.](https://docs-assets.developer.apple.com/published/cc8f1498cf0906c5cbba7b0a71fff511/privacy-social-media-post-location-alert%402x.png) + +![A screenshot of a permission alert for a social media app displaying a purpose string that reads Social Media Would Like to Access Your Photos. Allow access to photos to upload photos from your library. The string is followed by three buttons in a stack. From the top, the buttons are titled Select Photos, Allow Access to All Photos, and Donโ€™t Allow.](https://docs-assets.developer.apple.com/published/6143de7f950793edc8d632a54bf5d2bb/privacy-social-media-post-photo-alert%402x.png) + +![A screenshot of a permission alert for a social media app displaying a purpose string that reads Social Media Would Like to Access Your Contacts. Find friends using Social Media and add them to your network. The string is followed by two side-by-side buttons: Donโ€™t Allow and Allow.](https://docs-assets.developer.apple.com/published/9a0f4d978424e52a782b4f1596426415/privacy-social-media-friends-contacts-alert%402x.png) + +### [Pre-alert screens, windows, or views](https://developer.apple.com/design/human-interface-guidelines/privacy#Pre-alert-screens-windows-or-views) + +Ideally, the current context helps people understand why youโ€™re requesting their permission. If itโ€™s essential to provide additional details, you can display a custom screen or window before the system alert appears. The following guidelines apply to custom views that display before system alerts that request permission to access protected data and resources, including camera, microphone, location, contact, calendar, and tracking. + +**Include only one button and make it clear that it opens the system alert.** People can feel manipulated when a custom screen or window also includes a button that doesnโ€™t open the alert because the experience diverts them from making their choice. Another type of manipulation is using a term like โ€œAllowโ€ to title the custom screenโ€™s button. If the custom button seems similar in meaning and visual weight to the allow button in the alert, people can be more likely to choose the alertโ€™s allow button without meaning to. Use a term like โ€œContinueโ€ or โ€œNextโ€ to title the single button in your custom screen or window, clarifying that its action is to open the system alert. + +![A screenshot of an app's pre-alert screen that reads Turning on location services allows us to provide features like: alerts when your friends are nearby, news of events happening near you, tagging and sharing your location. You can change this later in the Settings app. Below the text is a button titled Next.](https://docs-assets.developer.apple.com/published/bda87e1bb5098ab79fee0d0a3be3a10b/privacy-custom-messaging-correct%402x.png) + +![A checkmark in a circle to indicate a correct example.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +**Donโ€™t include additional actions in your custom screen or window.** For example, donโ€™t provide a way for people to leave the screen or window without viewing the system alert โ€” like offering an option to close or cancel. + +![A screenshot of an appโ€™s pre-alert screen that includes a button titled Cancel that appears below the Next button.](https://docs-assets.developer.apple.com/published/56cc76fcd5f87de8dae06080b81358f2/privacy-custom-messaging-incorrect-cancel-button%402x.png) + +![An X in a circle to indicate an incorrect example.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png)Donโ€™t include an option to cancel. + +![A screenshot of an appโ€™s pre-alert screen that includes a Close button in the top-left corner. The Next button appears near the bottom of the screen.](https://docs-assets.developer.apple.com/published/a5cb7d6881eb22e248afd3f806743f67/privacy-custom-messaging-incorrect-close-button%402x.png) + +![An X in a circle to indicate an incorrect example.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png)Donโ€™t include an option to close the view. + +### [Tracking requests](https://developer.apple.com/design/human-interface-guidelines/privacy#Tracking-requests) + +App tracking is a sensitive issue. In some cases, it might make sense to display a custom screen or window that describes the benefits of tracking. If you want to perform app tracking as soon as people launch your app, you must display the system-provided alert before you collect any tracking data. + +**Never precede the system-provided alert with a custom screen or window that could confuse or mislead people.** People sometimes tap quickly to dismiss alerts without reading them. A custom messaging screen, window, or view that takes advantage of such behaviors to influence choices will lead to rejection by App Store review. + +There are several prohibited custom-screen designs that will cause rejection. Some examples are offering incentives, displaying a screen or window that looks like a request, displaying an image of the alert, and annotating the screen behind the alert (as shown below). To learn more, see [App Review Guidelines: 5.1.1 (iv)](https://developer.apple.com/app-store/review/guidelines/#data-collection-and-storage). + + * Incentive + * Imitation request + * Alert image + * Alert annotation + + + +![A screenshot of an appโ€™s pre-tracking message that reads Allow tracking and get a $100 credit toward your next purchase. Below the text is an image of a dollar sign inside a circle. Below the image is a button titled Get $100 credit.](https://docs-assets.developer.apple.com/published/6000f4e89c244b12c8438aec034f7d1b/privacy-custom-messaging-prohibited-incentive%402x.png) + +![An X in a circle to indicate an incorrect example.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png)Donโ€™t offer incentives for granting the request. You canโ€™t offer people compensation for granting their permission, and you canโ€™t withhold functionality or content or make your app unusable until people allow you to track them. + +![A screenshot of an appโ€™s pre-tracking message that reads Allow tracking for a better experience. Below the text is a bar graph image that shows four bars increasing in height from left to right. Below the graph is a button titled Allow Tracking.](https://docs-assets.developer.apple.com/published/f1d292d13b6548e9eb72397e0d3ad760/privacy-custom-messaging-prohibited-imitation%402x.png) + +![An X in a circle to indicate an incorrect example.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png)Donโ€™t display a custom screen that mirrors the functionality of the system alert. In particular, donโ€™t create a button title that uses โ€œAllowโ€ or similar terms, because people donโ€™t allow anything in a pre-alert screen. + +![A screenshot of an appโ€™s pre-tracking message that reads Choose Allow when prompted. Below the text is an image of the system-provided alert. Below the image is a button titled Continue. The Allow While Using the App button in the system-provided alert image is circled.](https://docs-assets.developer.apple.com/published/5ae208fd0806ac0d7e89f9939a93c6e5/privacy-custom-messaging-prohibited-alert%402x.png) + +![An X in a circle to indicate an incorrect example.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png)Donโ€™t show an image of the standard alert and modify it in any way. + +![A screenshot of an appโ€™s pre-tracking message that reads Allow tracking for a better experience. The appโ€™s custom screen also includes an upward-pointing arrow and the words Choose Allow in the lower third of the screen.](https://docs-assets.developer.apple.com/published/780cf726198155101ee7cff6d786669f/privacy-custom-messaging-prohibited-alert-annotation%402x.png) + +![An X in a circle to indicate an incorrect example.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png)Donโ€™t add a visual cue that draws peopleโ€™s attention to the system alertโ€™s Allow buttons. + +## [Location button](https://developer.apple.com/design/human-interface-guidelines/privacy#Location-button) + +In iOS, iPadOS, and watchOS, Core Location provides a button so people can grant your app temporary authorization to access their location at the moment a task needs it. A location buttonโ€™s appearance can vary to match your appโ€™s UI and it always communicates the action of location sharing in a way thatโ€™s instantly recognizable. + +![An image of a lozenge-shaped blue button that displays a white location indicator โ€” that is, a narrow arrow head shape that points to the top right โ€” followed by the text Current Location.](https://docs-assets.developer.apple.com/published/2d4e44adec80170cec96d3446617e700/location-button%402x.png) + +The first time people open your app and tap a location button, the system displays a standard alert. The alert helps people understand how using the button limits your appโ€™s access to their location, and reminds them of the location indicator that appears when sharing starts. + +![A screenshot of the alert displayed by the location button that appears on top of a background image showing a partial map. The alert reads Allow Social Media to access your location? Turning on location will allow us to show you nearby post locations. Below this text the alert displays a small image of the map, zoomed in to show part of Cupertino. Below the map are three buttons; from the top the titles are Allow Once, Allow While Using App, and Don't Allow.](https://docs-assets.developer.apple.com/published/5cff6abb7fc42b749c616ab763a09968/privacy-social-media-map-location-alert%402x.png) + +After people confirm their understanding of the buttonโ€™s action, simply tapping the location button gives your app one-time permission to access their location. Although each one-time authorization expires when people stop using your app, they donโ€™t need to reconfirm their understanding of the buttonโ€™s behavior. + +Note + +If your app has no authorization status, tapping the location button has the same effect as when a person chooses _Allow Once_ in the standard alert. If people previously chose _While Using the App_ , tapping the location button doesnโ€™t change your appโ€™s status. For developer guidance, see [`LocationButton`](https://developer.apple.com/documentation/CoreLocationUI/LocationButton) (SwiftUI) and [`CLLocationButton`](https://developer.apple.com/documentation/CoreLocationUI/CLLocationButton) (Swift). + +**Consider using the location button to give people a lightweight way to share their location for specific app features.** For example, your app might help people attach their location to a message or post, find a store, or identify a building, plant, or animal theyโ€™ve encountered in their location. If you know that people often grant your app _Allow Once_ permission, consider using the location button to help them benefit from sharing their location without having to repeatedly interact with the alert. + +**Consider customizing the location button to harmonize with your UI.** Specifically, you can: + + * Choose the system-provided title that works best with your feature, such as โ€œCurrent Locationโ€ or โ€œShare My Current Location.โ€ + + * Choose the filled or outlined location glyph. + + * Select a background color and a color for the title and glyph. + + * Adjust the buttonโ€™s corner radius. + + + + +To help people recognize and trust location buttons, you canโ€™t customize the buttonโ€™s other visual attributes. The system also ensures a location button remains legible by warning you about problems like low-contrast color combinations or too much translucency. In addition to fixing such problems, youโ€™re responsible for making sure the text fits in the button โ€” for example, button text needs to fit without truncation at all accessibility text sizes and when translated into other languages. + +Important + +If the system identifies consistent problems with your customized location button, it wonโ€™t give your app access to the device location when people tap it. Although such a button can perform other app-specific actions, people may lose trust in your app if your location button doesnโ€™t work as they expect. + +## [Protecting data](https://developer.apple.com/design/human-interface-guidelines/privacy#Protecting-data) + +Protecting peopleโ€™s information is paramount. Give people confidence in your appโ€™s security and help preserve their privacy by taking advantage of system-provided security technologies when you need to store information locally, authorize people for specific operations, and transport information across a network. + +Here are some high-level guidelines. + +**Avoid relying solely on passwords for authentication.** Where possible, use [passkeys](https://developer.apple.com/documentation/authenticationservices/public-private_key_authentication/supporting_passkeys/) to replace passwords. If you need to continue using passwords for authentication, augment security by requiring two-factor authentication (for developer guidance, see [Securing Logins with iCloud Keychain Verification Codes](https://developer.apple.com/documentation/AuthenticationServices/securing-logins-with-icloud-keychain-verification-codes)). To further protect access to apps that people keep logged in on their device, use biometric identification like Face ID, Optic ID, or Touch ID. For developer guidance, see [Local Authentication](https://developer.apple.com/documentation/LocalAuthentication). + +**Store sensitive information in a keychain.** A keychain provides a secure, predictable user experience when handling someoneโ€™s private information. For developer guidance, see [Keychain services](https://developer.apple.com/documentation/Security/keychain-services). + +**Never store passwords or other secure content in plain-text files.** Even if you restrict access using file permissions, sensitive information is much safer in an encrypted keychain. + +**Avoid inventing custom authentication schemes.** If your app requires authentication, prefer system-provided features like [passkeys](https://developer.apple.com/documentation/authenticationservices/public-private_key_authentication/supporting_passkeys/), [Sign in with Apple](https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple) or [Password AutoFill](https://developer.apple.com/documentation/Security/password-autofill). For related guidance, see [Managing accounts](https://developer.apple.com/design/human-interface-guidelines/managing-accounts). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/privacy#Platform-considerations) + + _No additional considerations for iOS, iPadOS, tvOS, or watchOS._ + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/privacy#macOS) + +**Sign your app with a valid Developer ID.** If you choose to distribute your app outside the store, signing your app with Developer ID identifies you as an Apple developer and confirms that your app is safe to use. For developer guidance, see [Xcode Help](https://developer.apple.com/go/?id=ios-app-distribution-guide). + +**Protect peopleโ€™s data with app sandboxing.** Sandboxing provides your app with access to system resources and user data while protecting it from malware. All apps submitted to the Mac App Store require sandboxing. For developer guidance, see [Configuring the macOS App Sandbox](https://developer.apple.com/documentation/Xcode/configuring-the-macos-app-sandbox). + +**Avoid making assumptions about who is signed in.** Because of fast user switching, multiple people may be active on the same system. + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/privacy#visionOS) + +By default, visionOS uses ARKit algorithms to handle features like persistence, world mapping, segmentation, matting, and environment lighting. These algorithms are always running, allowing apps and games to automatically benefit from ARKit while in the Shared Space. + +ARKit doesnโ€™t send data to apps in the Shared Space; to access ARKit APIs, your app must open a Full Space. Additionally, features like Plane Estimation, Scene Reconstruction, Image Anchoring, and Hand Tracking require peopleโ€™s permission to access any information. For developer guidance, see [Setting up access to ARKit data](https://developer.apple.com/documentation/visionOS/setting-up-access-to-arkit-data). + +In visionOS, user input is private by design. The system automatically displays hover effects when people look at interactive components you create using SwiftUI or RealityKit, giving people the visual feedback they need without exposing where theyโ€™re looking before they tap. For guidance, see [Eyes](https://developer.apple.com/design/human-interface-guidelines/eyes) and [Gestures > visionOS](https://developer.apple.com/design/human-interface-guidelines/gestures#visionOS). + +Developer access to device cameras works differently in visionOS than it does in other platforms. Specifically, the back camera provides blank input and is only available as a compatibility convenience; the front camera provides input for [spatial Personas](https://developer.apple.com/design/human-interface-guidelines/shareplay#visionOS), but only after people grant their permission. If the iOS or iPadOS app youโ€™re bringing to visionOS includes a feature that needs camera access, remove it or replace it with an option for people to import content instead. For developer guidance, see [Making your existing app compatible with visionOS](https://developer.apple.com/documentation/visionOS/making-your-app-compatible-with-visionos). + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/privacy#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/privacy#Related) + +[Entering data](https://developer.apple.com/design/human-interface-guidelines/entering-data) + +[Onboarding](https://developer.apple.com/design/human-interface-guidelines/onboarding) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/privacy#Developer-documentation) + +[Requesting access to protected resources](https://developer.apple.com/documentation/UIKit/requesting-access-to-protected-resources) โ€” UIKit + +[Security](https://developer.apple.com/documentation/Security) + +[Requesting authorization to use location services](https://developer.apple.com/documentation/CoreLocation/requesting-authorization-to-use-location-services) โ€” CoreLocation + +[App Tracking Transparency](https://developer.apple.com/documentation/AppTrackingTransparency) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/privacy#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/0A08BD06-2B59-45BA-AA75-C9206946195D/9945_wide_250x141_1x.jpg) Integrate privacy into your development process ](https://developer.apple.com/videos/play/wwdc2025/246) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/473C8E4A-1764-482D-BE24-B3A7BBDBD526/9996_wide_250x141_1x.jpg) Whatโ€™s new in passkeys ](https://developer.apple.com/videos/play/wwdc2025/279) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/39DEAE04-CBAD-401A-973C-3916F2B9624A/9251_wide_250x141_1x.jpg) Whatโ€™s new in privacy ](https://developer.apple.com/videos/play/wwdc2024/10123) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/privacy#Change-log) + +Date| Changes +---|--- +June 21, 2023| Consolidated guidance into new page and updated for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/right-to-left.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/right-to-left.md new file mode 100644 index 00000000..73347391 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/right-to-left.md @@ -0,0 +1,206 @@ +--- +title: "Right to left | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/right-to-left + +# Right to left + +Support right-to-left languages like Arabic and Hebrew by reversing your interface as needed to match the reading direction of the related scripts. + +![A sketch of a right-aligned bulleted list within a window, suggesting an interface displayed in a right-to-left language. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/5d683460f2af897b631f4dad86fd3473/foundations-rtl-intro%402x.png) + +When people choose a language for their device โ€” or just your app or game โ€” they expect the interface to adapt in various ways (to learn more, see [Localization](https://developer.apple.com/localization/)). + +System-provided UI frameworks support right-to-left (RTL) by default, allowing system-provided UI components to flip automatically in the RTL context. If you use system-provided elements and standard layouts, you might not need to make any changes to your appโ€™s automatically reversed interface. + +If you want to fine-tune your layout or enhance specific localizations to adapt to different currencies, numerals, or mathematical symbols that can occur in various locales in countries that use RTL languages, follow these guidelines. + +## [Text alignment](https://developer.apple.com/design/human-interface-guidelines/right-to-left#Text-alignment) + +**Adjust text alignment to match the interface direction, if the system doesnโ€™t do so automatically.** For example, if you left-align text with content in the left-to-right (LTR) context, right-align the text to match the contentโ€™s mirrored position in the RTL context. + +![An illustration showing a layout of text and images in an interface. Three bars that represent text are left-aligned above a rounded rectangle area. A placeholder image is centered in the area, above another bar at the bottom edge. The bar inside the area is left-aligned.](https://docs-assets.developer.apple.com/published/7bdc0741a96d6e2aa88b79c64e151c8a/text-alignment-ltr-screen%402x.png)Left-aligned text in the LTR context + +![An illustration showing a layout of text and images in an interface. Three bars that represent text are right-aligned above a rounded rectangle area. A placeholder image is centered in the area, above another bar at the bottom edge. The bar inside the area is right-aligned. The placeholder image isn't flipped.](https://docs-assets.developer.apple.com/published/10386033d677b3fd65ec33ac16d67e56/text-alignment-rtl-screen%402x.png)Right-aligned content in the RTL context + +**Align a paragraph based on its language, not on the current context.** When the alignment of a paragraph โ€” defined as three or more lines of text โ€” doesnโ€™t match its language, it can be difficult to read. For example, right-aligning a paragraph that consists of LTR text can make the beginning of each line difficult to see. To improve readability, continue aligning one- and two-line text blocks to match the reading direction of the current context, but align a paragraph to match its language. + +![An image showing two paragraphs of placeholder copy. The first paragraph is in Arabic and is right-aligned. The second paragraph is in English and is left-aligned.](https://docs-assets.developer.apple.com/published/b32ae443b1d7daa1bb661b56b42b8a34/paragraph-alignment-correct%402x.png)A left-aligned paragraph in the RTL context + +![A checkmark in a circle to indicate a correct example.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +![An image showing two paragraphs of placeholder copy. The first paragraph is in Arabic and the second paragraph is in English. Both paragraphs are right-aligned.](https://docs-assets.developer.apple.com/published/738bda44c81a146b02cbd67db5985ff2/paragraph-alignment-wrong%402x.png)A right-aligned paragraph in the RTL context + +![An X in a circle to indicate an incorrect example.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +**Use a consistent alignment for all text items in a list.** To ensure a comfortable reading and scanning experience, reverse the alignment of all items in a list, including items that are displayed in a different script. + +![An illustration of a right-aligned list of gray bars that represent right-to-left text.](https://docs-assets.developer.apple.com/published/8e497bdc80a98b7896b492d2e5bfb57b/mixed-script-list-alignment-correct%402x.png)Right-aligned content in the RTL context + +![A checkmark in a circle to indicate a correct example.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +![An illustration of a list of gray bars. The first, third, fourth, and fifth bars represent right-to-left text. The second bar is incorrectly left-aligned.](https://docs-assets.developer.apple.com/published/8764f467c4870522419bb26fa5894c09/mixed-script-list-alignment-wrong%402x.png)Mixed alignment in the RTL content + +![An X in a circle to indicate an incorrect example.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +## [Numbers and characters](https://developer.apple.com/design/human-interface-guidelines/right-to-left#Numbers-and-characters) + +Different RTL languages can use different number systems. For example, Hebrew text uses Western Arabic numerals, whereas Arabic text might use either Western or Eastern Arabic numerals. The use of Western and Eastern Arabic numerals varies among countries and regions and even among areas within the same country or region. + +If your app covers mathematical concepts or other number-centric topics, itโ€™s a good idea to identify the appropriate way to display such information in each locale you support. In contrast, apps that donโ€™t address number-related topics can generally rely on system-provided number representations. + +![From the left, the numerals one, two, and three in Western Arabic numerals.](https://docs-assets.developer.apple.com/published/c40d3d208a9aee56d680d6915fb44fff/textformat-123-ltr%402x.png)Western Arabic numerals + +![From the right, the numerals one, two, and three in Eastern Arabic numerals.](https://docs-assets.developer.apple.com/published/8a9f9c2f6fb291304a5d93e27be0bead/textformat-123-ar%402x.png)Eastern Arabic numerals + +**Donโ€™t reverse the order of numerals in a specific number.** Regardless of the current language or the surrounding content, the digits in a specific number โ€” such as โ€œ541,โ€ a phone number, or a credit card number โ€” always appear in the same order. + +![From the left, the two words order and number followed by the number 123456 in Latin script.](https://docs-assets.developer.apple.com/published/e6ae8d9dab2a6da825829cf88bfb6adb/latin-numerals%402x.png)Latin + +![From the right, the two words order and number followed by the number 12345 in Hebrew script.](https://docs-assets.developer.apple.com/published/8b4b0b82384424720d861865ac61ad37/hebrew-numerals%402x.png)Hebrew + +![From the right, the two words order and number in Arabic script, followed by the number 12345 in Western Arabic numerals.](https://docs-assets.developer.apple.com/published/427e50992b8e4900fa7f64c73ad8c0b1/western-arabic-numerals%402x.png)Arabic (Western Arabic numerals) + +![From the right, the two words order and number in Arabic script, followed by the number 12345 in Eastern Arabic numerals.](https://docs-assets.developer.apple.com/published/6edfa8597370c06b66cdbbaae728f97b/eastern-arabic-numerals%402x.png)Arabic (Eastern Arabic numerals) + +**Reverse the order of numerals that show progress or a counting direction; never flip the numerals themselves.** Controls like progress bars, sliders, and rating controls often include numerals to clarify their meaning. If you use numerals in this way, be sure to reverse the order of the numerals to match the direction of the flipped control. Also reverse a sequence of numerals if you use the sequence to communicate a specific order. + +![A horizontal row of five stars. From the left, the first three and a half stars are filled. Below the stars is a row of Latin numerals, each numeral vertically aligned with a star above. From the left, the numerals are one, two, three, four, and five.](https://docs-assets.developer.apple.com/published/d249f8e9df8a8dfcf1526dc3f5c4dd5b/match-numeral-order-to-directional-controls-latin%402x.png)Latin + +![A horizontal row of five stars. From the right, the first three and a half stars are filled. Below the stars is a row of Eastern Arabic numerals, each numeral vertically aligned with a star above. From the right, the numerals are one, two, three, four, and five.](https://docs-assets.developer.apple.com/published/77bb1e2c8c704fa2235bd7cc8d7acf31/match-numeral-order-to-directional-controls-eastern-arabic%402x.png)Arabic (Eastern Arabic numerals) + +![A horizontal row of five stars. From the right, the first three and a half stars are filled. Below the stars is a row of Western Arabic numerals, each numeral vertically aligned with a star above. From the right, the numerals are one, two, three, four, and five.](https://docs-assets.developer.apple.com/published/164c27556e186de5aa0c0312639f1c8f/match-numeral-order-to-directional-controls-western-arabic-hebrew%402x.png)Hebrew + +![A horizontal row of five stars. From the right, the first three and a half stars are filled. Below the stars is a row of Western Arabic numerals, each numeral vertically aligned with a star above. From the right, the numerals are one, two, three, four, and five.](https://docs-assets.developer.apple.com/published/164c27556e186de5aa0c0312639f1c8f/match-numeral-order-to-directional-controls-western-arabic-hebrew%402x.png)Arabic (Western Arabic numerals) + +## [Controls](https://developer.apple.com/design/human-interface-guidelines/right-to-left#Controls) + +**Flip controls that show progress from one value to another.** Because people tend to view forward progress as moving in the same direction as the language they read, it makes sense to flip controls like sliders and progress indicators in the RTL context. When you do this, also be sure to reverse the positions of the accompanying glyphs or images that depict the beginning and ending values of the control. + +![An illustration of a volume control slider. The left side has a right-facing speaker glyph with no sound emerging, and the right side has a right-facing speaker glyph with sound waves projecting from it, showing that moving the thumb from left to right makes the volume louder.](https://docs-assets.developer.apple.com/published/7ef757b48788617e37c2a275b6b47f6d/flipped-directional-control-ltr%402x.png)A directional control in the LTR context + +![An illustration of a volume control slider. The right side has a left-facing speaker glyph with no sound emerging, and the left side has a left-facing speaker glyph with sound waves projecting from it, showing that moving the thumb from right to left makes the volume louder.](https://docs-assets.developer.apple.com/published/b5619e5a9fb04db70e26dac8c20313b2/flipped-directional-control-rtl%402x.png)A directional control in the RTL context + +**Flip controls that help people navigate or access items in a fixed order.** For example, in the RTL context, a back button must point to the right so the flow of screens matches the reading order of the RTL language. Similarly, next or previous buttons that let people access items in an ordered list need to flip in the RTL context to match the reading order. + +**Preserve the direction of a control that refers to an actual direction or points to an onscreen area.** For example, if you provide a control that means โ€œto the right,โ€ it must always point right, regardless of the current context. + +**Visually balance adjacent Latin and RTL scripts when necessary.** In buttons, labels, and titles, Arabic or Hebrew text can appear too small when next to uppercased Latin text, because Arabic and Hebrew donโ€™t include uppercase letters. To visually balance Arabic or Hebrew text with Latin text that uses all capitals, it often works well to increase the RTL font size by about 2 points. + +![A horizontal row of three blue oval buttons. Each button is labeled with the word download. From the left, the labels are in Latin, Arabic, and Hebrew scripts, with the English label using all capital letters. Two horizontal red lines run across all three buttons, the top line is the ascender line and the bottom line is the baseline. Every letter in the English label touches both lines. Only the last two letters in the Arabic label touch or extend below the baseline; only the last letter touches the ascender line. No letters in the Hebrew label touch either line. In comparison with the Latin label, both the Arabic and Hebrew labels look small.](https://docs-assets.developer.apple.com/published/190b48a71d8d934047905be986732fb4/download-uneven-vertical-height%402x.png)Arabic and Hebrew text can look too small next to uppercased Latin text of the same font size. + +![A horizontal row of three blue oval buttons. Each button is labeled with the word download. From the left, the labels are in Latin, Arabic, and Hebrew scripts, with the English label using all capital letters. Two horizontal red lines run across all three buttons, the top line is the ascender line and the bottom line is the baseline. Every letter in the English label touches both lines. The last two letters in the Arabic label touch or extend below the baseline, and the first and last letters extend above the ascender line. All letters in the Hebrew label touch the base line and the ascender line. The increased size of the Arabic and Hebrew labels make them look similar in size to the Latin label.](https://docs-assets.developer.apple.com/published/19099f313875cd49849a1ca28f1dfca4/download-even-vertical-height%402x.png)You can slightly increase the font size of Arabic and Hebrew text to visually balance uppercased Latin text. + +## [Images](https://developer.apple.com/design/human-interface-guidelines/right-to-left#Images) + +**Avoid flipping images like photographs, illustrations, and general artwork.** Flipping an image often changes the imageโ€™s meaning; flipping a copyrighted image could be a violation. If an imageโ€™s content is strongly connected to reading direction, consider creating a new version of the image instead of flipping the original. + +![A simplified illustration of a globe that uses solid black shapes to show most of Africa, Europe, Asia, Australia, and Antarctica.](https://docs-assets.developer.apple.com/published/ca80fd6003c4ebff97714123a33c974e/image-displayed-right%402x.png) + +![A checkmark in a circle to indicate a correct example.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +![A simplified illustration of a globe that shows a horizontally flipped Eastern hemisphere with Africa on the far right and Australia on the far left.](https://docs-assets.developer.apple.com/published/0310648a2b1ff40a796e5544d057d30b/image-displayed-wrong%402x.png) + +![An X in a circle to indicate an incorrect example.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +**Reverse the positions of images when their order is meaningful.** For example, if you display multiple images in a specific order like chronological, alphabetical, or favorite, reverse their positions to preserve the orderโ€™s meaning in the RTL context. + +![An illustration showing a layout of text and images within a rounded rectangle. A short bar representing text is left-aligned in the upper-left corner. Below the bar is an area that contains four squares, including a blue square with a placeholder image on the left side. From the left, a row of five square areas at the bottom of the rectangle contain the following shapes: heart, circle, star, square, and triangle.](https://docs-assets.developer.apple.com/published/f8e833e7f73aa3f6ce268dd33f174862/image-positions-ltr%402x.png)Items with meaningful positions in the LTR context + +![An illustration showing a layout of text and images within a rounded rectangle. A short bar representing text is right-aligned in the upper-right corner. Below the bar is an area that contains four squares, including a blue square with a placeholder image on the right side. From the right, a row of five square areas at the bottom of the rectangle contain the following shapes: heart, circle, star, square, and triangle.](https://docs-assets.developer.apple.com/published/5071b9cf5e2c0a2395803018149eab87/image-positions-rtl%402x.png)Items with meaningful positions in the RTL context + +## [Interface icons](https://developer.apple.com/design/human-interface-guidelines/right-to-left#Interface-icons) + +When you use [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) to supply interface icons for your app, you get variants for the RTL context and localized symbols for Arabic and Hebrew, among other languages. If you create custom symbols, you can specify their directionality. For developer guidance, see [Creating custom symbol images for your app](https://developer.apple.com/documentation/UIKit/creating-custom-symbol-images-for-your-app). + +![Three horizontal lines, stacked evenly on top of each other. Each line is preceded by a bullet on left. The shape of a closed book with its spine on the left. A rounded rectangle containing a left-aligned row of three dots. A pencil is slanted at about forty-five degrees, with its point right of the rightmost dot and its eraser extending out of the top-right corner of the rectangle. A rounded rectangle with a black bar across the top that occupies about a quarter of the rectangle's height. A left-aligned row of white dots is in the left side of the bar. A rounded rectangle that contains a smaller, solid-black rounded rectangle near the left side. Outside the rectangle and to the right is a solid-black semicircle with a vertical straight edge that's close to the vertical right side of the rectangle.](https://docs-assets.developer.apple.com/published/eec2236f5595e04904c2b5494696ec1b/directional-symbols-ltr%402x.png)LTR variants of directional symbols + +![Three horizontal lines, stacked evenly on top of each other. Each line is preceded by a bullet on right. The shape of a closed book with its spine on the right. A rounded rectangle containing a right-aligned row of three dots. A pencil is slanted at about forty-five degrees, with its point left of the leftmost dot and its eraser extending out of the middle of the rectangle's top. A rounded rectangle with a black bar across the top that occupies about a quarter of the rectangle's height. A right-aligned row of white dots is in the right side of the bar. A rounded rectangle that contains a smaller, solid-black rounded rectangle near the right side. Outside the rectangle and to the left is a solid-black semicircle with a vertical straight edge that's close to the vertical left side of the rectangle.](https://docs-assets.developer.apple.com/published/9f036ccf7a0ca74375f080c94feb77a3/directional-symbols-rtl%402x.png)RTL variants of directional symbols + +**Flip interface icons that represent text or reading direction.** For example, if an interface icon uses left-aligned bars to represent text in the LTR context, right-align the bars in the RTL context. + +![A rounded rectangle that contains three horizontal left-aligned lines.](https://docs-assets.developer.apple.com/published/298befd594e841846cd466f60d2bea6a/doc-plaintext-ltr%402x.png)LTR variant of a symbol that represents text + +![A rounded rectangle that contains three horizontal right-aligned lines.](https://docs-assets.developer.apple.com/published/bfae7054f6aec52f1a63e31b6c0db79d/doc-plaintext-rtl%402x.png)RTL variant of a symbol that represents text + +**Consider creating a localized version of an interface icon that displays text.** Some interface icons include letters or words to help communicate a script-related concept, like font-size choice or a signature. If you have a custom interface icon that needs to display actual text, consider creating a localized version. For example, SF Symbols offers different versions of the signature, rich-text, and I-beam pointer symbols for use with Latin, Hebrew, and Arabic text, among others. + +![A small X left-aligned above a horizontal line. A stylized signature begins at the X and finishes at the right end of the line. A rounded rectangle containing a capital letter A in the top-left corner and a stack of two horizontal lines in the top-right corner. A placeholder image appears in the bottom half of the rectangle. A large capital letter A to the left of a tall I-beam cursor.](https://docs-assets.developer.apple.com/published/431f27ff945804173931cfd38f595b2c/text-icon-localized-latin%402x.png)Latin + +![A small X right-aligned above a horizontal line. A stylized signature begins at the X and finishes at the left end of the line. A rounded rectangle containing the letter Alef in the top-right corner and a stack of two horizontal lines in the top-left corner. A placeholder image appears in the bottom half of the rectangle. A large letter Alef to the right of a tall I-beam cursor.](https://docs-assets.developer.apple.com/published/b457fc7b677ccbf085cd1ea1d8bc5601/text-icon-localized-hebrew%402x.png)Hebrew + +![A small X right-aligned above a horizontal line. A stylized signature begins at the X and finishes at the left end of the line. A rounded rectangle containing the letter Ain in the top-right corner and a stack of two horizontal lines in the top-left corner. A placeholder image appears in the bottom half of the rectangle. A large letter Dad to the right of a tall I-beam cursor.](https://docs-assets.developer.apple.com/published/7c91fa369eb21255aed0a545bcf9b62d/text-icon-localized-arabic%402x.png)Arabic + +If you have a custom interface icon that uses letters or words to communicate a concept unrelated to reading or writing, consider designing an alternative image that doesnโ€™t use text. + +**Flip an interface icon that shows forward or backward motion.** When something moves in the same direction that people read, they typically interpret that direction as forward; when something moves in the opposite direction, people tend to interpret the direction as backward. An interface icon that depicts an object moving forward or backward needs to flip in the RTL context to preserve the meaning of the motion. For example, an icon that represents a speaker typically shows sound waves emanating forward from the speaker. In the LTR context, the sound waves come from the left, so in the RTL context, the icon needs to flip to show the waves coming from the right. + +![The outline of a speaker with three concentric curved lines emanating to the right.](https://docs-assets.developer.apple.com/published/d43d629eea61239a9268d6616551b48c/speaker-wave-3-ltr%402x.png)LTR variant of a symbol that depicts forward motion + +![The outline of a speaker with three concentric curved lines emanating to the left.](https://docs-assets.developer.apple.com/published/d10bb4c00b214c16a802183377134b59/speaker-wave-3-rtl%402x.png)RTL variant of a symbol that depicts forward motion + +**Donโ€™t flip logos or universal signs and marks.** Displaying a flipped logo confuses people and can have legal repercussions. Always display a logo in its original form, even if it includes text. People expect universal symbols and marks like the checkmark to have a consistent appearance, so avoid flipping them. + +![A rounded square that contains the black Apple TV logo, which consists of a solid black apple to the left of the lowercase letters T and V.](https://docs-assets.developer.apple.com/published/7c7eb6d19b63d77412c7754893c0f65c/appletv-ltr%402x.png)A logo + +![A checkmark.](https://docs-assets.developer.apple.com/published/31cfb3b8b93a1747eddac562a979a9cb/checkmark-ltr%402x.png)A universal symbol or mark + +**In general, avoid flipping interface icons that depict real-world objects.** Unless you use the object to indicate directionality, itโ€™s best to avoid flipping an icon that represents a familiar item. For example, clocks work the same everywhere, so a traditional clock interface icon needs to look the same regardless of language direction. Some interface icons might seem to reference language or reading direction because they represent items that are slanted for right-handed use. However, most people are right-handed, so flipping an icon that shows a right-handed tool isnโ€™t necessary and might be confusing. + +![A black disk with two white lines in the nine o'clock position.](https://docs-assets.developer.apple.com/published/2d167db99027c9f44270a86a273f225f/clock-fill-ltr%402x.png) + +![A pencil with an eraser, slanted at about forty-five degrees with the point in the bottom-left.](https://docs-assets.developer.apple.com/published/6597719e77e19638bb265cd6c58f9a8a/pencil-ltr%402x.png) + +![The silhouette of a game controller with a white plus sign on the left and two white buttons on the right.](https://docs-assets.developer.apple.com/published/c3f51c228de248bf096aae7164836eab/gamecontroller-fill-ltr%402x.png) + +**Before merely flipping a complex custom interface icon, consider its individual components and the overall visual balance.** In some cases, a component โ€” like a badge, slash, or magnifying glass โ€” needs to adhere to a visual design language regardless of localization. For example, SF Symbols maintains visual consistency by using the same backslash to represent the prohibition or negation of a symbolโ€™s meaning in both LTR and RTL versions. + +![A silhouette of a speaker pointing right with a backslash on top of it.](https://docs-assets.developer.apple.com/published/0557fd6fd8fc1b2c347cd869baa6ae0e/speaker-slash-fill-ltr%402x.png)LTR variant of a symbol that includes a backslash + +![A silhouette of a speaker pointing left with a backslash on top of it.](https://docs-assets.developer.apple.com/published/42dc822fc59ebc4c8d02d6e6c7fa0959/speaker-slash-fill-rtl%402x.png)RTL variant of a symbol that includes a backslash + +In other cases, you might need to flip a component (or its position) to ensure the localized version of the icon still makes sense. For example, if a badge represents the actual UI that people see in your app, it needs to flip if your UI flips. Alternatively, if a badge modifies the meaning of an interface icon, consider whether flipping the badge preserves both the modified meaning and the overall visual balance of the icon. In the images shown below, the badge doesnโ€™t depict an object in the UI, but keeping it in the top-right corner visually unbalances the cart. + +![A silhouette of a wheeled shopping cart that faces right. A white plus sign inside a black disk is in the top-right corner.](https://docs-assets.developer.apple.com/published/faa9849953c7b1b1470db91ed25125d0/cart-fill-badge-plus-ltr%402x.png) + +![A checkmark in a circle to indicate a correct example.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +![A silhouette of a wheeled shopping cart that faces left. A white plus sign inside a black disk is in the top-right corner.](https://docs-assets.developer.apple.com/published/c065f8369e681461bc34ea590b80994b/cart-fill-badge-rtl-unbalanced%402x.png) + +![An X in a circle to indicate an incorrect example.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![A silhouette of a wheeled shopping cart that faces left. A white plus sign inside a black disk is in the top-left corner.](https://docs-assets.developer.apple.com/published/97251e1850265c3b1d654d1e4631ca74/cart-fill-badge-plus-rtl%402x.png) + +![A checkmark in a circle to indicate a correct example.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +If your custom interface icon includes a component that can imply handedness, like a tool, consider preserving the orientation of the tool while flipping the base image if necessary. + +![A rounded rectangle that contains a black dot in the top-right corner. The outline of a magnifying glass that contains a stack of two left-aligned lines is on top of the rectangle and to the left of the dot, slanted at about 135 degrees.](https://docs-assets.developer.apple.com/published/0c8dd8148be262162bb75a017e2ae197/mail-and-text-magnifyingglass-ltr%402x.png)LTR variant of a symbol that depicts a tool + +![A rounded rectangle that contains a black dot in the top-left corner. The outline of a magnifying glass that contains a stack of two rightt-aligned lines is on top of the rectangle and to the right of the dot, slanted at about 135 degrees.](https://docs-assets.developer.apple.com/published/f3ca739120456691b67e55d150596716/mail-and-text-magnifyingglass-rtl%402x.png)RTL variant of a symbol that depicts a tool + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/right-to-left#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/right-to-left#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/right-to-left#Related) + +[Layout](https://developer.apple.com/design/human-interface-guidelines/layout) + +[Inclusion](https://developer.apple.com/design/human-interface-guidelines/inclusion) + +[SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/right-to-left#Developer-documentation) + +[Localization](https://developer.apple.com/localization/) + +[Preparing views for localization](https://developer.apple.com/documentation/SwiftUI/Preparing-views-for-localization) โ€” SwiftUI + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/right-to-left#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/4498DDBC-5903-48A6-85EB-47BCACA39DFB/9915_wide_250x141_1x.jpg) Enhance your appโ€™s multilingual experience ](https://developer.apple.com/videos/play/wwdc2025/222) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/124/7F5167EA-F6A3-4605-83FF-FF75E802969C/6527_wide_250x141_1x.jpg) Design for Arabic ](https://developer.apple.com/videos/play/wwdc2022/10034) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/sf-symbols.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/sf-symbols.md new file mode 100644 index 00000000..881debfd --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/sf-symbols.md @@ -0,0 +1,310 @@ +--- +title: "SF Symbols | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/sf-symbols + +# SF Symbols + +SF Symbols provides thousands of consistent, highly configurable symbols that integrate seamlessly with the San Francisco system font, automatically aligning with text in all weights and sizes. + +![A sketch of the SF Symbols icon. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/06d528652b87b23f1cecaf5faceedf30/foundations-sf-symbols-intro%402x.png) + +You can use a symbol to convey an object or concept wherever interface icons can appear, such as in toolbars, tab bars, context menus, and within text. + +Availability of individual symbols and features varies based on the version of the system youโ€™re targeting. Symbols and symbol features introduced in a given year arenโ€™t available in earlier operating systems. + +Visit [SF Symbols](https://developer.apple.com/sf-symbols/) to download the app and browse the full set of symbols. Be sure to understand the terms and conditions for using SF Symbols, including the prohibition against using symbols โ€” or images that are confusingly similar โ€” in app icons, logos, or any other trademarked use. For developer guidance, see [Configuring and displaying symbol images in your UI](https://developer.apple.com/documentation/UIKit/configuring-and-displaying-symbol-images-in-your-ui). + +## [Rendering modes](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Rendering-modes) + +SF Symbols provides four rendering modes โ€” monochrome, hierarchical, palette, and multicolor โ€” that give you multiple options when applying color to symbols. For example, you might want to use multiple opacities of your appโ€™s accent color to give symbols depth and emphasis, or specify a palette of contrasting colors to display symbols that coordinate with various color schemes. + +To support the rendering modes, SF Symbols organizes a symbolโ€™s paths into distinct layers. For example, the `cloud.sun.rain.fill` symbol consists of three layers: the primary layer contains the cloud paths, the secondary layer contains the paths that define the sun and its rays, and the tertiary layer contains the raindrop paths. + +![An image of the cloud sun rain fill symbol. The cloud is black and the raindrops and sun are gray to indicate that the cloud is in the primary layer.](https://docs-assets.developer.apple.com/published/42c350caa5e5117d40d45ac28c258832/sf-three-layers-primary%402x.png)Primary + +![An image of the cloud sun rain fill symbol. The sun is black and the raindrops and cloud are gray to indicate that the sun is in the secondary layer.](https://docs-assets.developer.apple.com/published/9acc461ef73c512ab21e4713fcdc75a3/sf-three-layers-secondary%402x.png)Secondary + +![An image of the cloud sun rain fill symbol. The raindrops are black and the sun and cloud are gray to indicate that the raindrops are in the primary layer.](https://docs-assets.developer.apple.com/published/f2ec783b9aedc7f59c3485efb83fbb94/sf-three-layers-tertiary%402x.png)Tertiary + +Depending on the rendering mode you choose, a symbol can produce various appearances. For example, Hierarchical rendering mode assigns a different opacity of a single color to each layer, creating a visual hierarchy that gives depth to the symbol. + +![An image of the cloud sun rain fill symbol that uses three different opacities of the system blue color in the symbolโ€™s three different layers: the cloud is fully opaque, the sun is about 50% opaque, and the raindrops are about 25% opaque.](https://docs-assets.developer.apple.com/published/35fe9f56dee989f094845e640951fef5/sf-three-layers-color%402x.png) + +To learn more about supporting rendering modes in custom symbols, see [Custom symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Custom-symbols). + +SF Symbols supports the following rendering modes. + +**Monochrome** โ€” Applies one color to all layers in a symbol. Within a symbol, paths render in the color you specify or as a transparent shape within a color-filled path. + +![A diagram showing a row of eight symbols, all of which use a single opacity of the system blue color.](https://docs-assets.developer.apple.com/published/b296a9ee1b06b49c011209c83b537096/sf-monochrome%402x.png) + +**Hierarchical** โ€” Applies one color to all layers in a symbol, varying the colorโ€™s opacity according to each layerโ€™s hierarchical level. + +![A diagram showing a row of eight symbols, each of which uses different opacities of the system blue color. From the left, the square and arrow up symbol uses full opacity for the arrow and low opacity in the square. Next, folder badge plus uses full opacity for the badge and low opacity for the folder. Trash slash uses full opacity for the slash and low opacity for the can. Calendar day timeline right uses full opacity for the horizontal indicator and low opacity for the square and dots. List number uses full opacity for the column of numbers and low opacity for the horizontal lines. Text format A B C dotted underline uses full opacity for the dots under the low-opacity letters. iPhone radio waves left and right uses full opacity for the device outline, mid opacity in the screen area, and low opacity for the radio wave lines. Lastly, the PC symbol uses full opacity for the device outline and the onscreen sad face and horizontal lines and mid opacity in the screen background.](https://docs-assets.developer.apple.com/published/c035476f6266b094b051d4e392329092/sf-hierarchical%402x.png) + +**Palette** โ€” Applies two or more colors to a symbol, using one color per layer. Specifying only two colors for a symbol that defines three levels of hierarchy means the secondary and tertiary layers use the same color. + +![A diagram showing a row of eight symbols, each of which uses a combination of gray and the system blue color. From the left, the square and arrow up symbol uses blue for the arrow and light gray for the square. Next, folder badge plus uses blue for the badge and light gray for the folder. Trash slash uses blue for the slash and light gray for the can. Calendar day timeline right uses blue for the horizontal indicator and light gray for the square and dots. List number uses blue for the column of numbers and light gray for the horizontal lines. Text format A B C dotted underline uses blue for the dots under the light gray letters. iPhone radio waves left and right uses blue for the device outline, medium gray in the screen area, and light gray for the radio wave lines. Lastly, the PC symbol uses blue for the device outline and the onscreen sad face and horizontal lines and medium gray in the screen background.](https://docs-assets.developer.apple.com/published/5ea6a976464ec36e5dbdbf30588a25e0/sf-palette%402x.png) + +**Multicolor** โ€” Applies intrinsic colors to some symbols to enhance meaning. For example, the `leaf` symbol uses green to reflect the appearance of leaves in the physical world, whereas the `trash.slash` symbol uses red to signal data loss. Some multicolor symbols include layers that can receive other colors. + +![A diagram showing a row of eight symbols, using combinations of various colors. From the left, the square and arrow up symbol uses blue for all lines. Next, folder badge plus uses green for the badge and blue for the folder. Trash slash uses red for both the slash and the can. Calendar day timeline right uses red for the horizontal indicator, dark gray for the square, and light gray for the dots. List number uses black for the column of numbers and medium gray for the horizontal lines. Text format A B C dotted underline uses red for the dots under the black letters. iPhone radio waves left and right uses blue for all lines. Lastly, the PC symbol uses yellow for the device outline, white for the onscreen sad face and horizontal lines, and blue in the screen background.](https://docs-assets.developer.apple.com/published/82097ab3d98f098d12935ab4d6c1c896/sf-multicolor%402x.png) + +Regardless of rendering mode, using system-provided colors ensures that symbols automatically adapt to accessibility accommodations and appearance modes like vibrancy and Dark Mode. For developer guidance, see [renderingMode(_:)](https://developer.apple.com/documentation/swiftui/image/renderingmode\(_:\)). + +**Confirm that a symbolโ€™s rendering mode works well in every context.** Depending on factors like the size of a symbol and its contrast with the current background color, different rendering modes can affect how well people can discern the symbolโ€™s details. You can use the automatic setting to get a symbolโ€™s preferred rendering mode, but itโ€™s still a good idea to check the results for places where a different rendering mode might improve a symbolโ€™s legibility. + +## [Gradients](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Gradients) + +In SF Symbols 7 and later, gradient rendering generates a smooth linear gradient from a single source color. You can use gradients across all rendering modes for both system and custom colors and for custom symbols. Gradients render for symbols of any size, but look best at larger sizes. + +![The sun symbol with a solid yellow fill.](https://docs-assets.developer.apple.com/published/2df52fad04d6250f02143138ff76da14/sf-symbols-sun-solid-fill%402x.png)Solid fill + +![The sun symbol with a gradient fill derived from a single yellow source color. The gradient color is bright on the left edge of the symbol, and subtly darkens as it approaches the right edge.](https://docs-assets.developer.apple.com/published/18a48b9b3b9f3842ff42f1331edeb5fb/sf-symbols-sun-gradient-fill%402x.png)Gradient fill + +## [Variable color](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Variable-color) + +With variable color, you can represent a characteristic that can change over time โ€” like capacity or strength โ€” regardless of rendering mode. To visually communicate such a change, variable color applies color to different layers of a symbol as a value reaches different thresholds between zero and 100 percent. + +For example, you could use variable color with the `speaker.wave.3` symbol to communicate three different ranges of sound โ€” plus the state where thereโ€™s no sound โ€” by mapping the layers that represent the curved wave paths to different ranges of decibel values. In the case of no sound, no wave layers get color. In all other cases, a wave layer receives color when the sound reaches a threshold the system defines based on the number of nonzero states you want to represent. + +![A diagram showing four versions of the speaker wave three symbol, each of which displays color in a different number of wave paths. From the left, the number of waves with color is zero, one, two, and three.](https://docs-assets.developer.apple.com/published/e03af602ef484d26ff5cc3428e98079a/sf-variable-color%402x.png) + +Sometimes, it can make sense for some of a symbolโ€™s layers to opt out of variable color. For example, in the `speaker.wave.3` symbol shown above, the layer that contains the speaker path doesnโ€™t receive variable color because a speaker doesnโ€™t change as the sound level changes. A symbol can support variable color in any number of layers. + +**Use variable color to communicate change โ€” donโ€™t use it to communicate depth.** To convey depth and visual hierarchy, use Hierarchical rendering mode to elevate certain layers and distinguish foreground and background elements in a symbol. + +## [Weights and scales](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Weights-and-scales) + +SF Symbols provides symbols in a wide range of weights and scales to help you create adaptable designs. + +![A diagram showing the square and arrow up symbol in all 27 weights and scales.](https://docs-assets.developer.apple.com/published/a46e2c294c605c4f5e5f626c67d6bb2d/sf-scales-weights%402x.png) + +Each of the nine symbol weights โ€” from ultralight to black โ€” corresponds to a weight of the San Francisco system font, helping you achieve precise weight matching between symbols and adjacent text, while supporting flexibility for different sizes and contexts. + +Each symbol is also available in three scales: small, medium (the default), and large. The scales are defined relative to the cap height of the San Francisco system font. + +![A diagram showing the first of three images of the plus circle symbol followed by the capitalized word add. In each image, the word uses the same size, but the symbol uses a different size. The symbol size is small in this image. Two parallel horizontal lines appear across all three images. The top line shows the height of the capital letter A and the bottom line is the baseline under the word add. In this small symbol, the circle touches both lines.](https://docs-assets.developer.apple.com/published/bf6a6d81c531a772bbe9c768af32f0b8/sf-symbol-scale-small%402x.png)Small + +![The second of three images of the plus circle symbol followed by the capitalized word add. In this medium symbol, the circle extends slightly above and below the lines.](https://docs-assets.developer.apple.com/published/aa672f59358a8bb354e4ea9e7d258467/sf-symbol-scale-medium%402x.png)Medium + +![The third of three images of the plus circle symbol followed by the capitalized word add. In this large symbol, the vertical line of the plus sign almost touches both lines.](https://docs-assets.developer.apple.com/published/6696a9dbf59f8ef7118ac712067de2e6/sf-symbol-scale-large%402x.png)Large + +Specifying a scale lets you adjust a symbolโ€™s emphasis compared to adjacent text, without disrupting the weight matching with text that uses the same point size. For developer guidance, see [`imageScale(_:)`](https://developer.apple.com/documentation/SwiftUI/View/imageScale\(_:\)) (SwiftUI), [`UIImage.SymbolScale`](https://developer.apple.com/documentation/UIKit/UIImage/SymbolScale) (UIKit), and [`NSImage.SymbolConfiguration`](https://developer.apple.com/documentation/AppKit/NSImage/SymbolConfiguration-swift.class) (AppKit). + +## [Design variants](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Design-variants) + +SF Symbols defines several design variants โ€” such as fill, slash, and enclosed โ€” that can help you communicate precise states and actions while maintaining visual consistency and simplicity in your UI. For example, you could use the slash variant of a symbol to show that an item or action is unavailable, or use the fill variant to indicate selection. + +Outline is the most common variant in SF Symbols. An outlined symbol has no solid areas, resembling the appearance of text. Most symbols are also available in a fill variant, in which the areas within some shapes are solid. + +In addition to outline and fill, SF Symbols also defines variants that include a slash or enclose a symbol within a shape like a circle, square, or rectangle. In many cases, enclosed and slash variants can combine with outline or fill variants. + +![A diagram showing two rows of the same five symbols. In the top row, every symbol uses the outline variant; the bottom row shows the fill variant of each symbol. From the left, the symbols are heart, heart slash, heart circle, heart square, and a heart in a rectangle.](https://docs-assets.developer.apple.com/published/c4486c6d1dc36ea164665276e912139a/sf-variants%402x.png) + +SF Symbols provides many variants for specific languages and writing systems, including Latin, Arabic, Hebrew, Hindi, Thai, Chinese, Japanese, Korean, Cyrillic, Devanagari, and several Indic numeral systems. Language- and script-specific variants adapt automatically when the device language changes. For guidance, see [Images](https://developer.apple.com/design/human-interface-guidelines/right-to-left#Images). + +![A diagram with eight rows of the same twelve symbols, where each row shows a localized version of the symbol. From the left the symbols are doc rich text, doc rich text fill, character book closed, character book closed fill, character bubble, character bubble fill, character, text format superscript, text format subscript, text format size, character text box, and character cursor I beam.](https://docs-assets.developer.apple.com/published/cf9d526c8f3a39b600f0226125a2b228/sf-localized%402x.png) + +Symbol variants support a range of design goals. For example: + + * The outline variant works well in toolbars, lists, and other places where you display a symbol alongside text. + + * Symbols that use an enclosing shape โ€” like a square or circle โ€” can improve legibility at small sizes. + + * The solid areas in a fill variant tend to give a symbol more visual emphasis, making it a good choice for iOS tab bars and swipe actions and places where you use an accent color to communicate selection. + + + + +In many cases, the view that displays a symbol determines whether to use outline or fill, so you donโ€™t have to specify a variant. For example, an iOS tab bar prefers the fill variant, whereas a toolbar takes the outline variant. + +## [Animations](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Animations) + +SF Symbols provides a collection of expressive, configurable animations that enhance your interface and add vitality to your app. Symbol animations help communicate ideas, provide feedback in response to peopleโ€™s actions, and signal changes in status or ongoing activities. + +Animations work on all SF Symbols in the library, in all rendering modes, weights, and scales, and on custom symbols. For considerations when animating custom symbols, see [Custom symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Custom-symbols). You can control the playback of an animation, whether you want the animation to run from start to finish, or run indefinitely, repeating its effect until a condition is met. You can customize behaviors, like changing the playback speed of an animation or determining whether to reverse an animation before repeating it. For developer guidance, see [Symbols](https://developer.apple.com/documentation/Symbols) and [`SymbolEffect`](https://developer.apple.com/documentation/Symbols/SymbolEffect). + +**Appear** โ€” Causes a symbol to gradually emerge into view. + +Video with custom controls. + +Content description: A video showing three symbols with the same appear animation effect applied to each. In each animation, the symbol layers gradually animate into view. From the left, the symbols are an antenna with radio waves that animate from the center outward, a photo stack with lines representing a stack animating from the bottom to the top, and a waveform animating from left to right. + +Play + +**Disappear** โ€” Causes a symbol to gradually recede out of view. + +Video with custom controls. + +Content description: A video showing three symbols with the same disappear animation effect applied to each. In each animation, all the symbol layers gradually animate out of view. From the left, the symbols are a folder with a badge plus icon, two overlapping lightbulbs, and two overlapping chat bubbles + +Play + +**Bounce** โ€” Briefly scales a symbol with an elastic-like movement that goes either up or down and then returns to the symbolโ€™s initial state. The bounce animation plays once by default and can help communicate that an action occurred or needs to take place. + +Video with custom controls. + +Content description: A video showing three symbols with the same bounce animation effect applied to each. In each animation, the symbol layers individually bounce. From the left, the symbols are a music note with three lines, text that reads haha, and the Live Photos icon. + +Play + +**Scale** โ€” Changes the size of a symbol, increasing or decreasing its scale. Unlike the bounce animation, which returns the symbol to its original state, the scale animation persists until you set a new scale or remove the effect. You might use the scale animation to draw peopleโ€™s attention to a selected item or as feedback when people choose a symbol. + +Video with custom controls. + +Content description: A video showing three symbols with the same scale animation effect applied to each. In each animation, the symbol decreases in size, and after a pause, increases back to the original size. From the left, the symbols are a PIP exit window, a 3D stack of three diagonally positioned squares, and an overlapping HomePod and HomePod mini. + +Play + +**Pulse** โ€” Varies the opacity of a symbol over time. This animation automatically pulses only the layers within a symbol that are annotated to pulse, and optionally can pulse all layers within a symbol. You might use the pulse animation to communicate ongoing activity, playing it continuously until a condition is met. + +Video with custom controls. + +Content description: A video showing three symbols with the same pulse animation effect applied to each. In each animation, one layer pulses its opacity. From the left, the symbols are the AirPlay icon with a pulsing screen, a chat bubble with a waveform that is overlapped with a pulsing pause button, and a pulsing rectangle to represent a screen that is overlapped with a person icon. + +Play + +**Variable color** โ€” Incrementally varies the opacity of layers within a symbol. This animation can be cumulative or iterative. When cumulative, color changes persist for each layer until the animation cycle is complete. When iterative, color changes occur one layer at a time. You might use variable color to communicate progress or ongoing activity, such as playback, connecting, or broadcasting. You can customize the animation to autoreverse โ€” meaning reverse the animation to the starting point and replay the sequence โ€” as well as hide inactive layers rather than reduce their opacity. + +The arrangement of layers within a symbol determines how variable color behaves during a repeating animation. Symbols with layers that are arranged linearly where the start and end points donโ€™t meet are annotated as _open loop_. Symbols with layers that follow a complete shape where the start and end points do meet, like in a circular progress indicator, are annotated as _closed loop_. Variable color animations for symbols with closed loop designs feature seamless, continuous playback. + +Video with custom controls. + +Content description: A video showing three symbols with the same variable color animation effect applied to each. In each animation, color is added one path at a time. From the left, the symbols are a speaker with color cycling through three sound waves, a Wi-Fi symbol with color cycling through two paths that represent signal strength before reversing and replaying the animation, and a sprinkler icon with color cycling through droplets. + +Play + +**Replace** โ€” Replaces one symbol with another. The replace animation works between arbitrary symbols and across all weights and rendering modes. This animation features three configurations: + + * Down-up, where the outgoing symbol scales down and the incoming symbol scales up, communicating a change in state. + + * Up-up, where both the outgoing and incoming symbols scale up. This configuration communicates a change in state that includes a sense of forward progression. + + * Off-up, where the outgoing symbol hides immediately and the incoming symbol scales up. This configuration communicates a state change that emphasizes the next available state or action. + + + + +Video with custom controls. + +Content description: A video showing three symbols with the same replace animation effect applied to each. In each animation, one symbol is replaced by a new symbol, and then replaced by the original symbol. From the left, the symbols are a grid of four squares replaced by a bulleted list, a cloud with rain replaced by a cloud partly blocking the sun, and microphone symbol replaced by an x symbol in a circle. + +Play + +From left to right: down-up, up-up, off-up + +**Magic Replace** โ€” Performs a smart transition between two symbols with related shapes. For example, slashes can draw on and off, and badges can appear or disappear, or you can replace them independently of the base symbol. Magic Replace is the new default replace animation, but doesnโ€™t occur between unrelated symbols; the default down-up animation occurs instead. You can choose a custom direction for the fallback animation in these situations if you prefer one other than the default. + +Video with custom controls. + +Content description: A video showing three symbols each with a shape being added, removed, or replaced using the Magic Replace animation effect. In each animation, the symbol is transformed, and then the transformation is reverted. From the left, the symbols are a credit card with a triangle caution shape added, a microphone with a diagonal slash added, and a circular ID with a checkmark badge replaced by an X badge. + +Play + +**Wiggle** โ€” Moves the symbol back and forth along a directional axis. You might use the wiggle animation to highlight a change or a call to action that a person might overlook. Wiggle can also add a dynamic emphasis to an interaction or reinforce what the symbol is representing, such as when an arrow points in a specific direction. + +Video with custom controls. + +Content description: A video showing three symbols that wiggle laterally, rotationally, or along a linear axis. From the left, the symbols are an arrow pointing down at a container that wiggles vertically; a stack of two photos that wiggles rotationally; and a top-down car between two lane markers with arrows pointing inward that wiggles horizontally. + +Play + +**Breathe** โ€” Smoothly increases and decreases the presence of a symbol, giving it a living quality. You might use the breathe animation to convey status changes, or signal that an activity is taking place, like an ongoing recording session. Breathe is similar to pulse; however pulse animates by changing opacity alone, while breathe changes both opacity and size to convey ongoing activity. + +Video with custom controls. + +Content description: A video showing three symbols that breathe in and out, growing and shrinking in size and changing opacity in a smooth rhythm. From the left, the symbols are a stylized waveform of vertical lines that expand and contract from left to right with a pulse of variable opacity; a pair of translation word bubbles that grow with reduced opacity, then shrink with increased opacity; and three concentric mindfulness rings that pulse outward with reduced opacity, then inward with increased opacity. + +Play + +**Rotate** โ€” Rotates the symbol to act as a visual indicator or imitate an objectโ€™s behavior in the real world. For example, when a task is in progress, rotation confirms that itโ€™s working as expected. The rotate animation causes some symbols to rotate entirely, while in others only certain parts of the symbol rotate. Symbols like the desk fan, for example, use the By Layer rotation option to spin only the fan blades. + +Video with custom controls. + +Content description: A video showing three symbols that either rotate completely or contain a rotating shape. From the left, the symbols are a rotating gear; a desk fan with rotating fan blades; and two dots rotating on concentric orbital paths around a center circle. + +Play + +**Draw On / Draw Off** โ€” In SF Symbols 7 and later, draws the symbol along a path through a set of guide points, either from offscreen to onscreen (Draw On) or from onscreen to offscreen (Draw Off). You can draw all layers at once, stagger them, or draw each layer one at a time. You might use the draw animation to convey progress, as with a download, or to reinforce the meaning of a symbol, like a directional arrow. + +**Apply symbol animations judiciously.** While thereโ€™s no limit to how many animations you can add to a view, too many animations can overwhelm an interface and distract people. + +**Make sure that animations serve a clear purpose in communicating a symbolโ€™s intent.** Each type of animation has a discrete movement that communicates a certain type of action or elicits a certain response. Consider how people might interpret an animated symbol and whether the animation, or combination of animations, might be confusing. + +**Use symbol animations to communicate information more efficiently.** Animations provide visual feedback, reinforcing that something happened in your interface. You can use animations to present complex information in a simple way and without taking up a lot of visual space. + +**Consider your appโ€™s tone when adding animations.** When animating a symbol, think about what the animation can convey and how that might align with your brand identity and your appโ€™s overall style and tone. For guidance, see [Branding](https://developer.apple.com/design/human-interface-guidelines/branding). + +## [Custom symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Custom-symbols) + +If you need a symbol that SF Symbols doesnโ€™t provide, you can create your own. To create a custom symbol, first export the template for a symbol thatโ€™s similar to the design you want, then use a vector-editing tool to modify it. For developer guidance, see [Creating custom symbol images for your app](https://developer.apple.com/documentation/UIKit/creating-custom-symbol-images-for-your-app). + +Important + +SF Symbols includes copyrighted symbols that depict Apple products and features. You can display these symbols in your app, but you canโ€™t customize them. To help you identify a noncustomizable symbol, the SF Symbols app badges it with an Info icon; to help you use the symbol correctly, the inspector pane describes its usage restrictions. + +Using a process called _annotating_ , you can assign a specific color โ€” or a specific hierarchical level, such as primary, secondary, or tertiary โ€” to each layer in a custom symbol. Depending on the rendering modes you support, you can use a different mode in each instance of the symbol in your app. + +**Use the template as a guide.** Create a custom symbol thatโ€™s consistent with the ones the system provides in level of detail, optical weight, alignment, position, and perspective. Strive to design a symbol that is: + + * Simple + + * Recognizable + + * Inclusive + + * Directly related to the action or content it represents + + + + +For guidance, see [Icons](https://developer.apple.com/design/human-interface-guidelines/icons). + +**Assign negative side margins to your custom symbol if necessary.** SF Symbols supports negative side margins to aid optical horizontal alignment when a symbol contains a badge or other elements that increase its width. For example, negative side margins can help you horizontally align a stack of folder symbols, some of which include a badge. The name of each margin includes the relevant configuration โ€” such as โ€œleft-margin-Regular-Mโ€ โ€” so be sure to use this naming pattern if you add margins to your custom symbols. + +**Optimize layers to use animations with custom symbols.** If you want to animate your symbol by layer, make sure to annotate the layers in the SF Symbols app. The Z-order determines the order that you want to apply colors to the layers of a variable color symbol, and you can choose whether to animate those changes from front-to-back, or back-to-front. You can also animate by layer groups to have related layers move together. + +**Test animations for custom symbols.** Itโ€™s important to test your custom symbols with all of the animation presets because the shapes and paths might not appear how you expect when the layers are in motion. To get the most out of this feature, consider drawing your custom symbols with whole shapes. For example, a custom symbol similar to the `person.2.fill` symbol doesnโ€™t need to create a cutout for the shape representing the person on the left. Instead, you can draw the full shape of the person, and in addition to that, draw an offset path of the person on the right to help represent the gap between them. You can later annotate this offset path as an erase layer to render the symbol as you want. This method of drawing helps preserve additional layer information that allows for animations to perform as you expect. + +**Avoid making custom symbols that include common variants, such as enclosures or badges.** The SF Symbols app offers a component library for creating variants of your custom symbol. Using the component library allows you to create commonly used variants of your custom symbol while maintaining design consistency with the included SF Symbols. + +**Provide alternative text labels for custom symbols.** Alternative text labels โ€” or accessibility descriptions โ€” let VoiceOver describe visible UI and content, making navigation easier for people with visual disabilities. For guidance, see [VoiceOver](https://developer.apple.com/design/human-interface-guidelines/voiceover). + +**Donโ€™t design replicas of Apple products.** Apple products are copyrighted and you canโ€™t reproduce them in your custom symbols. Also, you canโ€™t customize a symbol that SF Symbols identifies as representing an Apple feature or product. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Related) + +[Download SF Symbols](https://developer.apple.com/sf-symbols/) + +[Typography](https://developer.apple.com/design/human-interface-guidelines/typography) + +[Icons](https://developer.apple.com/design/human-interface-guidelines/icons) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Developer-documentation) + +[Symbols](https://developer.apple.com/documentation/Symbols) โ€” Symbols framework + +[Configuring and displaying symbol images in your UI](https://developer.apple.com/documentation/UIKit/configuring-and-displaying-symbol-images-in-your-ui) โ€” UIKit + +[Creating custom symbol images for your app](https://developer.apple.com/documentation/UIKit/creating-custom-symbol-images-for-your-app) โ€” UIKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/4F1E5BC7-921E-46C0-927A-D84295787A94/9994_wide_250x141_1x.jpg) Whatโ€™s new in SF Symbols 7 ](https://developer.apple.com/videos/play/wwdc2025/337) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/sf-symbols#Change-log) + +Date| Changes +---|--- +July 28, 2025| Updated with guidance for Draw animations and gradient rendering in SF Symbols 7. +June 10, 2024| Updated with guidance for new animations and features of SF Symbols 6. +June 5, 2023| Added a new section on animations. Included animation guidance for custom symbols. +September 14, 2022| Added a new section on variable color. Removed instructions on creating custom symbol paths, exporting templates, and layering paths, deferring to developer articles that cover these topics. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/spatial-layout.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/spatial-layout.md new file mode 100644 index 00000000..7e454d8b --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/spatial-layout.md @@ -0,0 +1,142 @@ +--- +title: "Spatial layout | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/spatial-layout + +# Spatial layout + +Spatial layout techniques help you take advantage of the infinite canvas of Apple Vision Pro and present your content in engaging, comfortable ways. + +![A sketch of axes in the X, Y, and Z dimensions, suggesting three-dimensional layout. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/e13737927c465ae264094aa019129252/foundations-spatial-layout-intro%402x.png) + +## [Field of view](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Field-of-view) + +A personโ€™s _field of view_ is the space they can see without moving their head. The dimensions of an individualโ€™s field of view while wearing Apple Vision Pro vary based on factors like the way people configure the Light Seal and the extent of their peripheral acuity. + +![A screenshot of a blank app window in visionOS. A series of concentric circles overlay the image, conveying 30-, 60-, and 90-degree fields of view.](https://docs-assets.developer.apple.com/published/88086621da558b375ed5ef8ea0002283/visionos-field-of-view-layout%402x.png) + +Important + +The system doesnโ€™t provide information about a personโ€™s field of view. + +**Center important content within the field of view.** By default, visionOS launches an app directly in front of people, placing it within their field of view. In an immersive experience, you can help people keep their attention on important content by keeping it centered and not displaying distracting motion or bright, high-contrast objects in the periphery. + + * Upright viewing + * Angled viewing + + + +Video with custom controls. + +Content description: An animation of a person wearing Apple Vision Pro and sitting upright in a chair. The person is directly facing a square that represents an app window in visionOS that's centered in the person's field of view. A dotted line animates from the person's eyes to the center of the window. + +Play + +Video with custom controls. + +Content description: An animation of a person wearing Apple Vision Pro and reclining in a chair. The person is looking at a square that represents an app window in visionOS. The app window is positioned a short distance from the person, is raised in the air, and is tilted toward the person so it's centered within the person's field of view. A dotted line animates from the person's eyes to the center of the window. + +Play + +**Avoid anchoring content to the wearerโ€™s head.** Although you generally want your app to stay within the field of view, anchoring content so that it remains statically in front of someone can make them feel stuck, confined, and uncomfortable, especially if the content obscures a lot of passthrough and decreases the apparent stability of their surroundings. Instead, anchor content in peopleโ€™s space, giving them the freedom to look around naturally and view different objects in different locations. + +## [Depth](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Depth) + +People rely on visual cues like distance, occlusion, and shadow to perceive depth and make sense of their surroundings. On Apple Vision Pro, the system automatically uses visual effects like color temperature, reflections, and shadow to help people perceive the depth of virtual content. When people move a virtual object in space โ€” or when they change their position relative to that object โ€” the visual effects change the objectโ€™s apparent depth, making the experience feel more lifelike. + +Because people can view your content from any angle, incorporating small amounts of depth throughout your interface โ€” even in standard windows โ€” can help it look more natural. When you use SwiftUI, the system adds visual effects to views in a 2D window, making them appear to have depth. For developer guidance, see [Adding 3D content to your app](https://developer.apple.com/documentation/visionOS/adding-3d-content-to-your-app). + +![A screenshot of a 2D Notes window in visionOS. A note titled Nature Walks is open on the trailing side of the view, with sketches of leaves accompanied by handwritten text descriptions.](https://docs-assets.developer.apple.com/published/2b07a7f22124deaea6c2ce31a93d8833/visionos-spatial-layout-2d-window%402x.png) + +If you need to present content with additional depth, you use RealityKit to create a 3D object (for developer guidance, see [RealityKit](https://developer.apple.com/documentation/RealityKit)). You can display the 3D object anywhere, or you can use a _volume_ , which is a component that displays 3D content. A volume is similar to a window, but without a visible frame. For guidance, see [visionOS volumes](https://developer.apple.com/design/human-interface-guidelines/windows#visionOS-volumes). + +Video with custom controls. + +Content description: A recording showing a 3D model of a satellite within a visionOS volume. As the viewer approaches the satellite and manipulates its orientation, light reflections adjust based on the position of the viewer and angle of the satellite. + +Play + +**Provide visual cues that accurately communicate the depth of your content.** If visual cues are missing or they conflict with a personโ€™s real-world experience, people can experience visual discomfort. + +**Use depth to communicate hierarchy.** Depth helps an object appear to stand out from surrounding content, making it more noticeable. People also tend to notice changes in depth: for example, when a sheet appears over a window, the window recedes along the z-axis, allowing the sheet to come forward and become visually prominent. + +**In general, avoid adding depth to text.** Text that appears to hover above its background is difficult to read, which slows people down and can sometimes cause vision discomfort. + +**Make sure depth adds value.** In general, you want to use depth to clarify and delight โ€” you donโ€™t need to use it everywhere. As you add depth to your design, think about the size and relative importance of objects. Depth is great for visually separating large, important elements in your app, like making a tab bar or toolbar stand out from a window, but it may not work as well on small objects. For example, using depth to make a buttonโ€™s symbol stand out from its background can make the button less legible and harder to use. Also review how often you use different depths throughout your app. People need to refocus their eyes to perceive each difference in depth, and doing so too often or quickly can be tiring. + +## [Scale](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Scale) + +visionOS defines two types of scale to preserve the appearance of depth while optimizing usability. + +_Dynamic scale_ helps content remain comfortably legible and interactive regardless of its proximity to people. Specifically, visionOS automatically increases a windowโ€™s scale as it moves away from the wearer and decreases it as the window moves closer, making the window appear to maintain the same size at all distances. + +Video with custom controls. + +Content description: An animation that shows a square representing an app window in a 3D space. The square animates to move back along its plane from its initial position. As it moves, it dynamically grows in size. A frame representing the original position remains visible for comparison. After the movement is complete, the entire environment rotates to convey that, from the viewer's angle, the window always remains the same size. + +Play + +_Fixed scale_ means that an object maintains the same scale regardless of its proximity to people. A fixed-scale object appears smaller when it moves farther from the viewer along the z-axis, similar to the way an object in a personโ€™s physical surroundings looks smaller when itโ€™s far away than it does when itโ€™s close up. + +Video with custom controls. + +Content description: An animation that shows a square representing an app window in a 3D space. The square animates to move back along its plane from its initial position. As it moves, it becomes smaller. A frame representing the original position remains visible for comparison. After the movement is complete, the entire environment rotates to convey that, from the viewer's angle, the window appears to have receded into the distance. + +Play + +To support dynamic scaling and the appearance of depth, visionOS defines a point as an angle, in contrast to other platforms, which define a point as a number of pixels that can vary with the [resolution](https://developer.apple.com/design/human-interface-guidelines/images#Resolution) of a 2D display. + +**Consider using fixed scale when you want a virtual object to look exactly like a physical object.** For example, you might want to maintain the life-size scale of a product you offer so it can look more realistic when people view it in their space. Because interactive content needs to scale to maintain usability as it gets closer or farther away, prefer applying fixed scale sparingly, reserving it for noninteractive objects that need it. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Best-practices) + +**Avoid displaying too many windows.** Too many windows can obscure peopleโ€™s surroundings, making them feel overwhelmed, constricted, and even uncomfortable. It can also make it cumbersome for people to relocate an app because it means moving a lot of windows. + +**Prioritize standard, indirect gestures.** People can make an _indirect_ gesture without moving their hand into their field of view. In contrast, making a _direct_ gesture requires people to touch the virtual object with their finger, which can be tiring, especially when the object is positioned at or above their line of sight. In visionOS, people use indirect gestures to perform the standard gestures they already know. When you prioritize indirect gestures, people can use them to interact with any object they look at, whatever its distance. If you support direct gestures, consider reserving them for nearby objects that invite close inspection or manipulation for short periods of time. For guidance, see [Gestures > visionOS](https://developer.apple.com/design/human-interface-guidelines/gestures#visionOS). + +**Rely on the Digital Crown to help people recenter windows in their field of view.** When people move or turn their head, content might no longer appear where they want it to. If this happens, people can press the [Digital Crown](https://developer.apple.com/design/human-interface-guidelines/digital-crown) when they want to recenter content in front of them. Your app doesnโ€™t need to do anything to support this action. + +**Include enough space around interactive components to make them easy for people to look at.** When people look at an interactive element, visionOS displays a visual hover effect that helps them confirm the element is the one they want. Itโ€™s crucial to include enough space around an interactive component so that looking at it is easy and comfortable, while preventing the hover effect from crowding other content. For example, place multiple, regular-size [buttons](https://developer.apple.com/design/human-interface-guidelines/buttons#visionOS) so their centers are at least 60 points apart, leaving 16 points or more of space between them. Also, donโ€™t let controls overlap other interactive elements or views, because doing so can make selecting a single element difficult. + +**Let people use your app with minimal or no physical movement.** Unless some physical movement is essential to your experience, help everyone enjoy it while remaining stationary. + +**Use the floor to help you place a large immersive experience.** If your immersive experience includes content that extends up from the floor, place it using a flat horizontal plane. Aligning this plane with the floor can help it blend seamlessly with peopleโ€™s surroundings and provide a more intuitive experience. + +To learn more about windows and volumes in visionOS, see [Windows > visionOS](https://developer.apple.com/design/human-interface-guidelines/windows#visionOS); for guidance on laying content within a window, see [Layout > visionOS](https://developer.apple.com/design/human-interface-guidelines/layout#visionOS). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Platform-considerations) + + _Not supported in iOS, iPadOS, macOS, tvOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Related) + +[Eyes](https://developer.apple.com/design/human-interface-guidelines/eyes) + +[Layout](https://developer.apple.com/design/human-interface-guidelines/layout) + +[Immersive experiences](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Developer-documentation) + +[Presenting windows and spaces](https://developer.apple.com/documentation/visionOS/presenting-windows-and-spaces) โ€” visionOS + +[Positioning and sizing windows](https://developer.apple.com/documentation/visionOS/positioning-and-sizing-windows) โ€” visionOS + +[Adding 3D content to your app](https://developer.apple.com/documentation/visionOS/adding-3d-content-to-your-app) โ€” visionOS + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/2EFC2AAC-0656-4A18-B4C9-8E23CCD81E90/9989_wide_250x141_1x.jpg) Meet SwiftUI spatial layout ](https://developer.apple.com/videos/play/wwdc2025/273) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/15489B11-8744-483D-AD38-EF78D8962FF4/8126_wide_250x141_1x.jpg) Principles of spatial design ](https://developer.apple.com/videos/play/wwdc2023/10072) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/38E4EE32-29B5-4478-B8B6-35B8ACA67B16/8130_wide_250x141_1x.jpg) Design for spatial user interfaces ](https://developer.apple.com/videos/play/wwdc2023/10076) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Change-log) + +Date| Changes +---|--- +March 29, 2024| Emphasized the importance of keeping interactive elements from overlapping each other. +June 21, 2023| New page. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/typography.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/typography.md new file mode 100644 index 00000000..2f919329 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/typography.md @@ -0,0 +1,1146 @@ +--- +title: "Typography | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/typography + +# Typography + +Your typographic choices can help you display legible text, convey an information hierarchy, communicate important content, and express your brand or style. + +![A sketch of a small letter A to the left of a large letter A, suggesting the use of typography to convey hierarchical information. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/d90940d120149af7220e4fedfd1c10bd/foundations-typography-intro%402x.png) + +## [Ensuring legibility](https://developer.apple.com/design/human-interface-guidelines/typography#Ensuring-legibility) + +**Use font sizes that most people can read easily.** People need to be able to read your content at various viewing distances and under a variety of conditions. Follow the recommended default and minimum text sizes for each platform โ€” for both custom and system fonts โ€” to ensure your text is legible on all devices. Keep in mind that font weight can also impact how easy text is to read. If you use a custom font with a thin weight, aim for larger than the recommended sizes to increase legibility. + +Platform| Default size| Minimum size +---|---|--- +iOS, iPadOS| 17 pt| 11 pt +macOS| 13 pt| 10 pt +tvOS| 29 pt| 23 pt +visionOS| 17 pt| 12 pt +watchOS| 16 pt| 12 pt + +**Test legibility in different contexts.** For example, you need to test game text for legibility on each platform on which your game runs. If testing shows that some of your text is difficult to read, consider using a larger type size, increasing contrast by modifying the text or background colors, or using typefaces designed for optimized legibility, like the system fonts. + +![A screenshot that shows a game running on iPhone in landscape. A name appears above each of 3 plants and a status message appears in a rounded rectangle in the top-right corner. All text uses a size that's too small, and the 3 plant names don't have visible backgrounds.](https://docs-assets.developer.apple.com/published/aaf334356178859f6e86eb42913c53dd/game-typography-incorrect%402x.png) + +Testing a game on a new platform can show where text is hard to read. + +![A screenshot that shows a game running on iPhone in landscape. A name appears within a shaded lozenge shape above each of 3 plants and a status message appears in a rounded rectangle in the top-right corner. All text uses a size that's at least the recommended minimum.](https://docs-assets.developer.apple.com/published/4c38de72c94767cbb6bd7763720ef281/game-typography-correct%402x.png) + +Increasing text size and adding visible background shapes can help make text easier to read. + +**In general, avoid light font weights.** For example, if youโ€™re using system-provided fonts, prefer Regular, Medium, Semibold, or Bold font weights, and avoid Ultralight, Thin, and Light font weights, which can be difficult to see, especially when text is small. + +## [Conveying hierarchy](https://developer.apple.com/design/human-interface-guidelines/typography#Conveying-hierarchy) + +**Adjust font weight, size, and color as needed to emphasize important information and help people visualize hierarchy.** Be sure to maintain the relative hierarchy and visual distinction of text elements when people adjust text sizes. + +**Minimize the number of typefaces you use, even in a highly customized interface.** Mixing too many different typefaces can obscure your information hierarchy and hinder readability, in addition to making an interface feel internally inconsistent or poorly designed. + +**Prioritize important content when responding to text-size changes.** Not all content is equally important. When someone chooses a larger text size, they typically want to make the content they care about easier to read; they donโ€™t always want to increase the size of every word on the screen. For example, when people increase text size to read the content in a tabbed window, they donโ€™t expect the tab titles to increase in size. Similarly, in a game, people are often more interested in a characterโ€™s dialog than in transient hit-damage values. + +## [Using system fonts](https://developer.apple.com/design/human-interface-guidelines/typography#Using-system-fonts) + +Apple provides two typeface families that support an extensive range of weights, sizes, styles, and languages. + +**San Francisco (SF)** is a sans serif typeface family that includes the SF Pro, SF Compact, SF Arabic, SF Armenian, SF Georgian, SF Hebrew, and SF Mono variants. + +![The phrase 'The quick brown fox jumps over the lazy dog.' shown in the San Francisco Pro font.](https://docs-assets.developer.apple.com/published/e270b0f4e91f523bb7372a39447ad4e4/typography-sanfrancisco%402x.png) + +The system also offers SF Pro, SF Compact, SF Arabic, SF Armenian, SF Georgian, and SF Hebrew in rounded variants you can use to coordinate text with the appearance of soft or rounded UI elements, or to provide an alternative typographic voice. + +**New York (NY)** is a serif typeface family designed to work well by itself and alongside the SF fonts. + +![The phrase 'The quick brown fox jumps over the lazy dog.' shown in the New York font.](https://docs-assets.developer.apple.com/published/8dcb4d6f97b97a957a0d73e4ee85730c/typography-new-york%402x.png) + +You can download the San Francisco and New York fonts [here](https://developer.apple.com/fonts/). + +The system provides the SF and NY fonts in the _variable_ font format, which combines different font styles together in one file, and supports interpolation between styles to create intermediate ones. + +Note + +Variable fonts support _optical sizing_ , which refers to the adjustment of different typographic designs to fit different sizes. On all platforms, the system fonts support _dynamic optical sizes_ , which merge discrete optical sizes (like Text and Display) and weights into a single, continuous design, letting the system interpolate each glyph or letterform to produce a structure thatโ€™s precisely adapted to the point size. With dynamic optical sizes, you donโ€™t need to use discrete optical sizes unless youโ€™re working with a design tool that doesnโ€™t support all the features of the variable font format. + +To help you define visual hierarchies and create clear and legible designs in many different sizes and contexts, the system fonts are available in a variety of weights, ranging from Ultralight to Black, and โ€” in the case of SF โ€” several widths, including Condensed and Expanded. Because SF Symbols use equivalent weights, you can achieve precise weight matching between symbols and adjacent text, regardless of the size or style you choose. + +![The word 'text' shown in the SF Pro font, repeated in two rows of nine columns each. The rows show upright and italic styles, and the columns show font weights ranging from ultralight to black.](https://docs-assets.developer.apple.com/published/8b07ec795d9ad16c787edb0030018a09/font-weight-sf-pro%402x.png) + +Note + +[SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) provides a comprehensive library of symbols that integrate seamlessly with the San Francisco system font, automatically aligning with text in all weights and sizes. Consider using symbols when you need to convey a concept or depict an object, especially within text. + +The system defines a set of typographic attributes โ€” called text styles โ€” that work with both typeface families. A _text style_ specifies a combination of font weight, point size, and leading values for each text size. For example, the _body_ text style uses values that support a comfortable reading experience over multiple lines of text, while the _headline_ style assigns a font size and weight that help distinguish a heading from surrounding content. Taken together, the text styles form a typographic hierarchy you can use to express the different levels of importance in your content. Text styles also allow text to scale proportionately when people change the systemโ€™s text size or make accessibility adjustments, like turning on Larger Text in Accessibility settings. + +**Consider using the built-in text styles.** The system-defined text styles give you a convenient and consistent way to convey your information hierarchy through font size and weight. Using text styles with the system fonts also ensures support for Dynamic Type and larger accessibility type sizes (where available), which let people choose the text size that works for them. For guidance, see [Supporting Dynamic Type](https://developer.apple.com/design/human-interface-guidelines/typography#Supporting-Dynamic-Type). + +**Modify the built-in text styles if necessary.** System APIs define font adjustments โ€” called _symbolic traits_ โ€” that let you modify some aspects of a text style. For example, the bold trait adds weight to text, letting you create another level of hierarchy. You can also use symbolic traits to adjust leading if you need to improve readability or conserve space. For example, when you display text in wide columns or long passages, more space between lines (_loose leading_) can make it easier for people to keep their place while moving from one line to the next. Conversely, if you need to display multiple lines of text in an area where height is constrained โ€” for example, in a list row โ€” decreasing the space between lines (_tight leading_) can help the text fit well. If you need to display three or more lines of text, avoid tight leading even in areas where height is limited. For developer guidance, see [`leading(_:)`](https://developer.apple.com/documentation/SwiftUI/Font/leading\(_:\)). + +Developer note + +You can use the constants defined in [`Font.Design`](https://developer.apple.com/documentation/SwiftUI/Font/Design) to access all system fonts โ€” donโ€™t embed system fonts in your app or game. For example, use [`Font.Design.default`](https://developer.apple.com/documentation/SwiftUI/Font/Design/default) to get the system font on all platforms; use [`Font.Design.serif`](https://developer.apple.com/documentation/SwiftUI/Font/Design/serif) to get the New York font. + +**If necessary, adjust tracking in interface mockups.** In a running app, the system font dynamically adjusts tracking at every point size. To produce an accurate interface mockup of an interface that uses the variable system fonts, you donโ€™t have to choose a discrete optical size at certain point sizes, but you might need to adjust the tracking. For guidance, see [Tracking values](https://developer.apple.com/design/human-interface-guidelines/typography#Tracking-values). + +## [Using custom fonts](https://developer.apple.com/design/human-interface-guidelines/typography#Using-custom-fonts) + +**Make sure custom fonts are legible.** People need to be able to read your custom font easily at various viewing distances and under a variety of conditions. While using a custom font, be guided by the recommended minimum font sizes for various styles and weights in [Specifications](https://developer.apple.com/design/human-interface-guidelines/typography#Specifications). + +**Implement accessibility features for custom fonts.** System fonts automatically support Dynamic Type (where available) and respond when people turn on accessibility features, such as Bold Text. If you use a custom font, make sure it implements the same behaviors. For developer guidance, see [Applying custom fonts to text](https://developer.apple.com/documentation/SwiftUI/Applying-Custom-Fonts-to-Text). In a Unity-based game, you can use [Appleโ€™s Unity plug-ins](https://github.com/apple/unityplugins) to support Dynamic Type. If the plug-in isnโ€™t appropriate for your game, be sure to let players adjust text size in other ways. + +## [Supporting Dynamic Type](https://developer.apple.com/design/human-interface-guidelines/typography#Supporting-Dynamic-Type) + +Dynamic Type is a system-level feature in iOS, iPadOS, tvOS, visionOS, and watchOS that lets people adjust the size of visible text on their device to ensure readability and comfort. For related guidance, see [Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility). + +![A screenshot of a Mail message on iPhone, using the default font size. From the left, the message header displays the sender's contact photo or initials, followed by a two-line layout with the sender name and date on top and the recipient name and attachment glyph on the bottom. The message body contains four lines of text and the address of Muir Woods National Monument.](https://docs-assets.developer.apple.com/published/65fab16931136a1aa542fb71e9ec181b/typography-default-type%402x.png) + +Mail content at the default text size + +![A screenshot of a Mail message on iPhone, using the largest accessibility font size. From the top, the message header displays the sender name on one line, followed by the truncated recipient name on the next line, and the date and attachment glyph on the third line. Below the header and message title, the first line and part of the second line of body text are visible on the screen.](https://docs-assets.developer.apple.com/published/5840a6f168607659543494f5cebe266d/typography-dynamic-type%402x.png) + +Mail content at the largest accessibility text size + +For a list of available Dynamic Type sizes, see [Specifications](https://developer.apple.com/design/human-interface-guidelines/typography#Specifications). You can also download Dynamic Type size tables in the [Apple Design Resources](https://developer.apple.com/design/resources/) for each platform. + +For developer guidance, see [Text input and output](https://developer.apple.com/documentation/SwiftUI/Text-input-and-output). To support Dynamic Type in Unity-based games, use [Appleโ€™s Unity plug-ins](https://github.com/apple/unityplugins). + +**Make sure your appโ€™s layout adapts to all font sizes.** Verify that your design scales, and that text and glyphs are legible at all font sizes. On iPhone or iPad, turn on Larger Accessibility Text Sizes in Settings > Accessibility > Display & Text Size > Larger Text, and confirm that your app remains comfortably readable. + +**Increase the size of meaningful interface icons as font size increases.** If you use interface icons to communicate important information, make sure theyโ€™re easy to view at larger font sizes too. When you use [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols), you get icons that scale automatically with Dynamic Type size changes. + +**Keep text truncation to a minimum as font size increases.** In general, aim to display as much useful text at the largest accessibility font size as you do at the largest standard font size. Avoid truncating text in scrollable regions unless people can open a separate view to read the rest of the content. You can prevent text truncation in a label by configuring it to use as many lines as needed to display a useful amount of text. For developer guidance, see [`numberOfLines`](https://developer.apple.com/documentation/UIKit/UILabel/numberOfLines). + +**Consider adjusting your layout at large font sizes.** When font size increases in a horizontally constrained context, inline items (like glyphs and timestamps) and container boundaries can crowd text and cause truncation or overlapping. To improve readability, consider using a stacked layout where text appears above secondary items. Multicolumn text can also be less readable at large sizes due to horizontal space constraints. Reduce the number of columns when the font size increases to avoid truncation and enhance readability. For developer guidance, see [`isAccessibilityCategory`](https://developer.apple.com/documentation/UIKit/UIContentSizeCategory/isAccessibilityCategory). + +**Maintain a consistent information hierarchy regardless of the current font size.** For example, keep primary elements toward the top of a view even when the font size is very large, so that people donโ€™t lose track of these elements. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/typography#Platform-considerations) + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/typography#iOS-iPadOS) + +SF Pro is the system font in iOS and iPadOS. iOS and iPadOS apps can also use NY. + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/typography#macOS) + +SF Pro is the system font in macOS. NY is available for Mac apps built with Mac Catalyst. macOS doesnโ€™t support Dynamic Type. + +**When necessary, use dynamic system font variants to match the text in standard controls.** Dynamic system font variants give your text the same look and feel of the text that appears in system-provided controls. Use the variants listed below to achieve a look thatโ€™s consistent with other apps on the platform. + +Dynamic font variant| API +---|--- +Control content| [`controlContentFont(ofSize:)`](https://developer.apple.com/documentation/AppKit/NSFont/controlContentFont\(ofSize:\)) +Label| [`labelFont(ofSize:)`](https://developer.apple.com/documentation/AppKit/NSFont/labelFont\(ofSize:\)) +Menu| [`menuFont(ofSize:)`](https://developer.apple.com/documentation/AppKit/NSFont/menuFont\(ofSize:\)) +Menu bar| [`menuBarFont(ofSize:)`](https://developer.apple.com/documentation/AppKit/NSFont/menuBarFont\(ofSize:\)) +Message| [`messageFont(ofSize:)`](https://developer.apple.com/documentation/AppKit/NSFont/messageFont\(ofSize:\)) +Palette| [`paletteFont(ofSize:)`](https://developer.apple.com/documentation/AppKit/NSFont/paletteFont\(ofSize:\)) +Title| [`titleBarFont(ofSize:)`](https://developer.apple.com/documentation/AppKit/NSFont/titleBarFont\(ofSize:\)) +Tool tips| [`toolTipsFont(ofSize:)`](https://developer.apple.com/documentation/AppKit/NSFont/toolTipsFont\(ofSize:\)) +Document text (user)| [`userFont(ofSize:)`](https://developer.apple.com/documentation/AppKit/NSFont/userFont\(ofSize:\)) +Monospaced document text (user fixed pitch)| [`userFixedPitchFont(ofSize:)`](https://developer.apple.com/documentation/AppKit/NSFont/userFixedPitchFont\(ofSize:\)) +Bold system font| [`boldSystemFont(ofSize:)`](https://developer.apple.com/documentation/AppKit/NSFont/boldSystemFont\(ofSize:\)) +System font| [`systemFont(ofSize:)`](https://developer.apple.com/documentation/AppKit/NSFont/systemFont\(ofSize:\)) + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/typography#tvOS) + +SF Pro is the system font in tvOS, and apps can also use NY. + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/typography#visionOS) + +SF Pro is the system font in visionOS. If you use NY, you need to specify the type styles you want. + +visionOS uses bolder versions of the Dynamic Type body and title styles and it introduces Extra Large Title 1 and Extra Large Title 2 for wide, editorial-style layouts. For guidance using vibrancy to indicate hierarchy in text and symbols, see [Materials > visionOS](https://developer.apple.com/design/human-interface-guidelines/materials#visionOS). + +**In general, prefer 2D text.** The more visual depth text characters have, the more difficult they can be to read. Although a small amount of 3D text can provide a fun visual element that draws peopleโ€™s attention, if youโ€™re going to display content that people need to read and understand, prefer using text that has little or no visual depth. + +![A screenshot that shows the correct placement of 2D text on a window in visionOS.](https://docs-assets.developer.apple.com/published/b7eca42cb50603b5ae1630781ce6d4c7/visionos-typography-2d-text-correct%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +![A screenshot that shows the incorrect placement of 3D text on a window in visionOS.](https://docs-assets.developer.apple.com/published/8568cd71b363e427fb91a874b8c30aa8/visionos-typography-3d-text-incorrect%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +**Make sure text looks good and remains legible when people scale it.** Use a text style that makes the text look good at full scale, then test it for legibility at different scales. + +**Maximize the contrast between text and the background of its container.** By default, the system displays text in white, because this color tends to provide a strong contrast with the default system background material, making text easier to read. If you want to use a different text color, be sure to test it in a variety of contexts. + +**If you need to display text thatโ€™s not on a background, consider making it bold to improve legibility.** In this situation, you generally want to avoid adding shadows to increase text contrast. The current space might not include a visual surface on which to cast an accurate shadow, and you canโ€™t predict the size and density of shadow that would work well with a personโ€™s current Environment. + +**Keep text facing people as much as possible.** If you display text thatโ€™s associated with a point in space, such as a label for a 3D object, you generally want to use _billboarding_ โ€” that is, you want the text to face the wearer regardless of how they or the object move. If you donโ€™t rotate text to remain facing the wearer, the text can become impossible to read because people may view it from the side or a highly oblique angle. For example, imagine a virtual lamp that appears to be on a physical desk with a label anchored directly above it. For the text to remain readable, the label needs to rotate around the y-axis as people move around the desk; in other words, the baseline of the text needs to remain perpendicular to the personโ€™s line of sight. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/typography#watchOS) + +SF Compact is the system font in watchOS, and apps can also use NY. In complications, watchOS uses SF Compact Rounded. + +## [Specifications](https://developer.apple.com/design/human-interface-guidelines/typography#Specifications) + +You can display emphasized variants of system text styles using symbolic traits. In SwiftUI, use the [`bold()`](https://developer.apple.com/documentation/SwiftUI/Text/bold\(\)) modifier; in UIKit, use [`traitBold`](https://developer.apple.com/documentation/UIKit/UIFontDescriptor/SymbolicTraits-swift.struct/traitBold) in the [`UIFontDescriptor`](https://developer.apple.com/documentation/UIKit/UIFontDescriptor) API. The emphasized weights can be medium, semibold, bold, or heavy. The following specifications include the emphasized weight for each text style. + +### [iOS, iPadOS Dynamic Type sizes](https://developer.apple.com/design/human-interface-guidelines/typography#iOS-iPadOS-Dynamic-Type-sizes) + + * xSmall + * Small + * Medium + * Large (default) + * xLarge + * xxLarge + * xxxLarge + + + +#### [xSmall](https://developer.apple.com/design/human-interface-guidelines/typography#xSmall) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 31| 38| Bold +Title 1| Regular| 25| 31| Bold +Title 2| Regular| 19| 24| Bold +Title 3| Regular| 17| 22| Semibold +Headline| Semibold| 14| 19| Semibold +Body| Regular| 14| 19| Semibold +Callout| Regular| 13| 18| Semibold +Subhead| Regular| 12| 16| Semibold +Footnote| Regular| 12| 16| Semibold +Caption 1| Regular| 11| 13| Semibold +Caption 2| Regular| 11| 13| Semibold + +Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +#### [Small](https://developer.apple.com/design/human-interface-guidelines/typography#Small) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 32| 39| Bold +Title 1| Regular| 26| 32| Bold +Title 2| Regular| 20| 25| Bold +Title 3| Regular| 18| 23| Semibold +Headline| Semibold| 15| 20| Semibold +Body| Regular| 15| 20| Semibold +Callout| Regular| 14| 19| Semibold +Subhead| Regular| 13| 18| Semibold +Footnote| Regular| 12| 16| Semibold +Caption 1| Regular| 11| 13| Semibold +Caption 2| Regular| 11| 13| Semibold + +Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +#### [Medium](https://developer.apple.com/design/human-interface-guidelines/typography#Medium) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 33| 40| Bold +Title 1| Regular| 27| 33| Bold +Title 2| Regular| 21| 26| Bold +Title 3| Regular| 19| 24| Semibold +Headline| Semibold| 16| 21| Semibold +Body| Regular| 16| 21| Semibold +Callout| Regular| 15| 20| Semibold +Subhead| Regular| 14| 19| Semibold +Footnote| Regular| 12| 16| Semibold +Caption 1| Regular| 11| 13| Semibold +Caption 2| Regular| 11| 13| Semibold + +Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +#### [Large (default)](https://developer.apple.com/design/human-interface-guidelines/typography#Large-default) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 34| 41| Bold +Title 1| Regular| 28| 34| Bold +Title 2| Regular| 22| 28| Bold +Title 3| Regular| 20| 25| Semibold +Headline| Semibold| 17| 22| Semibold +Body| Regular| 17| 22| Semibold +Callout| Regular| 16| 21| Semibold +Subhead| Regular| 15| 20| Semibold +Footnote| Regular| 13| 18| Semibold +Caption 1| Regular| 12| 16| Semibold +Caption 2| Regular| 11| 13| Semibold + +Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +#### [xLarge](https://developer.apple.com/design/human-interface-guidelines/typography#xLarge) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 36| 43| Bold +Title 1| Regular| 30| 37| Bold +Title 2| Regular| 24| 30| Bold +Title 3| Regular| 22| 28| Semibold +Headline| Semibold| 19| 24| Semibold +Body| Regular| 19| 24| Semibold +Callout| Regular| 18| 23| Semibold +Subhead| Regular| 17| 22| Semibold +Footnote| Regular| 15| 20| Semibold +Caption 1| Regular| 14| 19| Semibold +Caption 2| Regular| 13| 18| Semibold + +Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +#### [xxLarge](https://developer.apple.com/design/human-interface-guidelines/typography#xxLarge) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 38| 46| Bold +Title 1| Regular| 32| 39| Bold +Title 2| Regular| 26| 32| Bold +Title 3| Regular| 24| 30| Semibold +Headline| Semibold| 21| 26| Semibold +Body| Regular| 21| 26| Semibold +Callout| Regular| 20| 25| Semibold +Subhead| Regular| 19| 24| Semibold +Footnote| Regular| 17| 22| Semibold +Caption 1| Regular| 16| 21| Semibold +Caption 2| Regular| 15| 20| Semibold + +Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +#### [xxxLarge](https://developer.apple.com/design/human-interface-guidelines/typography#xxxLarge) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 40| 48| Bold +Title 1| Regular| 34| 41| Bold +Title 2| Regular| 28| 34| Bold +Title 3| Regular| 26| 32| Semibold +Headline| Semibold| 23| 29| Semibold +Body| Regular| 23| 29| Semibold +Callout| Regular| 22| 28| Semibold +Subhead| Regular| 21| 28| Semibold +Footnote| Regular| 19| 24| Semibold +Caption 1| Regular| 18| 23| Semibold +Caption 2| Regular| 17| 22| Semibold + +Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +### [iOS, iPadOS larger accessibility type sizes](https://developer.apple.com/design/human-interface-guidelines/typography#iOS-iPadOS-larger-accessibility-type-sizes) + + * AX1 + * AX2 + * AX3 + * AX4 + * AX5 + + + +#### [AX1](https://developer.apple.com/design/human-interface-guidelines/typography#AX1) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 44| 52| Bold +Title 1| Regular| 38| 46| Bold +Title 2| Regular| 34| 41| Bold +Title 3| Regular| 31| 38| Semibold +Headline| Semibold| 28| 34| Semibold +Body| Regular| 28| 34| Semibold +Callout| Regular| 26| 32| Semibold +Subhead| Regular| 25| 31| Semibold +Footnote| Regular| 23| 29| Semibold +Caption 1| Regular| 22| 28| Semibold +Caption 2| Regular| 20| 25| Semibold + +Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +#### [AX2](https://developer.apple.com/design/human-interface-guidelines/typography#AX2) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 48| 57| Bold +Title 1| Regular| 43| 51| Bold +Title 2| Regular| 39| 47| Bold +Title 3| Regular| 37| 44| Semibold +Headline| Semibold| 33| 40| Semibold +Body| Regular| 33| 40| Semibold +Callout| Regular| 32| 39| Semibold +Subhead| Regular| 30| 37| Semibold +Footnote| Regular| 27| 33| Semibold +Caption 1| Regular| 26| 32| Semibold +Caption 2| Regular| 24| 30| Semibold + +Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +#### [AX3](https://developer.apple.com/design/human-interface-guidelines/typography#AX3) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 52| 61| Bold +Title 1| Regular| 48| 57| Bold +Title 2| Regular| 44| 52| Bold +Title 3| Regular| 43| 51| Semibold +Headline| Semibold| 40| 48| Semibold +Body| Regular| 40| 48| Semibold +Callout| Regular| 38| 46| Semibold +Subhead| Regular| 36| 43| Semibold +Footnote| Regular| 33| 40| Semibold +Caption 1| Regular| 32| 39| Semibold +Caption 2| Regular| 29| 35| Semibold + +Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +#### [AX4](https://developer.apple.com/design/human-interface-guidelines/typography#AX4) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 56| 66| Bold +Title 1| Regular| 53| 62| Bold +Title 2| Regular| 50| 59| Bold +Title 3| Regular| 49| 58| Semibold +Headline| Semibold| 47| 56| Semibold +Body| Regular| 47| 56| Semibold +Callout| Regular| 44| 52| Semibold +Subhead| Regular| 42| 50| Semibold +Footnote| Regular| 38| 46| Semibold +Caption 1| Regular| 37| 44| Semibold +Caption 2| Regular| 34| 41| Semibold + +Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +#### [AX5](https://developer.apple.com/design/human-interface-guidelines/typography#AX5) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 60| 70| Bold +Title 1| Regular| 58| 68| Bold +Title 2| Regular| 56| 66| Bold +Title 3| Regular| 55| 65| Semibold +Headline| Semibold| 53| 62| Semibold +Body| Regular| 53| 62| Semibold +Callout| Regular| 51| 60| Semibold +Subhead| Regular| 49| 58| Semibold +Footnote| Regular| 44| 52| Semibold +Caption 1| Regular| 43| 51| Semibold +Caption 2| Regular| 40| 48| Semibold + +Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +### [macOS built-in text styles](https://developer.apple.com/design/human-interface-guidelines/typography#macOS-built-in-text-styles) + +Text style| Weight| Size (points)| Line height (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 26| 32| Bold +Title 1| Regular| 22| 26| Bold +Title 2| Regular| 17| 22| Bold +Title 3| Regular| 15| 20| Semibold +Headline| Bold| 13| 16| Heavy +Body| Regular| 13| 16| Semibold +Callout| Regular| 12| 15| Semibold +Subheadline| Regular| 11| 14| Semibold +Footnote| Regular| 10| 13| Semibold +Caption 1| Regular| 10| 13| Medium +Caption 2| Medium| 10| 13| Semibold + +Point size based on image resolution of 144 ppi for @2x designs. + +### [tvOS built-in text styles](https://developer.apple.com/design/human-interface-guidelines/typography#tvOS-built-in-text-styles) + +Text style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Title 1| Medium| 76| 96| Bold +Title 2| Medium| 57| 66| Bold +Title 3| Medium| 48| 56| Bold +Headline| Medium| 38| 46| Bold +Subtitle 1| Regular| 38| 46| Medium +Callout| Medium| 31| 38| Bold +Body| Medium| 29| 36| Bold +Caption 1| Medium| 25| 32| Bold +Caption 2| Medium| 23| 30| Bold + +Point size based on image resolution of 72 ppi for @1x and 144 ppi for @2x designs. + +### [watchOS Dynamic Type sizes](https://developer.apple.com/design/human-interface-guidelines/typography#watchOS-Dynamic-Type-sizes) + + * xSmall + * Small + * Large + * xLarge + * xxLarge + * xxxLarge + + + +#### [xSmall](https://developer.apple.com/design/human-interface-guidelines/typography#xSmall) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 30| 32.5| Bold +Title 1| Regular| 28| 30.5| Semibold +Title 2| Regular| 24| 26.5| Semibold +Title 3| Regular| 17| 19.5| Semibold +Headline| Semibold| 14| 16.5| Semibold +Body| Regular| 14| 16.5| Semibold +Caption 1| Regular| 13| 15.5| Semibold +Caption 2| Regular| 12| 14.5| Semibold +Footnote 1| Regular| 11| 13.5| Semibold +Footnote 2| Regular| 10| 12.5| Semibold + +#### [Small (default 38mm)](https://developer.apple.com/design/human-interface-guidelines/typography#Small-default-38mm) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 32| 34.5| Bold +Title 1| Regular| 30| 32.5| Semibold +Title 2| Regular| 26| 28.5| Semibold +Title 3| Regular| 18| 20.5| Semibold +Headline| Semibold| 15| 17.5| Semibold +Body| Regular| 15| 17.5| Semibold +Caption 1| Regular| 14| 16.5| Semibold +Caption 2| Regular| 13| 15.5| Semibold +Footnote 1| Regular| 12| 14.5| Semibold +Footnote 2| Regular| 11| 13.5| Semibold + +#### [Large (default 40mm/41mm/42mm)](https://developer.apple.com/design/human-interface-guidelines/typography#Large-default-40mm41mm42mm) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 36| 38.5| Bold +Title 1| Regular| 34| 36.5| Semibold +Title 2| Regular| 27| 30.5| Semibold +Title 3| Regular| 19| 21.5| Semibold +Headline| Semibold| 16| 18.5| Semibold +Body| Regular| 16| 18.5| Semibold +Caption 1| Regular| 15| 17.5| Semibold +Caption 2| Regular| 14| 16.5| Semibold +Footnote 1| Regular| 13| 15.5| Semibold +Footnote 2| Regular| 12| 14.5| Semibold + +#### [xLarge (default 44mm/45mm/49mm)](https://developer.apple.com/design/human-interface-guidelines/typography#xLarge-default-44mm45mm49mm) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 40| 42.5| Bold +Title 1| Regular| 38| 40.5| Semibold +Title 2| Regular| 30| 32.5| Semibold +Title 3| Regular| 20| 22.5| Semibold +Headline| Semibold| 17| 19.5| Semibold +Body| Regular| 17| 19.5| Semibold +Caption 1| Regular| 16| 18.5| Semibold +Caption 2| Regular| 15| 17.5| Semibold +Footnote 1| Regular| 14| 16.5| Semibold +Footnote 2| Regular| 13| 15.5| Semibold + +#### [xxLarge](https://developer.apple.com/design/human-interface-guidelines/typography#xxLarge) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 41| 43.5| Bold +Title 1| Regular| 39| 41.5| Semibold +Title 2| Regular| 31| 33.5| Semibold +Title 3| Regular| 21| 23.5| Semibold +Headline| Semibold| 18| 20.5| Semibold +Body| Regular| 18| 20.5| Semibold +Caption 1| Regular| 17| 19.5| Semibold +Caption 2| Regular| 15| 18.5| Semibold +Footnote 1| Regular| 15| 17.5| Semibold +Footnote 2| Regular| 14| 16.5| Semibold + +#### [xxxLarge](https://developer.apple.com/design/human-interface-guidelines/typography#xxxLarge) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 42| 44.5| Bold +Title 1| Regular| 40| 42.5| Semibold +Title 2| Regular| 32| 34.5| Semibold +Title 3| Regular| 22| 24.5| Semibold +Headline| Semibold| 19| 21.5| Semibold +Body| Regular| 19| 21.5| Semibold +Caption 1| Regular| 18| 20.5| Semibold +Caption 2| Regular| 17| 19.5| Semibold +Footnote 1| Regular| 16| 18.5| Semibold +Footnote 2| Regular| 15| 17.5| Semibold + +### [watchOS larger accessibility type sizes](https://developer.apple.com/design/human-interface-guidelines/typography#watchOS-larger-accessibility-type-sizes) + + * AX1 + * AX2 + * AX3 + + + +#### [AX1](https://developer.apple.com/design/human-interface-guidelines/typography#AX1) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 44| 46.5| Bold +Title 1| Regular| 42| 44.5| Semibold +Title 2| Regular| 34| 41| Semibold +Title 3| Regular| 24| 26.5| Semibold +Headline| Semibold| 21| 23.5| Semibold +Body| Regular| 21| 23.5| Semibold +Caption 1| Regular| 18| 20.5| Semibold +Caption 2| Regular| 17| 19.5| Semibold +Footnote 1| Regular| 16| 18.5| Semibold +Footnote 2| Regular| 15| 17.5| Semibold + +#### [AX2](https://developer.apple.com/design/human-interface-guidelines/typography#AX2) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 45| 47.5| Bold +Title 1| Regular| 43| 46| Semibold +Title 2| Regular| 35| 37.5| Semibold +Title 3| Regular| 25| 27.5| Semibold +Headline| Semibold| 22| 24.5| Semibold +Body| Regular| 22| 24.5| Semibold +Caption 1| Regular| 19| 21.5| Semibold +Caption 2| Regular| 18| 20.5| Semibold +Footnote 1| Regular| 17| 19.5| Semibold +Footnote 2| Regular| 16| 17.5| Semibold + +#### [AX3](https://developer.apple.com/design/human-interface-guidelines/typography#AX3) + +Style| Weight| Size (points)| Leading (points)| Emphasized weight +---|---|---|---|--- +Large Title| Regular| 46| 48.5| Bold +Title 1| Regular| 44| 47| Semibold +Title 2| Regular| 36| 38.5| Semibold +Title 3| Regular| 26| 28.5| Semibold +Headline| Semibold| 23| 25.5| Semibold +Body| Regular| 23| 25.5| Semibold +Caption 1| Regular| 20| 22.5| Semibold +Caption 2| Regular| 19| 21.5| Semibold +Footnote 1| Regular| 18| 20.5| Semibold +Footnote 2| Regular| 17| 19.5| Semibold + +### [Tracking values](https://developer.apple.com/design/human-interface-guidelines/typography#Tracking-values) + +#### [iOS, iPadOS, visionOS tracking values](https://developer.apple.com/design/human-interface-guidelines/typography#iOS-iPadOS-visionOS-tracking-values) + + * SF Pro + * SF Pro Rounded + * New York + + + +#### [SF Pro](https://developer.apple.com/design/human-interface-guidelines/typography#SF-Pro) + +Size (points)| Tracking (1/1000 em)| Tracking (points) +---|---|--- +6| +41| +0.24 +7| +34| +0.23 +8| +26| +0.21 +9| +19| +0.17 +10| +12| +0.12 +11| +6| +0.06 +12| 0| 0.0 +13| -6| -0.08 +14| -11| -0.15 +15| -16| -0.23 +16| -20| -0.31 +17| -26| -0.43 +18| -25| -0.44 +19| -24| -0.45 +20| -23| -0.45 +21| -18| -0.36 +22| -12| -0.26 +23| -4| -0.10 +24| +3| +0.07 +25| +6| +0.15 +26| +8| +0.22 +27| +11| +0.29 +28| +14| +0.38 +29| +14| +0.40 +30| +14| +0.40 +31| +13| +0.39 +32| +13| +0.41 +33| +12| +0.40 +34| +12| +0.40 +35| +11| +0.38 +36| +10| +0.37 +37| +10| +0.36 +38| +10| +0.37 +39| +10| +0.38 +40| +10| +0.37 +41| +9| +0.36 +42| +9| +0.37 +43| +9| +0.38 +44| +8| +0.37 +45| +8| +0.35 +46| +8| +0.36 +47| +8| +0.37 +48| +8| +0.35 +49| +7| +0.33 +50| +7| +0.34 +51| +7| +0.35 +52| +6| +0.33 +53| +6| +0.31 +54| +6| +0.32 +56| +6| +0.30 +58| +5| +0.28 +60| +4| +0.26 +62| +4| +0.24 +64| +4| +0.22 +66| +3| +0.19 +68| +2| +0.17 +70| +2| +0.14 +72| +2| +0.14 +76| +1| +0.07 +80| 0| 0 +84| 0| 0 +88| 0| 0 +92| 0| 0 +96| 0| 0 + +Not all apps express tracking values as 1/1000 em. Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +#### [SF Pro Rounded](https://developer.apple.com/design/human-interface-guidelines/typography#SF-Pro-Rounded) + +Size (points)| Tracking (1/1000 em)| Tracking (points) +---|---|--- +6| +87| +0.51 +7| +80| +0.54 +8| +72| +0.57 +9| +65| +0.57 +10| +58| +0.57 +11| +52| +0.56 +12| +46| +0.54 +13| +40| +0.51 +14| +35| +0.48 +15| +30| +0.44 +16| +26| +0.41 +17| +22| +0.37 +18| +21| +0.37 +19| +20| +0.37 +20| +18| +0.36 +21| +17| +0.35 +22| +16| +0.34 +23| +16| +0.35 +24| +15| +0.35 +25| +14| +0.35 +26| +14| +0.36 +27| +14| +0.36 +28| +13| +0.36 +29| +13| +0.37 +30| +12| +0.37 +31| +12| +0.36 +32| +12| +0.38 +33| +12| +0.39 +34| +12| +0.38 +35| +11| +0.38 +36| +11| +0.39 +37| +10| +0.38 +38| +10| +0.39 +39| +10| +0.38 +40| +10| +0.39 +41| +10| +0.38 +42| +10| +0.39 +43| +9| +0.38 +44| +8| +0.37 +45| +8| +0.37 +46| +8| +0.36 +47| +8| +0.37 +48| +8| +0.35 +49| +8| +0.36 +50| +7| +0.34 +51| +6| +0.32 +52| +6| +0.33 +53| +6| +0.31 +54| +6| +0.32 +56| +6| +0.30 +58| +4| +0.25 +60| +4| +0.23 +62| +4| +0.21 +64| +3| +0.19 +66| +2| +0.16 +68| +2| +0.13 +70| +2| +0.14 +72| +2| +0.11 +76| +1| +0.07 +80| 0| 0.00 +84| 0| 0.00 +88| 0| 0.00 +92| 0| 0.00 +96| 0| 0.00 + +Not all apps express tracking values as 1/1000 em. Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +#### [New York](https://developer.apple.com/design/human-interface-guidelines/typography#New-York) + +Size (points)| Tracking (1/1000 em)| Tracking (points) +---|---|--- +6| +40| +0.23 +7| +32| +0.22 +8| +25| +0.20 +9| +20| +0.18 +10| +16| +0.15 +11| +11| +.12 +12| +6| +0.07 +13| +4| +0.05 +14| +2| +0.03 +15| +0| +0.00 +16| -2| -0.03 +17| -4| -0.07 +18| -6| -0.11 +19| -8| -0.15 +20| -10| -0.20 +21| -10| -0.21 +22| -10| -0.23 +23| -11| -0.25 +24| -11| -0.26 +25| -11| -0.27 +26| -12| -0.29 +27| -12| -0.32 +28| -12| -0.33 +29| -12| -0.34 +30| -12| -0.37 +31| -13| -0.39 +32| -13| -0.41 +33| -13| -0.42 +34| -14| -0.45 +35| -14| -0.48 +36| -14| -0.49 +38| -14| -0.52 +40| -14| -0.55 +42| -14| -0.57 +44| -14| -0.62 +46| -14| -0.65 +48| -14| -0.68 +50| -14| -0.71 +52| -14| -0.74 +54| -15| -0.79 +58| -15| -0.85 +62| -15| -0.91 +66| -15| -0.97 +70| -16| -1.06 +72| -16| -1.09 +80| -16| -1.21 +88| -16| -1.33 +96| -16| -1.50 +100| -16| -1.56 +120| -16| -1.88 +140| -16| -2.26 +160| -16| -2.58 +180| -17| -2.99 +200| -17| -3.32 +220| -18| -3.76 +240| -18| -4.22 +260| -18| -4.57 + +Not all apps express tracking values as 1/1000 em. Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +#### [macOS tracking values](https://developer.apple.com/design/human-interface-guidelines/typography#macOS-tracking-values) + +Size (points)| Tracking (1/1000 em)| Tracking (points) +---|---|--- +6| +41| +0.24 +7| +34| +0.23 +8| +26| +0.21 +9| +19| +0.17 +10| +12| +0.12 +11| +6| +0.06 +12| 0| 0.0 +13| -6| -0.08 +14| -11| -0.15 +15| -16| -0.23 +16| -20| -0.31 +17| -26| -0.43 +18| -25| -0.44 +19| -24| -0.45 +20| -23| -0.45 +21| -18| -0.36 +22| -12| -0.26 +23| -4| -0.10 +24| +3| +0.07 +25| +6| +0.15 +26| +8| +0.22 +27| +11| +0.29 +28| +14| +0.38 +29| +14| +0.40 +30| +14| +0.40 +31| +13| +0.39 +32| +13| +0.41 +33| +12| +0.40 +34| +12| +0.40 +35| +11| +0.38 +36| +10| +0.37 +37| +10| +0.36 +38| +10| +0.37 +39| +10| +0.38 +40| +10| +0.37 +41| +9| +0.36 +42| +9| +0.37 +43| +9| +0.38 +44| +8| +0.37 +45| +8| +0.35 +46| +8| +0.36 +47| +8| +0.37 +48| +8| +0.35 +49| +7| +0.33 +50| +7| +0.34 +51| +7| +0.35 +52| +6| +0.31 +53| +6| +0.33 +54| +6| +0.32 +56| +6| +0.30 +58| +5| +0.28 +60| +4| +0.26 +62| +4| +0.24 +64| +4| +0.22 +66| +3| +0.19 +68| +2| +0.17 +70| +2| +0.14 +72| +2| +0.14 +76| +1| +0.07 +80| 0| 0 +84| 0| 0 +88| 0| 0 +92| 0| 0 +96| 0| 0 + +Not all apps express tracking values as 1/1000 em. Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +#### [tvOS tracking values](https://developer.apple.com/design/human-interface-guidelines/typography#tvOS-tracking-values) + +Size (points)| Tracking (1/1000 em)| Tracking (points) +---|---|--- +6| +41| +0.24 +7| +34| +0.23 +8| +26| +0.21 +9| +19| +0.17 +10| +12| +0.12 +11| +6| +0.06 +12| 0| 0.0 +13| -6| -0.08 +14| -11| -0.15 +15| -16| -0.23 +16| -20| -0.31 +17| -26| -0.43 +18| -25| -0.44 +19| -24| -0.45 +20| -23| -0.45 +21| -18| -0.36 +22| -12| -0.26 +23| -4| -0.10 +24| +3| +0.07 +25| +6| +0.15 +26| +8| +0.22 +27| +11| +0.29 +28| +14| +0.38 +29| +14| +0.40 +30| +14| +0.40 +31| +13| +0.39 +32| +13| +0.41 +33| +12| +0.40 +34| +12| +0.40 +35| +11| +0.38 +36| +10| +0.37 +37| +10| +0.36 +38| +10| +0.37 +39| +10| +0.38 +40| +10| +0.37 +41| +9| +0.36 +42| +9| +0.37 +43| +9| +0.38 +44| +8| +0.37 +45| +8| +0.35 +46| +8| +0.36 +47| +8| +0.37 +48| +8| +0.35 +49| +7| +0.33 +50| +7| +0.34 +51| +7| +0.35 +52| +6| +0.31 +53| +6| +0.33 +54| +6| +0.32 +56| +6| +0.30 +58| +5| +0.28 +60| +4| +0.26 +62| +4| +0.24 +64| +4| +0.22 +66| +3| +0.19 +68| +2| +0.17 +70| +2| +0.14 +72| +2| +0.14 +76| +1| +0.07 +80| 0| 0 +84| 0| 0 +88| 0| 0 +92| 0| 0 +96| 0| 0 + +Not all apps express tracking values as 1/1000 em. Point size based on image resolution of 144 ppi for @2x and 216 ppi for @3x designs. + +#### [watchOS tracking values](https://developer.apple.com/design/human-interface-guidelines/typography#watchOS-tracking-values) + + * SF Compact + * SF Compact Rounded + + + +#### [SF Compact](https://developer.apple.com/design/human-interface-guidelines/typography#SF-Compact) + +Size (points)| Tracking (1/1000 em)| Tracking (points) +---|---|--- +6| +50| +0.29 +7| +30| +0.21 +8| +30| +0.23 +9| +30| +0.26 +10| +30| +0.29 +11| +24| +0.26 +12| +20| +0.23 +13| +16| +0.20 +14| +14| +0.19 +15| +4| +0.06 +16| 0| 0.00 +17| -4| -0.07 +18| -8| -0.14 +19| -12| -0.22 +20| 0| 0.00 +21| -2| -0.04 +22| -4| -0.09 +23| -6| -0.13 +24| -8| -0.19 +25| -10| -0.24 +26| -11| -0.28 +27| -12| -0.30 +28| -12| -0.34 +29| -14| -0.38 +30| -14| -0.42 +31| -15| -0.45 +32| -16| -0.50 +33| -17| -0.55 +34| -18| -0.60 +35| -18| -0.63 +36| -20| -0.69 +37| -20| -0.72 +38| -20| -0.74 +39| -20| -0.76 +40| -20| -0.78 +41| -20| -0.80 +42| -20| -0.82 +43| -20| -0.84 +44| -20| -0.86 +45| -20| -0.88 +46| -20| -0.92 +47| -20| -0.94 +48| -20| -0.96 +49| -21| -1.00 +50| -21| -1.03 +51| -21| -1.05 +52| -21| -1.07 +53| -22| -1.11 +54| -22| -1.13 +56| -22| -1.20 +58| -22| -1.25 +60| -22| -1.32 +62| -22| -1.36 +64| -23| -1.44 +66| -24| -1.51 +68| -24| -1.56 +70| -24| -1.64 +72| -24| -1.69 +76| -25| -1.86 +80| -26| -1.99 +84| -26| -2.13 +88| -26| -2.28 +92| -28| -2.47 +96| -28| -2.62 + +Not all apps express tracking values as 1/1000 em. Point size based on image resolution of 144 ppi for @2x designs. + +#### [SF Compact Rounded](https://developer.apple.com/design/human-interface-guidelines/typography#SF-Compact-Rounded) + +Size (points)| Tracking (1/1000 em)| Tracking (points) +---|---|--- +6| +28| +0.16 +7| +26| +0.18 +8| +24| +0.19 +9| +22| +0.19 +10| +20| +0.20 +11| +18| +0.19 +12| +16| +0.19 +13| +14| +0.18 +14| +12| +0.16 +15| +10| +0.15 +16| +8| +0.12 +17| +6| +0.10 +18| +4| +0.07 +19| +2| +0.04 +20| 0| 0.00 +21| -2| -0.04 +22| -4| -0.09 +23| -6| -0.13 +24| -8| -0.19 +25| -10| -0.24 +26| -11| -0.28 +27| -12| -0.30 +28| -12| -0.34 +29| -14| -0.38 +30| -14| -0.42 +31| -15| -0.45 +32| -16| -0.50 +33| -17| -0.55 +34| -18| -0.60 +35| -18| -0.63 +36| -20| -0.69 +37| -20| -0.72 +38| -20| -0.74 +39| -20| -0.76 +40| -20| -0.78 +41| -20| -0.80 +42| -20| -0.82 +43| -20| -0.84 +44| -20| -0.86 +45| -20| -0.88 +46| -20| -0.92 +47| -20| -0.94 +48| -20| -0.96 +49| -21| -1.00 +50| -21| -1.03 +51| -21| -1.05 +52| -21| -1.07 +53| -22| -1.11 +54| -22| -1.13 +56| -22| -1.20 +58| -22| -1.25 +60| -22| -1.32 +62| -22| -1.36 +64| -23| -1.44 +66| -24| -1.51 +68| -24| -1.56 +70| -24| -1.64 +72| -24| -1.69 +76| -25| -1.86 +80| -26| -1.99 +84| -26| -2.13 +88| -26| -2.28 +92| -28| -2.47 +96| -28| -2.62 + +Not all apps express tracking values as 1/1000 em. Point size based on image resolution of 144 ppi for @2x designs. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/typography#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/typography#Related) + +[Fonts for Apple platforms](https://developer.apple.com/fonts/) + +[SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/typography#Developer-documentation) + +[Text input and output](https://developer.apple.com/documentation/SwiftUI/Text-input-and-output) โ€” SwiftUI + +[Text display and fonts](https://developer.apple.com/documentation/UIKit/text-display-and-fonts) โ€” UIKit + +[Fonts](https://developer.apple.com/documentation/AppKit/fonts) โ€” AppKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/typography#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/E4CD1309-352A-444E-96A5-FB5D6BA69725/9167_wide_250x141_1x.jpg) Get started with Dynamic Type ](https://developer.apple.com/videos/play/wwdc2024/10074) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/124/F688D32F-15D2-4082-9505-DB2FA7FE0B0B/6736_wide_250x141_1x.jpg) Meet the expanded San Francisco font family ](https://developer.apple.com/videos/play/wwdc2022/110381) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/49/EA12E035-968D-4B74-AC8D-26CFD8F365A7/3823_wide_250x141_1x.jpg) The details of UI typography ](https://developer.apple.com/videos/play/wwdc2020/10175) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/typography#Change-log) + +Date| Changes +---|--- +December 16, 2025| Added emphasized weights to the Dynamic Type style specifications for each platform. +March 7, 2025| Expanded guidance for Dynamic Type. +June 10, 2024| Added guidance for using Appleโ€™s Unity plug-ins to support Dynamic Type in a Unity-based game and enhanced guidance on billboarding in a visionOS app or game. +September 12, 2023| Added artwork illustrating system font weights, and clarified tvOS specification table descriptions. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/writing.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/writing.md new file mode 100644 index 00000000..af8b233e --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-foundations/references/writing.md @@ -0,0 +1,91 @@ +--- +title: "Writing | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/writing + +# Writing + +The words you choose within your app are an essential part of its user experience. + +![A sketch of a document and pencil, suggesting written content. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/5bd05331c62b850b25ac62f8581b97b6/foundations-writing-intro%402x.png) + +Whether youโ€™re building an onboarding experience, writing an alert, or describing an image for accessibility, designing through the lens of language will help people get the most from your app or game. + +## [Getting started](https://developer.apple.com/design/human-interface-guidelines/writing#Getting-started) + +**Determine your appโ€™s voice.** Think about who youโ€™re talking to, so you can figure out the type of vocabulary youโ€™ll use. What types of words are familiar to people using your app? How do you want people to feel? The words for a banking app might convey trust and stability, for example, while the words in a game might convey excitement and fun. Create a list of common terms, and reference that list to keep your language consistent. Consistent language, along with a voice that reflects your appโ€™s values, helps everything feel more cohesive. + +**Match your tone to the context.** Once youโ€™ve established your appโ€™s voice, vary your tone based on the situation. Consider what people are doing while theyโ€™re using your app โ€” both in the physical world and within the app itself. Are they exercising and reached a goal? Or are they trying to make a payment and received an error? Situational factors affect both what you say and how you display the text on the screen. + +Compare the tone of these two examples from Apple Watch. In the first, the tone is straightforward and direct, reflecting the seriousness of the situation. In the second, the tone is light and congratulatory. + +![A screenshot of a Fall Detection message that reads: it looks like you've taken a hard fall.](https://docs-assets.developer.apple.com/published/6f5dc2b2e349ff901f831b3ba2c109c5/writing-fall-detection-message%402x.png) + +![A screenshot of an Activity message that reads: you set a personal record for your longest daily Move streak, 35 days!](https://docs-assets.developer.apple.com/published/55bb6afa80bc2f2034a1909d7f672bfc/writing-move-streak-message%402x.png) + +**Be clear.** Choose words that are easily understood and convey the right thing. Check each word to be sure it needs to be there. If you can use fewer words, do so. When in doubt, read your writing out loud. + +**Write for everyone.** For your app to be useful for as many people as possible, it needs to speak to as many people as possible. Choose simple, plain language and write with accessibility and localization in mind, avoiding jargon and gendered terminology. For guidance, see [Writing inclusively](https://help.apple.com/applestyleguide/#/apdcb2a65d68) and [VoiceOver](https://developer.apple.com/design/human-interface-guidelines/voiceover); for developer guidance, see [Localization](https://developer.apple.com/documentation/xcode/localization). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/writing#Best-practices) + +**Consider each screenโ€™s purpose**. Pay attention to the order of elements on a screen, and put the most important information first. Format your text to make it easy to read. If youโ€™re trying to convey more than one idea, consider breaking up the text onto multiple screens, and think about the flow of information across those screens. + +**Be action oriented.** Active voice and clear labels help people navigate through your app from one step to the next, or from one screen to another. When labeling buttons and links, itโ€™s almost always best to use a verb. Prioritize clarity and avoid the temptation to be too cute or clever with your labels. For example, just saying โ€œSendโ€ often works better than โ€œLetโ€™s do it!โ€ For links, avoid using โ€œClick hereโ€ in favor of more descriptive words or phrases, such as โ€œLearn more about UX Writing.โ€ This is especially important for people using screen readers to access your app. + +**Build language patterns.** Consistency builds familiarity, helping your app feel cohesive, intuitive, and thoughtfully designed. It also makes writing for your app easier, as you can return to these patterns again and again. + +**Adopt capitalization rules that align with your appโ€™s style, then apply them consistently.** While certain components, like [button labels](https://developer.apple.com/design/human-interface-guidelines/buttons#Content), have specific guidelines, how you format text reflects your appโ€™s voice. Title case is generally considered formal, while sentence case is more casual. Choose a style for each UI element type and use it consistently throughout your app โ€” for example, title case for all alerts or sentence case for all headlines. + +**Give clear guidance and use consistent language throughout processes with multiple steps.** If your app has a flow that spans multiple screens, decide how you want to label the actions that take people from one step to the next. Begin with language like โ€œGet Startedโ€ to indicate youโ€™re starting a flow. You can use the button label to hint at the next step, or use terms like โ€œContinueโ€ or โ€œNext,โ€ but be consistent with what you choose. Make it clear when a flow is complete by using language like โ€œDone.โ€ + +**Use possessive pronouns sparingly.** Possessive pronouns like _my_ and _your_ are often unnecessary to establish context. For example, โ€œFavoritesโ€ conveys the same message as โ€œYour Favorites,โ€ and is more succinct. If you do use possessive pronouns, use them consistently throughout your app, and try not to switch perspectives. Avoid using _we_ altogether because it may be unclear who the โ€œweโ€ in question refers to. This is particularly problematic in error messages like โ€œWeโ€™re having trouble loading this content.โ€ Something like โ€œUnable to load contentโ€ is much clearer. + +**Write for how people use each device.** People may use your app on several types of devices. While your language needs to be consistent across them, think about where it would be helpful to adjust your text to make it suitable for different devices. Make sure you describe gestures correctly on each device โ€” for example, not saying โ€œclickโ€ for a touch device like iPhone or iPad where you mean โ€œtap.โ€ + +Where and how people use a device, its screen size, and its location all affect how you write for your app. iPhone and Apple Watch, for example, offer opportunities for personalization, but their small screens require brevity. TVs, on the other hand, are often in common living spaces, and several people are likely to see anything on the screen, so consider who youโ€™re addressing. Bigger screens also require brevity, as the text must be large for people to see it from a distance. + +**Provide clear next steps on any blank screens.** An empty state, like a completed to-do list or bookmarks folder with nothing in it, can provide a good opportunity to make people feel welcome and educate them about your app. Empty states can also showcase your appโ€™s voice, but make sure that the content is useful and fits the context. An empty screen can be daunting if it isnโ€™t obvious what to do next, so guide people on actions they can take, and give them a button or link to do so if possible. Remember that empty states are usually temporary, so donโ€™t show crucial information that could then disappear. + +**Write clear error messages.** Itโ€™s always best to help people avoid errors. When an error message is necessary, display it as close to the problem as possible, avoid blame, and be clear about what someone can do to fix it. For example, โ€œThat password is too shortโ€ isnโ€™t as helpful as โ€œChoose a password with at least 8 characters.โ€ Remember that errors can be frustrating. Interjections like โ€œoops!โ€ or โ€œuh-ohโ€ are typically unnecessary and can sound insincere. If you find that language alone canโ€™t address an error thatโ€™s likely to affect many people, use that as an opportunity to rethink the interaction. + +**Choose the right delivery method.** There are many ways to get peopleโ€™s attention, whether or not they are actively using your app. When thereโ€™s something you want to communicate, consider the urgency and importance of the message. Think about the context in which someone might see the message, whether it requires immediate action, and how much supporting information someone might need. Choose the correct delivery method, and use a tone appropriate for the situation. For guidance, see [Notifications](https://developer.apple.com/design/human-interface-guidelines/notifications), [Alerts](https://developer.apple.com/design/human-interface-guidelines/alerts), and [Action sheets](https://developer.apple.com/design/human-interface-guidelines/action-sheets). + +**Keep settings labels clear and simple.** Help people easily find the settings they need by labeling them as practically as possible. If the setting label isnโ€™t enough, add an explanation. Describe what it does when turned on, and people can infer the opposite. In the Handwashing Timer setting for Apple Watch, for example, the description explains that a timer can start when youโ€™re washing your hands. It isnโ€™t necessary to tell you that a timer wonโ€™t start when this setting is off. + +![A partial screenshot showing the Handwashing Timer description, which reads: Apple Watch can detect when you're washing your hands and start a 20-second timer.](https://docs-assets.developer.apple.com/published/f368879d77e8dfdff158067bc3888c5f/writing-handwashing-settings%402x.png) + +If you need to direct someone to a setting, provide a direct link or button, rather than trying to describe its location. For guidance, see [Settings](https://developer.apple.com/design/human-interface-guidelines/settings). + +**Show hints in text fields.** If your app allows people to enter their own text, like account or contact information, label all fields clearly, and use hint or placeholder text so people know how to format the information. You can give an example in hint text, like โ€œname@example.com,โ€ or describe the information, such as โ€œYour name.โ€ Show errors right next to the field, and instruct people how to enter the information correctly, rather than scolding them for not following the rules. โ€œUse only letters for your nameโ€ is better than โ€œDonโ€™t use numbers or symbols.โ€ Avoid robotic error messages with no helpful information, like โ€œInvalid name.โ€ For guidance, see [Text fields](https://developer.apple.com/design/human-interface-guidelines/text-fields). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/writing#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/writing#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/writing#Related) + +[Apple Style Guide](https://help.apple.com/applestyleguide/#/) + +[Writing inclusively](https://help.apple.com/applestyleguide/#/apdcb2a65d68) + +[Inclusion](https://developer.apple.com/design/human-interface-guidelines/inclusion) + +[Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility) + +[Color](https://developer.apple.com/design/human-interface-guidelines/color) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/writing#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/B533077E-165E-43D5-98B8-DBF95101C0B2/10001_wide_250x141_1x.jpg) Make a big impact with small writing changes ](https://developer.apple.com/videos/play/wwdc2025/404) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/124/E58B8A59-15C1-4FB4-B61A-23DBA2AF6D28/6530_wide_250x141_1x.jpg) Writing for interfaces ](https://developer.apple.com/videos/play/wwdc2022/10037) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/writing#Change-log) + +Date| Changes +---|--- +December 16, 2025| Clarified guidance on language patterns, and added guidance for possessive pronouns. +February 27, 2023| New page. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/SKILL.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/SKILL.md new file mode 100644 index 00000000..bd6ebbb8 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/SKILL.md @@ -0,0 +1,110 @@ +--- +name: hig-inputs +description: "Check for .claude/apple-design-context.md before asking questions. Use existing context and only ask for information not already covered." +risk: unknown +source: community +date_added: '2026-02-27' +--- + +# Apple HIG: Inputs + +Check for `.claude/apple-design-context.md` before asking questions. Use existing context and only ask for information not already covered. + +## Key Principles + +### General + +1. **Support multiple input methods.** Touch, pointer, keyboard, pencil, voice, eyes, hands, controllers. Design for the inputs available on each platform. On iPadOS, support both touch and pointer; on macOS, both pointer and keyboard. + +2. **Consistent feedback for every input action.** Visible, audible, or haptic response. + +### Gestures + +3. **Standard gestures must behave consistently.** Tap to activate, swipe to scroll/navigate, pinch to zoom, long press for context menus, drag to move. Don't override system gestures (edge swipes for back, Home, notifications). + +4. **Use standard recognizers; keep custom gestures discoverable.** Apple's built-in recognizers handle edge cases and accessibility. If you add non-standard gestures, provide hints or coaching to teach them. + +### Apple Pencil + +5. **Precision drawing, markup, and selection.** Support pressure, tilt, and hover. Distinguish finger from Pencil when appropriate (finger pans, Pencil draws). + +6. **Support Scribble in text fields.** Users expect to write with Pencil in any text input. + +### Keyboards + +7. **Keyboard shortcuts and full navigation.** Standard shortcuts (Cmd+C/V/Z) plus custom ones visible in the iPadOS Command key overlay. Logical tab order. + +8. **Respect the software keyboard.** Adjust layout when keyboard appears. Use keyboard-avoidance APIs. + +### Game Controllers + +9. **MFi controllers with on-screen fallbacks.** Map to extended gamepad profile, sensible defaults, remappable. Always offer touch or keyboard alternatives. + +### Pointer and Trackpad + +10. **Native feel.** Hover effects, pointer shape adaptation, standard cursor behaviors. Two-finger scroll, pinch to zoom, swipe to navigate. + +### Digital Crown + +11. **Primary scrolling and value-adjustment input on watchOS.** Scrolling lists, adjusting values, navigating views. Haptic feedback at detents. + +### Eyes and Spatial (visionOS) + +12. **Look and pinch.** Generous hit targets (eye tracking is less precise than touch). Avoid sustained gaze for activation. Direct hand manipulation in immersive experiences. + +### Focus System + +13. **Critical for tvOS and visionOS.** Predictable focus movement. Every interactive element focusable. Clear visual indicators (scale, highlight, elevation). Logical focus groups. + +### Remotes + +14. **Siri Remote: limited surface.** Touch area for swiping, clickpad for selection, few physical buttons. Keep interactions simple. + +### Motion and Nearby + +15. **Gyroscope, accelerometer, UWB: use judiciously.** Suits gaming, fitness, AR. Not for essential tasks. Provide calibration and reset. For UWB, communicate distance and direction with visual or haptic cues. + +## Reference Index + +| Reference | Topic | Key content | +|---|---|---| +| [gestures.md](references/gestures.md) | Touch gestures | Tap, swipe, pinch, long press, drag, system gestures | +| [apple-pencil-and-scribble.md](references/apple-pencil-and-scribble.md) | Apple Pencil | Precision, pressure, tilt, hover, handwriting | +| [keyboards.md](references/keyboards.md) | Keyboards | Shortcuts, navigation, software keyboard, Command key | +| [game-controls.md](references/game-controls.md) | Game controllers | MFi, extended gamepad, remapping, fallbacks | +| [pointing-devices.md](references/pointing-devices.md) | Pointer/trackpad | Hover, cursor morphing, trackpad gestures | +| [digital-crown.md](references/digital-crown.md) | Digital Crown | Scrolling, value adjustment, haptic detents | +| [eyes.md](references/eyes.md) | Eye tracking | Look and tap, gaze targeting, hit target sizing | +| [spatial-interactions.md](references/spatial-interactions.md) | Spatial input | Hand gestures, direct manipulation, immersive input | +| [focus-and-selection.md](references/focus-and-selection.md) | Focus system | tvOS/visionOS navigation, focus indicators, groups | +| [remotes.md](references/remotes.md) | Remotes | Touch surface, clickpad, simple interactions | +| [gyro-and-accelerometer.md](references/gyro-and-accelerometer.md) | Motion sensors | Gyroscope, accelerometer, calibration, gaming | +| [nearby-interactions.md](references/nearby-interactions.md) | Nearby interactions | U1 chip, directional finding, proximity triggers | +| [camera-control.md](references/camera-control.md) | Camera Control | iPhone camera hardware button, quick launch | + +## Output Format + +1. **Input method recommendations by platform** and how they interact. +2. **Gesture specification table** -- standard and custom gestures with expected behaviors. +3. **Keyboard shortcut recommendations** following system conventions. +4. **Accessibility input alternatives** for VoiceOver, Switch Control, etc. + +## Questions to Ask + +1. Which platforms and input devices? +2. Productivity or casual app? +3. Custom gestures in the design? +4. Game controller support needed? + +## Related Skills + +- **hig-components-status** -- Progress indicators responding to input (pull-to-refresh) +- **hig-components-system** -- System experiences with unique input constraints +- **hig-technologies** -- VoiceOver, Siri voice input, ARKit spatial gesture context + +--- + +*Built by [Raintree Technology](https://raintree.technology) ยท [More developer tools](https://raintree.technology)* + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/apple-pencil-and-scribble.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/apple-pencil-and-scribble.md new file mode 100644 index 00000000..36402fc9 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/apple-pencil-and-scribble.md @@ -0,0 +1,148 @@ +--- +title: "Apple Pencil and Scribble | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble + +# Apple Pencil and Scribble + +Apple Pencil helps make drawing, handwriting, and marking effortless and natural, in addition to performing well as a pointer and UI interaction tool. + +![A sketch of a scribble mark, suggesting drawing with Apple Pencil. The image is overlaid with rectangular and circular grid lines and is tinted purple to subtly reflect the purple in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/48578c745cec42fe322ab69c99575b38/inputs-apple-pencil-and-scribble-intro%402x.png) + +Apple Pencil is a versatile, intuitive tool for iPad apps that offers pixelโ€‘level precision when jotting notes, sketching, painting, marking up documents, and more. Scribble lets people use Apple Pencil to enter text in any text field through fast, private, on-device handwriting recognition. + +For details on Apple Pencil features and compatibility, see [Apple Pencil](https://www.apple.com/apple-pencil/). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Best-practices) + +**Support behaviors people intuitively expect when using a marking instrument.** Most people have a lot of experience with real-world marking tools, and this knowledge informs their expectations when they use Apple Pencil with your app. To provide a delightful experience, think about the ways people interact with nondigital pencils, pens, and other marking instruments, and proactively support actions that people may naturally attempt. For example, people often want to write in the margins of documents or books. + +**Let people choose when to switch between Apple Pencil and finger input.** For example, if your app supports Apple Pencil for marking, also ensure that your appโ€™s controls respond to Apple Pencil so people donโ€™t have to switch to using their finger to activate them. In this scenario, a control that doesnโ€™t support Apple Pencil input might seem to be unresponsive, giving the impression of a malfunction or low battery. (Scribble only supports Apple Pencil input.) + +**Let people make a mark the moment Apple Pencil touches the screen**. You want the experience of putting Apple Pencil to screen to mirror the experience of putting a classic pencil to paper, so itโ€™s essential to avoid requiring people to tap a button or enter a special mode before they can make a mark. + +**Help people express themselves by responding to the way they use Apple Pencil.** Apple Pencil may sense tilt (altitude), force (pressure), orientation (azimuth), and [barrel roll](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Barrel-roll). Use this information to affect the strokes Apple Pencil makes, such as by varying thickness and intensity. When responding to pressure, keep things simple and intuitive. For example, it feels natural to affect continuous properties โ€” such as ink opacity or brush size โ€” by varying the pressure. + +![An illustration of Apple Pencil tilted up from a horizontal line by 45 degrees.](https://docs-assets.developer.apple.com/published/71e341540baa3fa3bd5bdf01a55cc8a8/apple-pencil-altitude%402x.png)Altitude + +![An illustration of Apple Pencil drawing a curved line that increases in thickness as more pressure is applied to the tool.](https://docs-assets.developer.apple.com/published/ce6370f2a90cf23b39ee77f7ba64ff02/apple-pencil-pressure%402x.png)Pressure + +![An illustration of Apple Pencil balancing on its tip at the center of a circle that has degree marks around its circumference. A line from the center of the circle to one of the degree marks indicates the angle at which Apple Pencil is tilted.](https://docs-assets.developer.apple.com/published/e3cd83ae350aac7fe4886903d65ac495/apple-pencil-azimuth%402x.png)Azimuth + +**Provide visual feedback to indicate a direct connection with content.** Make sure Apple Pencil appears to directly and immediately manipulate content it touches onscreen. Avoid letting Apple Pencil appear to initiate seemingly disconnected actions, or affect content on other parts of the screen. + +**Design a great left- and right-handed experience.** Avoid placing controls in locations that may be obscured by either hand. If thereโ€™s a chance controls may become obscured, consider letting people reposition them. + +![An illustration of an iPad app that shows a stack of three circular controls on both side edges. A drawing of a personโ€™s left hand holding an Apple Pencil is shown at the bottom-left corner of the screen, partially obscuring the controls on that side. The controls on the left edge are grayed out to indicate the original position they no longer occupy, while the controls on the right edge are bright to indicate their final position.](https://docs-assets.developer.apple.com/published/386201ad5a8d093d8c72fc4db57978aa/apple-pencil-controls-moved-right%402x.png) + +![An illustration of an iPad app that shows a stack of three circular controls on both side edges. A drawing of a personโ€™s right hand holding an Apple Pencil is shown at the bottom-right corner of the screen, partially obscuring the controls on that side. The controls on the right edge are grayed out to indicate the original position they no longer occupy, while the controls on the left edge are bright to indicate their final position.](https://docs-assets.developer.apple.com/published/6b9182644f4624493d4fbe541186a4dc/apple-pencil-controls-moved-left%402x.png) + +## [Hover](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Hover) + +**Use hover to help people predict what will happen when Apple Pencil touches the screen.** For example, as people hold Apple Pencil above the screen, a hover preview can show the dimensions and color of the mark that the current tool can make. As much as possible, avoid continuously modifying the preview as people move Apple Pencil closer or farther from the screen. A preview that changes according to height is unlikely to clarify the mark Apple Pencil will make, and frequent visual variations can be very distracting to people. + +**Avoid using hover to initiate an action.** Unlike tapping a button or marking the screen, hovering is a relatively imprecise motion that doesnโ€™t require people to think about the actual distance between Apple Pencil and the display. You donโ€™t want people to inadvertently perform an action โ€” especially a destructive action that they might want to undo โ€” just because they hold Apple Pencil near the screen. + +**Prefer showing a preview value thatโ€™s near the middle in a range of dynamic values.** Dynamic properties like opacity or flow can be difficult to depict at the highest or lowest ends of the spectrum. For example, previewing the appearance of a brush mark made with the maximum pressure could occlude the area in which people are marking; in contrast, depicting a mark made with the minimum pressure could be hard for people to detect, making the preview an inaccurate representation of an actual mark or even invisible. + +![An illustration of Apple Pencil hovering slightly above a gray rectangle that represents the screen. A small blue oval beneath the tip represents a preview.](https://docs-assets.developer.apple.com/published/d18ccebb3a51a66f6c6151bc1414d9a1/apple-pencil-preview-small%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration of Apple Pencil hovering slightly above a gray rectangle that represents the screen. A medium blue oval beneath the tip represents a preview.](https://docs-assets.developer.apple.com/published/4c5b24f4381fc1ed83af48f2a7ae3268/apple-pencil-preview-medium%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +![An illustration of Apple Pencil hovering slightly above a gray rectangle that represents the screen. A large blue oval beneath the tip represents a preview.](https://docs-assets.developer.apple.com/published/50d3aa8162579aced9bd752b856b5f6b/apple-pencil-preview-large%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +**Consider using hover to support relevant interactions close to where people are marking.** For example, you might respond to hover by displaying a contextual menu of tool sizes when people perform a gesture like [squeeze](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Squeeze) or press a modifier key on an attached keyboard. Revealing a menu near where people are marking lets them make choices without moving Apple Pencil or their hands to another part of the screen. + +**Prefer showing hover previews for Apple Pencil, not for a pointing device.** Although a pointing device can also respond to hover gestures, it might be confusing to provide the same visual feedback for both devices. If it makes sense in your app, you can restrict your hover preview to Apple Pencil only. For developer guidance, see [Adopting hover support for Apple Pencil](https://developer.apple.com/documentation/UIKit/adopting-hover-support-for-apple-pencil). + +## [Double tap](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Double-tap) + +**Respect peopleโ€™s settings for the double-tap gesture when they make sense in your app.** By default, models of Apple Pencil that support the double-tap gesture respond by toggling between the current tool and the eraser, but people can set the gesture to toggle between the current and previous tool, show and hide the color picker, or do nothing at all. If your app supports these behaviors, let people use their preferred gestures to perform them. If the systemwide double-tap settings donโ€™t make sense in your app, you can still use the gesture to change the interaction mode. For example, a 3D app that offers a mesh-editing tool could use double tap to toggle between the toolโ€™s raise and lower modes. + +**Give people a way to specify custom double-tap behavior if necessary.** If you offer custom double-tap behavior in addition to some or all of the default behaviors, provide a control that lets people choose the custom behavior mode. People need to know which mode theyโ€™re in; otherwise, they may get confused when your app responds differently to their interactions. In this scenario, make sure itโ€™s easy for people to discover the custom behavior your app supports, but donโ€™t turn it on by default. + +**Avoid using the double-tap gesture to perform an action that modifies content.** In rare cases, itโ€™s possible for people to double-tap accidentally, which means that they may not even be aware that your app has performed the action. Prefer using double tap to perform actions that are easy for people to undo. In particular, avoid using double tap to perform a potentially destructive action that might result in data loss. + +## [Squeeze](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Squeeze) + +Using Apple Pencil Pro, people can squeeze to perform an action. You can design a custom behavior that responds to squeeze, but recognize that people may choose to configure the squeeze gesture to run an [App Shortcut](https://developer.apple.com/design/human-interface-guidelines/app-shortcuts) instead of app-specific actions. + +Note + +The squeeze gesture is available only when the paired iPad screen is on and while the Apple Pencil Pro is not directly contacting it. Because squeeze works when thereโ€™s distance between Apple Pencil Pro and iPad, people might not always be visually aware of the gestureโ€™s onscreen result. + +**Treat squeeze as a single, quick gesture that performs a discrete โ€” not continuous โ€” action.** People sometimes squeeze with a lot of force, so holding a squeeze or squeezing several times quickly can be tiring. Help people remain comfortable by responding to a single squeeze and promptly displaying the result. + +**If you use squeeze to reveal app UI, like a contextual menu, display it close to Apple Pencil Pro.** Displaying the result of a squeeze near the tip of Apple Pencil Pro strengthens the connection between the device and the gesture, and can help people stay engaged with their task. + +**Define squeeze actions that are nondestructive and easy to undo.** As with the double-tap gesture, people can make the squeeze gesture without meaning to, so itโ€™s essential to avoid using squeeze to perform an action that could result in data loss. + +## [Barrel roll](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Barrel-roll) + +While marking with Apple Pencil Pro, people can use a barrel-roll gesture to change the type of mark theyโ€™re making. For example, while using Apple Pencil Pro to highlight content in Notes, people can barrel-roll to rotate the angle of the mark. + +**Use barrel roll only to modify marking behavior, not to enable navigation or display other controls.** In contrast to double tap and squeeze, barrel roll is naturally related to marking and doesnโ€™t make sense for performing an interface action. + +## [Scribble](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Scribble) + +With Scribble and Apple Pencil, people can simply write wherever text is accepted in your app โ€” they donโ€™t have to tap or switch modes first. Because Scribble is fully integrated into iPadOS, itโ€™s available to all apps by default. + +**Make text entry feel fluid and effortless.** By default, Scribble works in all standard text components โ€” such as text fields, text views, search fields, and editable fields in web content โ€” except password fields. If you use a custom text field in your app, avoid making people tap or select it before they can begin writing. + +**Make Scribble available everywhere people might want to enter text.** Unlike using the keyboard, using Apple Pencil encourages people to treat the screen the way they treat a sheet of paper. Help strengthen this perception in your app by making Scribble consistently available in places where text entry seems natural. For example, in Reminders, itโ€™s natural for people to create a new reminder by writing it in the blank space below the last item, even though the area doesnโ€™t contain a text field. For developer guidance, see [`UIIndirectScribbleInteraction`](https://developer.apple.com/documentation/UIKit/UIIndirectScribbleInteraction-1nfjm). + +**Avoid distracting people while they write.** Some text field behaviors work well for keyboard input, but can disrupt the natural writing experience that Apple Pencil provides. For example, itโ€™s best to avoid displaying autocompletion text as people write in a text field because the suggestions can visually interfere with their writing. Itโ€™s also a good idea to hide a fieldโ€™s placeholder text the moment people begin to write so that their input doesnโ€™t appear to overlap it. + +**While people are writing in a text field, make sure it remains stationary.** In some cases, it can make sense to move a text field when it becomes focused: for example, a search field might move to make more room to display results. Such a movement is fine when people are using the keyboard, but when theyโ€™re writing it can make them feel like theyโ€™ve lost control of where their input is going. If you canโ€™t prevent a text field from moving or resizing, consider delaying the change until people pause their writing. + +**Prevent autoscrolling text while people are writing and editing in a text field.** When transcribed text autoscrolls, people might try to avoid writing on top of it. Worse, if text scrolls while people are using Apple Pencil to select it, they might select a different range of text than they want. + +**Give people enough space to write.** A small text field can feel uncomfortable to write in. When you know that Apple Pencil input is likely, improve the writing experience in your app by increasing the size of the text field before people begin to write in it or when they pause writing; avoid resizing a text field while people are writing. For developer guidance, see [`UIScribbleInteraction`](https://developer.apple.com/documentation/UIKit/UIScribbleInteraction). + +![An illustration showing a stack of two text fields, where the top field is about half the width of the bottom field. Both text fields contain the word Name in the leading end, followed by a person's signature. The top text field is too narrow to fit all of the signature and is marked with an X in a circle to indicate incorrect usage. The bottom text field is wide enough to fit the full signature and is marked with a checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/0e2cd3f5562569097b9f668253dac0f7/apple-pencil-scribble%402x.png) + +## [Custom drawing](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Custom-drawing) + +Using [PencilKit](https://developer.apple.com/documentation/PencilKit), you can let people take notes, annotate documents and images, and draw with the same low-latency experience that iOS provides. PencilKit also makes it easy to create a custom drawing canvas in your app and offer a state-of-the-art tool picker and ink palette. + +**Help people draw on top of existing content.** By default, the colors on your PencilKit canvas dynamically adjust to Dark Mode, so people can create content in either mode and the results will look great in both. However, when people draw on top of existing content like a PDF or a photo, you want to prevent the dynamic adjustment of colors so that the markup remains sharp and visible. + +**Consider displaying custom undo and redo buttons when your app runs in a compact environment.** In a regular environment, the tool picker includes undo and redo buttons, but in a compact environment it doesnโ€™t. In a compact environment, you could display undo and redo buttons in a toolbar. You might also consider supporting the standard 3-finger undo/redo gesture, so people can use it in any environment. For guidance, see [Undo and redo](https://developer.apple.com/design/human-interface-guidelines/undo-and-redo). + +![An illustration of an iPad screen in landscape on the left and an iPhone screen in portrait on the right. Both screens show the tool picker at the bottom edge of the screen. The iPad screen shows the standard undo and redo buttons in the left end of the tool picker, and the iPhone screen shows the undo button in the top toolbar.](https://docs-assets.developer.apple.com/published/7587fbeb4272d990e295d093f79e1ef8/apple-pencil-undo-redo-buttons%402x.png) + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Platform-considerations) + + _Not supported in iOS, macOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Related) + +[Entering data](https://developer.apple.com/design/human-interface-guidelines/entering-data) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Developer-documentation) + +[PencilKit](https://developer.apple.com/documentation/PencilKit) + +[PaperKit](https://developer.apple.com/documentation/PaperKit) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/BE1C66C1-9D8C-4EF7-BE9A-A36251A00B86/10006_wide_250x141_1x.jpg) Meet PaperKit ](https://developer.apple.com/videos/play/wwdc2025/285) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/2104DC8F-01CE-453F-AF0E-A499CB193E97/9354_wide_250x141_1x.jpg) Squeeze the most out of Apple Pencil ](https://developer.apple.com/videos/play/wwdc2024/10214) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble#Change-log) + +Date| Changes +---|--- +May 7, 2024| Added guidance for handling squeeze and barrel roll on Apple Pencil Pro. +September 12, 2023| Updated artwork. +November 3, 2022| Added guidelines for using hover to enhance your app. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/camera-control.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/camera-control.md new file mode 100644 index 00000000..4b76a1a7 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/camera-control.md @@ -0,0 +1,107 @@ +--- +title: "Camera Control | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/camera-control + +# Camera Control + +The Camera Control provides direct access to your appโ€™s camera experience. + +![A stylized representation of the Camera Control.](https://docs-assets.developer.apple.com/published/73774a69a0e7738c02ffdaeccfe03830/inputs-camera-control-intro%402x.png) + +On iPhone 16 and iPhone 16 Pro models, the Camera Control quickly opens your appโ€™s camera experience to capture moments as they happen. When a person lightly presses the Camera Control, the system displays an overlay that extends from the device bezel. + +![A screenshot showing callouts to the Camera Control and overlay on iPhone in landscape orientation.](https://docs-assets.developer.apple.com/published/3d9efd41aaf5eb91e9d51fdf57a26472/camera-control-button-callout%402x.png) + +The overlay allows people to quickly adjust controls. A person can view the available controls by lightly double-pressing the Camera Control. After selecting a control, they can slide their finger on the Camera Control to adjust a value to capture their content as they want. + +![A partial screenshot of the Camera Control overlay displaying its controls.](https://docs-assets.developer.apple.com/published/59fe90435020556bfc9b5ed3f003b651/camera-control-picker%402x.png)Controls in the overlay + +## [Anatomy](https://developer.apple.com/design/human-interface-guidelines/camera-control#Anatomy) + +The Camera Control offers two types of controls for adjusting values or changing between options: + + * A _slider_ provides a range of values to choose from, such as how much contrast to apply to the content. + + * A _picker_ offers discrete options, such as turning a grid on and off in the viewfinder. + + + + +![A partial screenshot of the Camera Control overlay displaying a slider control.](https://docs-assets.developer.apple.com/published/3bb2ecfcd292742f087c064e5dfd1ec5/camera-control-slider-control%402x.png)Slider control + +![A partial screenshot of the Camera Control overlay displaying a picker control.](https://docs-assets.developer.apple.com/published/27e6bc8836d9265133dd150098c3865d/camera-control-picker-control%402x.png)Picker control + +In addition to custom controls that you create, the system provides a set of standard controls that you can optionally include in the overlay to allow someone to adjust their cameraโ€™s zoom and exposure. + +![A partial screenshot of the Camera Control overlay displaying the system zoom factor control.](https://docs-assets.developer.apple.com/published/3bb2ecfcd292742f087c064e5dfd1ec5/system-control-type-zoom-factor%402x.png)Zoom factor control + +![A partial screenshot of the Camera Control overlay displaying the system exposure bias control.](https://docs-assets.developer.apple.com/published/47568cc559bb20a5cea03ded2199916b/system-control-type-exposure-bias%402x.png)Exposure bias control + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/camera-control#Best-practices) + +**Use SF Symbols to represent control functionality.** The system doesnโ€™t support custom symbols; instead, pick a symbol from SF Symbols that clearly denotes a controlโ€™s behavior. iOS offers thousands of symbols you can use to represent the controls your app shows in the overlay. Symbols for controls donโ€™t represent their current state. To view available symbols, see the Camera & Photos section in the [SF Symbols app](https://developer.apple.com/sf-symbols/). + +![A partial screenshot of the Camera Control overlay displaying a camera flash control that uses the bolt.fill symbol.](https://docs-assets.developer.apple.com/published/29e9dac71ac35e1d3d9510a545fce3c3/camera-control-picker-sf-symbols-flash%402x.png)The `bolt.fill` symbol that represents a control for the camera flash + +![A partial screenshot of the Camera Control overlay displaying a camera filters control that uses the camera.filters symbol.](https://docs-assets.developer.apple.com/published/17466338143a202a0241d26725f23048/camera-control-picker-sf-symbols-filters%402x.png)The `camera.filters` symbol that represents a control for filters + +**Keep names of controls short.** Control labels adhere to Dynamic Type sizes, and longer names may obfuscate the cameraโ€™s viewfinder. + +**Include units or symbols with slider control values to provide context.** Providing descriptive information in the overlay, such as EV, %, or a custom string, helps people understand what the slider controls. For developer guidance, see [`localizedValueFormat`](https://developer.apple.com/documentation/AVFoundation/AVCaptureSlider/localizedValueFormat). + +![A partial screenshot showing an example of the Camera Control overlay with a slider control displaying a value and context for the type of value.](https://docs-assets.developer.apple.com/published/00f466e6926811164965fdb40483a34c/system-control-with-label%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png)Value with context + +![A partial screenshot showing an example of the Camera Control overlay with a slider control displaying a value without information about what the value represents.](https://docs-assets.developer.apple.com/published/b965fe40e836dfce1361f8653c3d2abb/system-control-no-label%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png)Value without context + +**Define prominent values for a slider control.** Prominent values are ones people choose most frequently, or values that are evenly spaced, like the major increments of zoom factor. When a person slides on the Camera Control to adjust a slider control, the system more easily lands on prominent values you define. For developer guidance, see [`prominentValues`](https://developer.apple.com/documentation/AVFoundation/AVCaptureSlider/prominentValues-199dz). + +**Make space for the overlay in the viewfinder.** The overlay and control labels occupy the screen area adjacent to the Camera Control in both portrait and landscape orientations. To avoid overlapping the interface elements of your camera capture experience, place your UI outside of the overlay areas. Maximize the height and width of the viewfinder and allow the overlay to appear and disappear over it. + +![Partial screenshots showing the Camera Control overlay with its control's label in the viewport in portrait and landscape orientations on iPhone.](https://docs-assets.developer.apple.com/published/efa0584ce5fa07cd540174b71ef59d6d/camera-control-portrait-landscape-orientation%402x.png) + +**Minimize distractions in the viewfinder.** When capturing a photo or video, people appreciate a large preview image with as few visual distractions as possible. Avoid duplicating controls, like sliders and toggles, in your UI and the overlay when the system displays the overlay. + +![A partial screenshot showing an example of the Camera Control overlay with UI elements removed from the capture viewport.](https://docs-assets.developer.apple.com/published/9cd4da3793671dd837c50d855ab265df/camera-control-screen-ui-good-example%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png)Keep UI minimal. + +![A partial screenshot showing an example of the Camera Control overlay with UI elements duplicated in the capture viewport.](https://docs-assets.developer.apple.com/published/eb4a1bc88b0f16c3074d57f8ff3afb9f/camera-control-screen-ui-bad-example%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png)Avoid showing controls in the viewfinder that people access in the overlay. + +**Enable or disable controls depending on the camera mode.** For example, disable video controls when taking photos. The overlay supports multiple controls, but you canโ€™t remove or add controls at runtime. + +**Consider how to arrange your controls.** Order commonly used controls toward the middle to allow quick access, and include lesser used controls on either side. When a person lightly presses the Camera Control to open the overlay again, the system remembers the last control they used in your app. + +**Allow people to use the Camera Control to launch your experience from anywhere.** Create a locked camera capture extension that lets people configure the Camera Control to launch your appโ€™s camera experience from their locked device, the Home Screen, or from within other apps. For guidance, see [Camera experiences on a locked device](https://developer.apple.com/design/human-interface-guidelines/controls#Camera-experiences-on-a-locked-device). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/camera-control#Platform-considerations) + + _Not supported in iPadOS, macOS, watchOS, tvOS, or visionOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/camera-control#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/camera-control#Related) + +[SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) + +[Controls](https://developer.apple.com/design/human-interface-guidelines/controls) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/camera-control#Developer-documentation) + +[Enhancing your app experience with the Camera Control](https://developer.apple.com/documentation/AVFoundation/enhancing-your-app-experience-with-the-camera-control) โ€” AVFoundation + +[`AVCaptureControl`](https://developer.apple.com/documentation/AVFoundation/AVCaptureControl) โ€” AVFoundation + +[LockedCameraCapture](https://developer.apple.com/documentation/LockedCameraCapture) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/camera-control#Change-log) + +Date| Changes +---|--- +September 9, 2024| New page. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/digital-crown.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/digital-crown.md new file mode 100644 index 00000000..26c1dd02 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/digital-crown.md @@ -0,0 +1,83 @@ +--- +title: "Digital Crown | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/digital-crown + +# Digital Crown + +The Digital Crown is an important hardware input for Apple Vision Pro and Apple Watch. + +![A sketch of a curved arrow beside a Digital Crown, that suggests turning the Digital Crown. The image is overlaid with rectangular and circular grid lines and is tinted purple to subtly reflect the purple in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/3b12fdaf898877ad12d62535cea6d032/inputs-digital-crown-intro%402x.png) + +On both Apple Vision Pro and Apple Watch, people can use the Digital Crown to interact with the system; on Apple Watch, people can also use the Digital Crown to interact with apps. + +![A close-up photograph of a person's head wearing Apple Vision Pro, with their index finger pointing at the Digital Crown.](https://docs-assets.developer.apple.com/published/b421afd55a6401eeacedaa088b02d909/digital-crown-apple-vision-pro%402x.png)The Digital Crown on Apple Vision Pro + +![A close-up photograph of Apple Watch, shown at an angle, with the Digital Crown prominently featured at the center of the image.](https://docs-assets.developer.apple.com/published/b557ec51bcbcaac70485ca87eda59c40/digital-crown-apple-watch%402x.png)The Digital Crown on Apple Watch + +## [Apple Vision Pro](https://developer.apple.com/design/human-interface-guidelines/digital-crown#Apple-Vision-Pro) + +On Apple Vision Pro, people use the Digital Crown to: + + * Adjust volume + + * Adjust the amount of immersion in a portal, an Environment, or an app or game running in a Full Space (for guidance, see [Immersive experiences](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences)) + + * Recenter content so itโ€™s in front of them + + * Open Accessibility settings + + * Exit an app and return to the Home View + + + + +## [Apple Watch](https://developer.apple.com/design/human-interface-guidelines/digital-crown#Apple-Watch) + +As people turn the Digital Crown, it generates information you can use to enhance or facilitate interactions with your app, like scrolling or operating standard or custom controls. + +Starting with watchOS 10, the Digital Crown takes on an elevated role as the primary input for navigation. On the watch face, people turn the Digital Crown to view widgets in the Smart Stack, and on the Home Screen, people use it to move vertically through their collection of apps. Within apps, people turn the Digital Crown to switch between vertically paginated tabs, and to scroll through list views and variable height pages. + +Beyond its use for navigation, turning the Digital Crown generates information you can use to enhance or facilitate interactions with your app, such as inspecting data or operating standard or custom controls. + +Note + +Apps donโ€™t respond to presses on the Digital Crown because watchOS reserves these interactions for system-provided functionality like revealing the Home Screen. + +Most Apple Watch models provide haptic feedback for the Digital Crown, which gives people a more tactile experience as they scroll through content. By default, the system provides linear haptic _detents_ โ€” or taps โ€” as people turn the Digital Crown a specific distance. Some system controls, like table views, provide detents as new items scroll onto the screen. + +**Anchor your appโ€™s navigation to the Digital Crown.** Starting with watchOS 10, turning the Digital Crown is the main way people navigate within and between apps. List, tab, and scroll views are vertically oriented, allowing people to use the Digital Crown to easily move between the important elements of your appโ€™s interface. When anchoring interactions to the Digital Crown, also be sure to back them up with corresponding touch screen interactions. + +**Consider using the Digital Crown to inspect data in contexts where navigation isnโ€™t necessary.** In contexts where the Digital Crown doesnโ€™t need to navigate through lists or between pages, itโ€™s a great tool to inspect data in your app. For example, in World Clock, turning the Digital Crown advances the time of day at a selected location, allowing people to compare various times of day to their current time. + +**Provide visual feedback in response to Digital Crown interactions.** For example, pickers change the currently displayed value as people use the Digital Crown. If you track turns directly, use this data to update your interface programmatically. If you donโ€™t provide visual feedback, people are likely to assume that turning the Digital Crown has no effect in your app. + +**Update your interface to match the speed with which people turn the Digital Crown.** People expect turning the Digital Crown to give them precise control over an interface, so it works well to use this speed to determine the speed at which you make changes. Avoid updating content at a rate that makes it difficult for people to select values. + +**Use the default haptic feedback when it makes sense in your app.** If haptic feedback doesnโ€™t feel right in the context of your app โ€” for example, if the default detents donโ€™t match your appโ€™s animation โ€” turn off the detents. You can also adjust the haptic feedback behavior for tables, letting them use linear detents instead of row-based detents. For example, if your table has rows with significantly different heights, linear detents may give people a more consistent experience. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/digital-crown#Platform-considerations) + + _Not supported in iOS, iPadOS, macOS, or tvOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/digital-crown#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/digital-crown#Related) + +[Feedback](https://developer.apple.com/design/human-interface-guidelines/feedback) + +[Action button](https://developer.apple.com/design/human-interface-guidelines/action-button) + +[Immersive experiences](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/digital-crown#Developer-documentation) + +[`WKCrownDelegate`](https://developer.apple.com/documentation/WatchKit/WKCrownDelegate) โ€” WatchKit + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/digital-crown#Change-log) + +Date| Changes +---|--- +December 5, 2023| Added artwork for Apple Vision Pro and Apple Watch, and clarified that visionOS apps donโ€™t receive direct information from the Digital Crown. +June 21, 2023| Updated to include guidance for visionOS. +June 5, 2023| Added guidelines emphasizing the central role of the Digital Crown for navigation. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/eyes.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/eyes.md new file mode 100644 index 00000000..0b12d1e4 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/eyes.md @@ -0,0 +1,120 @@ +--- +title: "Eyes | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/eyes + +# Eyes + +In visionOS, people look at a virtual object to identify it as a target they can interact with. + +![A sketch of a human eye. The image is overlaid with rectangular and circular grid lines and is tinted purple to subtly reflect the purple in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/126393ded1c486236fc7a9feabea30ea/inputs-eyes-intro%402x.png) + +When people look at an interactive element, visionOS highlights it, providing visual feedback that helps them confirm the item is one they want. The visual feedback, or _hover effect_ , shows people that they can use an [indirect gesture](https://developer.apple.com/design/human-interface-guidelines/gestures#visionOS) like tap to interact with the element. + +Video with custom controls. + +Content description: A recording of the Settings app showing the hover effect appear on several individual settings in turn as someone's eyes move from one to another. + +Play + +In some cases, the system can automatically display an expanded view of a component after people look at it. For example, when people look at a tab bar, the entire bar resizes to reveal text labels next to each tab. In this scenario, an individual tab also highlights before the tab bar expansion to let people select it before revealing the labels. Another example is a button that can reveal a tooltip when people look at it. + +Important + +To help preserve peopleโ€™s privacy, visionOS doesnโ€™t provide direct information about where people are looking before they tap. When you use system-provided components, visionOS automatically tells you when people tap the component. For developer guidance, see [Adopting best practices for privacy and user preferences](https://developer.apple.com/documentation/visionOS/adopting-best-practices-for-privacy). + +visionOS also supports _focus effects_ that help people navigate apps and the system using a connected input device like a keyboard or game controller. Focus effects are unrelated to the hover effect; to learn more, see [Focus and selection](https://developer.apple.com/design/human-interface-guidelines/focus-and-selection). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/eyes#Best-practices) + +**Always give people multiple ways to interact with your app.** Design your app to support the accessibility features people use to personalize the ways they interact with their devices. For guidance, see [Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility). + +**Design for visual comfort.** Help people accomplish their primary task by making sure that the objects they need to use are within their [field of view](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Field-of-view). When your app or game is running in the Shared Space or a Full Space, the system automatically places the first window or volume people open in a convenient location in front of them. While running in a Full Space, your app or game can also request access to information about a personโ€™s head pose to help you place 3D content appropriately. In all cases, you can improve the visual comfort of your experience when you avoid requiring people to make multiple quick eye adjustments, either over a large area or through multiple levels of depth. For guidance, see [Depth](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Depth). + +**Place content at a comfortable viewing distance.** For example, to help people remain comfortable while they read or engage with content over time, aim to place it at least one meter away. In general, you donโ€™t want to place content very close to people unless theyโ€™ll view or interact with it for only a little while. + +**Prefer using standard UI components.** System-provided components respond consistently when people look at them. If your custom components use different visual cues to provide visual feedback, it can be difficult for people to learn and remember how these components work. + +## [Making items easy to see](https://developer.apple.com/design/human-interface-guidelines/eyes#Making-items-easy-to-see) + +**Minimize visual distractions.** When thereโ€™s a lot of visual noise, it can be difficult for people to find the object theyโ€™re looking for. Visual movement can be even more distracting: When people sense movement โ€” especially in their peripheral vision โ€” they tend to respond automatically by looking at it, making it hard to keep looking at the object theyโ€™re interested in. For example, revealing content near a button people are looking at can cause them to involuntarily look at the new content instead of the button. + +**Make it easy for people to look at an item by providing enough space around it.** Because eyes naturally tend to make small, quick adjustments in direction even while people are looking at one place, crowding UI objects together can make it difficult for people to look at one of them without jumping to another. You can help ensure that thereโ€™s enough space between interactive items by using a margin of at least 16 points around the bounds of each item or by placing items so that their centers are always at least 60 points apart. For additional layout guidance, see [Layout](https://developer.apple.com/design/human-interface-guidelines/layout) and [Spatial layout](https://developer.apple.com/design/human-interface-guidelines/spatial-layout). + +**Avoid using a repeating pattern or texture that fills the field of view.** In some cases, peopleโ€™s eyes can lock onto different elements in a pattern or texture, making the elements appear to have different depths. To avoid this effect, consider using the pattern in a smaller area. + +## [Encouraging interaction](https://developer.apple.com/design/human-interface-guidelines/eyes#Encouraging-interaction) + +**Consider using subtle visual cues to encourage people to look at the item theyโ€™re most likely to want.** For example, it often works well to place the item near the center of the field of view or use techniques like gentle motion, increased contrast, or variations in color or scale to draw peopleโ€™s attention. In general, prefer cues that are noticeable without being flashy or harsh. + +**In general, give an interactive item a rounded shape.** Peopleโ€™s eyes tend to be drawn toward the corners in a shape, making it difficult to keep looking at the shapeโ€™s center. The more rounded an itemโ€™s shape, the easier it is for people to use their eyes to target it. + +![A square button.](https://docs-assets.developer.apple.com/published/d60c5b225c91f041c5ef7e273a9219b6/visionos-eyes-sharp-button-incorrect%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![A circular button.](https://docs-assets.developer.apple.com/published/61afcfc99cebef8a0feae23fc5803edc/visionos-eyes-rounded-button-correct%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +**If you create an interactive component that consists of more than one element, be sure to provide an overall containing shape that visionOS can highlight.** For example, if an image and a label below it combine to act as one interactive component, you need to define a custom region that encompasses both elements, allowing visionOS to highlight the entire region when people look at either element. + +## [Custom hover effects](https://developer.apple.com/design/human-interface-guidelines/eyes#Custom-hover-effects) + +When it makes sense in your app or game, you can design a hover effect that animates in a custom way when people look at an element, including system-provided or custom UI elements and RealityKit entities. You can use a custom hover effect to replace or augment a standard effect. + +Before you start designing custom hover effects, itโ€™s important to understand how they work. To enable a custom hover effect for an element, you create two states or appearances for it: one that shows the custom hover effect and one that doesnโ€™t. When someone looks at the element in your app or game, the system applies your predefined hover effect in a process thatโ€™s outside of your softwareโ€™s process. This means that you donโ€™t know when the system applies a custom hover effect or what state the element is in at that moment. The out-of-process nature of a custom hover effect also means that it canโ€™t run code that requires knowing when people are looking at the element. + +As an example that shows what a custom hover effect can and canโ€™t do, consider a photo-browsing app where a photoโ€™s custom effect displays a different symbol depending on whether people have added the photo to Favorites. The app specifies the appropriate symbol for a photoโ€™s custom hover effect and the system displays the effect if people look at the photo. However, the hover effect canโ€™t perform the favoriting action because the system doesnโ€™t tell the app when someone is looking at the photo. + +**Prefer using a custom hover effect to emphasize or enhance a special moment in your experience.** People are accustomed to the standard hover effects that provide visual feedback or, in the case of tab bars or tooltips, additional information, so a custom hover effect can be especially noticeable. Adding too many custom hover effects โ€” or using them when standard effects are sufficient โ€” can dilute the impact of your design, distract people from their task, and even cause visual discomfort. + +**Choose the right delay.** An elementโ€™s custom hover effect can appear instantly, after a short delay, or after a slightly longer delay, depending on how you expect people to interact with the element. + + * **No delay (default).** A custom hover effect that appears without delay tends to be especially useful when the effect is subtle or invites interaction, like when a knob appears on a slider. + + * **Short delay.** Consider using a short delay to let people look at an element and quickly interact with it without waiting for the effect to appear; for example, the expansion of tabs in a tab bar works this way. + + * **Long delay.** If your custom hover effect shows additional information, like when a tooltip appears below a button, a slightly longer delay can work well because most people wonโ€™t need to view the additional information every time. + + + + +**Aim to keep one or more of the elementโ€™s primary views unchanged in both states of a custom hover effect.** When at least one primary view remains constant during a hover effectโ€™s animation, it provides visual stability that can help people follow the elementโ€™s transition. If all of an elementโ€™s views move or change during a custom hover effect, it can disorient people and make them lose track of whatโ€™s happening. + +**Thoroughly test custom hover effects.** Testing is the only way to determine whether a custom hover effect looks good, responds appropriately, and makes your experience feel alive without distracting people. Aim to test your custom hover effects while wearing Apple Vision Pro so you can develop intuition about how to use them to enhance your experience. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/eyes#Platform-considerations) + + _Not supported in iOS, iPadOS, macOS, tvOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/eyes#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/eyes#Related) + +[Immersive experiences](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences) + +[Gestures](https://developer.apple.com/design/human-interface-guidelines/gestures) + +[Spatial layout](https://developer.apple.com/design/human-interface-guidelines/spatial-layout) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/eyes#Developer-documentation) + +[Adopting best practices for privacy and user preferences](https://developer.apple.com/documentation/visionOS/adopting-best-practices-for-privacy) โ€” visionOS + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/eyes#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/CA8CE5A1-B113-403F-BCB7-87871B4BBB52/10053_wide_250x141_1x.jpg) Design hover interactions for visionOS ](https://developer.apple.com/videos/play/wwdc2025/303) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/C6CDCC79-CCD0-4D2F-A4D1-8FC70DC663DB/8127_wide_250x141_1x.jpg) Design for spatial input ](https://developer.apple.com/videos/play/wwdc2023/10073) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/2C47B638-090D-4CBB-9E9E-EBE8114536D9/8132_wide_250x141_1x.jpg) Design considerations for vision and motion ](https://developer.apple.com/videos/play/wwdc2023/10078) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/eyes#Change-log) + +Date| Changes +---|--- +June 10, 2024| Added guidance for custom hover effects. +March 29, 2024| Added artwork showing the visionOS hover effect. +October 24, 2023| Clarified the difference between focus effects and the visionOS hover effect. +June 21, 2023| New page. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/focus-and-selection.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/focus-and-selection.md new file mode 100644 index 00000000..d1e8518c --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/focus-and-selection.md @@ -0,0 +1,120 @@ +--- +title: "Focus and selection | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/focus-and-selection + +# Focus and selection + +Focus helps people visually confirm the object that their interaction targets. + +![A sketch of a frame around a circular interface element, suggesting locking focus on an object. The image is overlaid with rectangular and circular grid lines and is tinted purple to subtly reflect the purple in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/13b5befef4936f31ce74db6aa05b7a0e/inputs-focus-and-selection-intro%402x.png) + +Focus supports simplified, component-based navigation. Using inputs like a remote, game controller, or keyboard, people bring focus to the components they want to interact with. + +In many cases, focusing an item also selects it. The exception is when automatic selection might cause a distracting context shift, like opening a new view. In tvOS, for example, people use the remote to move focus from item to item as they seek the one they want, but because selecting a focused item opens or activates it, selection requires a separate gesture. + +Different platforms communicate focus in different ways. For example, iPadOS and macOS show focus by drawing a ring around an item or highlighting it; tvOS generally uses the [parallax effect](https://developer.apple.com/design/human-interface-guidelines/images#Parallax-effect) to give the focused item an appearance of depth and liveliness. The combination of focus effects and interactions is sometimes called a _focus system_ or _focus model_. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/focus-and-selection#Best-practices) + +**Rely on system-provided focus effects.** System-defined focus effects are precisely tuned to complement interactions with Apple devices, providing experiences that feel responsive, fluid, and lifelike. Incorporating system-provided focus behaviors gives your app consistency and predictability, helping people understand it quickly. Consider creating custom focus effects only if itโ€™s absolutely necessary. + +**Avoid changing focus without peopleโ€™s interaction.** People rely on the focus system to help them know where they are in your app. If you change focus without their interaction, people have to spend time finding the newly focused item, delaying their current task. The exception is when people are moving focus using an input device that lets them make discrete, directional movements โ€” like a keyboard, remote, or game controller โ€” and a previously focused item disappears. In this scenario, there are only a small number of items within one discrete step of the previously focused item, so moving focus to one of these remaining items ensures that the focus indicator is in a location people can easily find. When people arenโ€™t moving focus by using such an input device, you canโ€™t predict the item theyโ€™ll target next, so itโ€™s generally best to simply hide the focus indicator when the focused object disappears. + +**Be consistent with the platform as you help people bring focus to items in your app.** For example, in iPadOS and macOS, a full keyboard access mode helps people use the keyboard to reach every control, so you only need to support focus for content elements like list items, text fields, and search fields, and not for controls like buttons, sliders, and toggles. In contrast, tvOS users rely on using directional gestures on a remote or game controller (or pressing the arrow keys on an attached keyboard) to reach every onscreen element, so you need to make sure that people can bring focus to every element in your app. + +**Indicate focus using visual appearances that are consistent with the platform.** For example, consider a window that contains a list of items. In iPadOS and macOS, the system draws focused list items using white text and a background highlight that matches the appโ€™s accent color, drawing unfocused items using the standard text color and a gray background highlight (for developer guidance, see [`UICollectionView`](https://developer.apple.com/documentation/UIKit/UICollectionView) and [`NSTableView`](https://developer.apple.com/documentation/AppKit/NSTableView)). + +**In general, use a focus ring for a text or search field, but use a highlight in a list or collection.** Although you can use a focus ring to draw attention to an item that fills a cell, like a photo, itโ€™s usually easier for people to view lists and collections when an entire row is highlighted. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/focus-and-selection#Platform-considerations) + + _Not supported in iOS or watchOS._ + +### [iPadOS](https://developer.apple.com/design/human-interface-guidelines/focus-and-selection#iPadOS) + +iPadOS 15 and later defines a focus system that supports keyboard interactions for navigating text fields, text views, and sidebars, in addition to various types of collection views and other custom views in your app. + +The iPadOS and tvOS focus systems are similar. People perform actions by moving a focus indicator to an item and then selecting it (for guidance, see [tvOS](https://developer.apple.com/design/human-interface-guidelines/focus-and-selection#tvOS)). Although the underlying system is the same, the user experiences are a little different. tvOS uses _directional focus_ , which means people can use the same interaction โ€” that is, swiping the Siri Remote or using only the arrow keys on a connected keyboard โ€” to navigate to every onscreen component. In contrast, iPadOS defines _focus groups_ , which represent specific areas within an app, like a sidebar, grid, or list. Using focus groups, iPadOS can support two different keyboard interactions. + + * Pressing the Tab key moves focus among focus groups, letting people navigate to sidebars, grids, and other app areas. + + * Pressing an arrow key supports a directional focus interaction thatโ€™s similar to tvOS, but limited to navigation among items in the same focus group. For example, people can use an arrow key to move through the items in a list or a sidebar. + + + + +Onscreen components can indicate focus by using the halo effect or the highlighted appearance. + +The _halo_ focus effect โ€” also known as the _focus ring_ โ€” displays a customizable outline around the component. You can apply the halo effect to custom views and to fully opaque content within a collection or list cell, such as an image. + +![An illustration of a collection view of photos showing the standard halo effect that outlines the focused photo.](https://docs-assets.developer.apple.com/published/2bfe6fedc5a6a8ecf6d7e74e9492a096/focus-and-selection-halo-focus-effect%402x.png) + +**Customize the halo focus effect when necessary.** By default, the system uses an itemโ€™s shape to infer the shape of its halo. If the system-provided halo doesnโ€™t give you the appearance you want, you can refine it to match contours like rounded corners or shapes defined by Bรฉzier paths. You can also adjust a haloโ€™s position if another component occludes or clips it. For example, you might need to ensure that a badge appears above the halo or that a parent view doesnโ€™t clip it. For developer guidance, see [`UIFocusHaloEffect`](https://developer.apple.com/documentation/UIKit/UIFocusHaloEffect). + +![An illustration of a collection view of photos showing a rounded-rectangle halo effect that outlines the focused photo.](https://docs-assets.developer.apple.com/published/1a84f872d0624355e89fa03b357ddd13/focus-and-selection-customized-halo%402x.png) + +The _highlighted_ appearance โ€” in which the componentโ€™s text uses the appโ€™s accent color โ€” also indicates focus, but itโ€™s not a focus effect. The highlight appearance occurs automatically when people select a collection view cell on which youโ€™ve set content configurations (for developer guidance, see [`UICollectionViewCell`](https://developer.apple.com/documentation/UIKit/UICollectionViewCell)). + +![An illustration of a list of menu items with the second item highlighted. The item's title and icon are tinted with a red accent color.](https://docs-assets.developer.apple.com/published/01261865c38379fa118f16057a54f23e/focus-and-selection-highlighted-appearance%402x.png) + +**Ensure that focus moves through your custom views in ways that make sense.** As people continue pressing the Tab key, focus moves through focus groups in reading order: leading to trailing, and top to bottom. Although focus moves through system-provided views in ways that people expect, you might need to adjust the order in which the focus system visits your custom views. For example, if you want focus to move down through a vertical stack of custom views before it moves in the trailing direction to the next view, you need to identify the stack container as a single focus group. For developer guidance, see [`focusGroupIdentifier`](https://developer.apple.com/documentation/UIKit/UIFocusEnvironment/focusGroupIdentifier). + +**Adjust the priority of an item to reflect its importance within a focus group.** When a group receives focus, its _primary item_ automatically receives focus too, making it easy for people to select the item theyโ€™re most likely to want. You can make an item primary by increasing its priority. For developer guidance, see [`UIFocusGroupPriority`](https://developer.apple.com/documentation/UIKit/UIFocusGroupPriority). + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/focus-and-selection#tvOS) + +**In a full-screen experience, let people use gestures to interact with the content, not to move focus.** When an item displays in full screen, it doesnโ€™t show focus, so people naturally assume that their gestures will affect the object, and not its focus state. + +**Avoid displaying a pointer.** People expect to navigate a fixed number of items by changing focus, not by trying to drag a tiny pointer around a huge screen. While free-form movement might make sense during gameplay, such as when looking for a hidden object or flying a plane, use the focus model when people navigate menus and other interface elements. If your app requires a pointer, make sure itโ€™s highly visible and feels integrated with your experience. + +**Design your interface to accommodate components in various focus states.** In tvOS, focusable items can have up to five different states, each of which is visually distinct. Because focusing an item often increases its scale, you need to supply assets for the larger, focused size to ensure they always look sharp, and you need to make sure the larger item doesnโ€™t crowd the surrounding interface. + +State| Description +---|--- +![An image of an unfocused button on top of a photograph. A small drop shadow makes it appear very close to the content behind it, with a translucent background infused by the colors of the content, and a high-contrast text color.](https://docs-assets.developer.apple.com/published/bfc53c88dc7a84a9ca45d43d8f7fb550/focus-and-selection-state-unfocused%402x.png)| The viewer hasnโ€™t brought focus to the item. Unfocused items appear less prominent than focused items. +![An image of a focused button on top of a photograph. Itโ€™s larger than an unfocused button, and a drop shadow makes it appear farther away from the content behind it, with an opaque white background and a black text label.](https://docs-assets.developer.apple.com/published/882b1286aa16b7a8d4a6367778a984b9/focus-and-selection-state-focused%402x.png)| The viewer brings focus to the item. A focused item visually stands out from the other onscreen content through elevation to the foreground, illumination, and animation. +![An image of a highlighted button on top of a photograph. Itโ€™s the same size as an unfocused button, and a drop shadow makes it appear a little farther away from the surface of the content behind it, with an opaque white background and a black text label.](https://docs-assets.developer.apple.com/published/d5388fe044717ba970895f33bdbebe3c/focus-and-selection-state-highlighted%402x.png)| The viewer chooses the focused item. A focused item provides instant visual feedback when people choose it. For example, a button might briefly invert its colors and animate before it transitions to its selected appearance. +![An image of a selected button on top of a photograph. Itโ€™s the same size as an unfocused button, and a small drop shadow makes it appear very close to the content behind it, with an opaque white background and a black text label.](https://docs-assets.developer.apple.com/published/ea6520ec5576b19ad7952c35a28c2dfc/focus-and-selection-state-selected%402x.png)| The viewer has chosen or activated the item in some way. For example, a heart-shaped button that people can use to favorite a photo might appear filled in the selected state and empty in the deselected state. +![An image of an unavailable button on top of a photograph. Itโ€™s the same size as an unfocused button. It lacks a drop shadow and appears to rest directly on the content behind it, with a translucent background tinted by the the colors of nearby content, and a low-contrast text color.](https://docs-assets.developer.apple.com/published/c1d9c327cbefe45ef0aeef12b93b956c/focus-and-selection-state-unavailable%402x.png)| The viewer canโ€™t bring focus to the item or choose it. An unavailable item appears inactive. + +For developer guidance, see [Adding user-focusable elements to a tvOS app](https://developer.apple.com/documentation/UIKit/adding-user-focusable-elements-to-a-tvos-app). + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/focus-and-selection#visionOS) + +visionOS supports the same focus system as in iPadOS and tvOS, letting people use a connected input device like a keyboard or game controller to interact with apps and the system. + +Note + +When people look at a virtual object to identify it as the object they want to interact with, the system uses the _hover effect_ , not a focus effect, to provide visual feedback (for guidance, see [Eyes](https://developer.apple.com/design/human-interface-guidelines/eyes)). The hover effect isnโ€™t related to the focus system. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/focus-and-selection#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/focus-and-selection#Related) + +[Eyes](https://developer.apple.com/design/human-interface-guidelines/eyes) + +[Keyboards](https://developer.apple.com/design/human-interface-guidelines/keyboards) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/focus-and-selection#Developer-documentation) + +[Focus Attributes](https://developer.apple.com/documentation/TVML/focus-attributes) โ€” TVML + +[Focus-based navigation](https://developer.apple.com/documentation/UIKit/focus-based-navigation) โ€” UIKit + +[About focus interactions for Apple TV](https://developer.apple.com/documentation/UIKit/about-focus-interactions-for-apple-tv) โ€” UIKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/focus-and-selection#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/C6CDCC79-CCD0-4D2F-A4D1-8FC70DC663DB/8127_wide_250x141_1x.jpg) Design for spatial input ](https://developer.apple.com/videos/play/wwdc2023/10073) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/38E4EE32-29B5-4478-B8B6-35B8ACA67B16/8130_wide_250x141_1x.jpg) Design for spatial user interfaces ](https://developer.apple.com/videos/play/wwdc2023/10076) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/49/F9A980A7-B00A-4856-9172-FDB610A419E5/3509_wide_250x141_1x.jpg) Design for the iPadOS pointer ](https://developer.apple.com/videos/play/wwdc2020/10640) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/focus-and-selection#Change-log) + +Date| Changes +---|--- +October 24, 2023| Clarified the difference between focus effects and the visionOS hover effect. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/game-controls.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/game-controls.md new file mode 100644 index 00000000..afdb6f95 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/game-controls.md @@ -0,0 +1,156 @@ +--- +title: "Game controls | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/game-controls + +# Game controls + +Precise, intuitive game controls enhance gameplay and can increase a playerโ€™s immersion in the game. + +![A sketch of a D-pad control from a game controller, suggesting gameplay. The image is overlaid with rectangular and circular grid lines and is tinted purple to subtly reflect the purple in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/b6c5b8cb6c62c9dd9f5e59ae745d6465/inputs-game-controls-intro%402x.png) + +On Apple platforms, a game can support input from physical game controllers or default system interactions, like touch, a remote, or a mouse and keyboard. Players might prefer to use physical game controllers, but there are two important reasons to also support a platformโ€™s default interaction methods: + + * Even though all platforms except watchOS support physical game controllers, not every player might have access to one. + + * Players appreciate games that let them use the platform interaction method theyโ€™re most familiar with. + + + + +To reach the widest audience and provide the best experience for each platform, keep these factors in mind when choosing the input methods to support. + +## [Touch controls](https://developer.apple.com/design/human-interface-guidelines/game-controls#Touch-controls) + +For iOS and iPadOS games, supporting touch interaction means that you can provide virtual controls on top of game content while also letting players interact with game elements by touching them directly. You can use the [Touch Controller](https://developer.apple.com/documentation/TouchController) framework to add these virtual controls to your game. Keep the following guidelines in mind to create an enjoyable touch control experience. + +**Determine whether it makes sense to display virtual controls on top of game content.** In general, virtual game controls benefit games that offer a large number of actions or require players to control movement. However, sometimes gameplay is more immersive and effective when players can interact directly with in-game objects. Look for opportunities to reduce the amount of virtual controls that overlap your game content by associating actions with in-game gestures instead. For example, consider letting players tap objects to select them instead of adding a virtual selection button. + +**Place virtual buttons where theyโ€™re easy to access.** Take into account the deviceโ€™s boundaries and [safe areas](https://developer.apple.com/design/human-interface-guidelines/layout#Guides-and-safe-areas) as well as comfortable locations for controls. Make sure to position buttons where they donโ€™t overlap system features like the Home indicator or Dynamic Island on iPhone. Place frequently used buttons near a playerโ€™s thumb, avoiding the circular regions where players expect movement and camera input to happen. Place secondary controls, like menus, at the top of the screen. + +![A graphic that shows ideal placement for touch controls for an iPhone in landscape orientation.](https://docs-assets.developer.apple.com/published/dd0cd40a5b38af26ea97072ecf987b24/game-controls-touch-input-heat-map%402x.png) + +Placing virtual controls within reach of peopleโ€™s thumbs can make your game more comfortable to play. + +**Make sure controls are large enough.** Make sure frequently used controls are a minimum size of 44x44 pt, and less important controls, such as menus, are a minimum size of 28x28 pt to accommodate peopleโ€™s fingers. + +**Always include visible and tactile press states.** A virtual control feels unresponsive without a visual and physical press state. Help players understand when they successfully interact with a button by adding a visual press state effect, such as a glow, that they can see even when their finger is covering the control. Combine this press state with sound and haptics to enhance the feeling of feedback. For guidance, see [Playing haptics](https://developer.apple.com/design/human-interface-guidelines/playing-haptics). + +![A right hand holding an iPhone in landscape orientation. The thumb is pressing down on a virtual button, and the button indicates its press state by increasing its opacity and showing a glow effect around it.](https://docs-assets.developer.apple.com/published/7e633e5b94444a3590ce03fee0d5c3df/game-controls-press-state%402x.png) + +**Use symbols that communicate the actions they perform.** Choose artwork that visually represents the action each button performs, such as a graphic of a weapon to represent an attack. Avoid using abstract shapes or controller-based naming like A, X, or R1 as artwork, which makes it harder for players to understand and remember what specific controls do. + +![A game controller button with a graphic of a square mapping to a virtual button with a graphic of a hand making a gesture to pick up an object.](https://docs-assets.developer.apple.com/published/d5ba7d4086cc786b24b789e29bfd3507/game-controls-button-to-action%402x.png) + +**Show and hide virtual controls to reflect gameplay.** Take advantage of the dynamic nature of touch controls and adapt what controls players see onscreen depending on their context. You can hide controls when an action isnโ€™t available or relevant, letting you reduce clutter and help players concentrate on whatโ€™s important. For example, consider hiding movement controls until a player touches the screen to reduce the amount of UI overlapping your game content. + + * Visible control + * Hidden control + + + +![A graphic that shows gameplay with a virtual control to move the character that's more visible while the character is moving.](https://docs-assets.developer.apple.com/published/2c9a0444ff10b37e8e5b54a9036d482e/game-controls-thumbstick-in-motion%402x.png) + +When the thumbstick moves to the right, it becomes more visible and shows a highlight to indicate the movement direction. + +![A graphic that shows gameplay with a virtual control to move the character that's less visible while the character is at rest.](https://docs-assets.developer.apple.com/published/8feb4b819cccdf9a74fa7c3ccd5d6e42/game-controls-thumbstick-at-rest%402x.png) + +When the thumbstick is at rest, the virtual control fades to show itโ€™s not in use. + +**Combine functionality into a single control.** Consider redesigning game mechanics that require players to press multiple buttons at the same time or in a sequence. Leverage gestures such as double tap and touch and hold to provide different variations of the same action, such as touch and hold to use a special powered up version of an attack. For multiple actions, such as walking or sprinting, consider combining the actions into a single control. + +![A graphic of a virtual button that supports both single tap and touch and hold gestures.](https://docs-assets.developer.apple.com/published/a92df74768951a55f0f4d406eedb6b4a/game-controls-power-up-action%402x.png) + +**Map movement and camera controls to predictable behavior.** Typically, players expect to control movement using the left side of their screen, and control camera direction using the right side of their screen. Maximize the amount of space that players can control both movement and the camera direction by using as large of an input area as possible. For movement control, opt to show a virtual thumbstick wherever the player lands their thumb instead of a static thumbstick position. For camera control, opt to use direct touch to pan the camera instead of a virtual thumbstick. + +![A graphic that shows placement for movement controls on the left side of the screen, and placement for camera controls on the right side of the screen.](https://docs-assets.developer.apple.com/published/7bc00774e35dd1839091df6f8c16a830/game-controls-camera-thumbstick-zones%402x.png) + +## [Physical controllers](https://developer.apple.com/design/human-interface-guidelines/game-controls#Physical-controllers) + +**Support the platformโ€™s default interaction method.** A game controller is an optional purchase, but every iPhone and iPad has a touchscreen, every Mac has a keyboard and a trackpad or mouse, every Apple TV has a remote, and every Apple Vision Pro responds to gestures people make with their eyes and hands. If you support game controllers, try to make sure thereโ€™s a fallback for using the platformโ€™s default interaction method. For developer guidance, see [Adding touch controls to games that support game controllers in iOS](https://developer.apple.com/documentation/GameController/adding-touch-controls-to-games-that-support-game-controllers-in-ios). + +**Tell people about game controller requirements.** In tvOS and visionOS, you can require the use of a physical game controller. The App Store displays a โ€œGame Controller Requiredโ€ badge to help people identify such apps. Remember that people can open your game at any time, even without a connected controller. If your app requires a game controller, check for its presence and gracefully prompt people to connect one. For developer guidance, see [`GCRequiresControllerUserInteraction`](https://developer.apple.com/documentation/BundleResources/Information-Property-List/GCRequiresControllerUserInteraction). + +**Automatically detect whether a controller is paired.** Instead of having players manually set up a physical game controller, you can automatically detect whether a controller is paired and get its profile. For developer documentation, see [Game Controller](https://developer.apple.com/documentation/GameController). + +![An illustration of a game controller with callouts that indicate the locations of the controllerโ€™s triggers, shoulder buttons, directional pad, and thumbsticks.](https://docs-assets.developer.apple.com/published/40b70c72921efd92b91da0453533baaa/game-controls-controller-anatomy%402x.png) + +**Customize onscreen content to match the connected game controller.** To simplify your gameโ€™s code, the Game Controller framework assigns standard names to controller elements based on their placement, but the colors and symbols on an actual game controller may differ. Be sure to use the connected controllerโ€™s labeling scheme when referring to controls or displaying related content in your interface. For developer guidance, see [`GCControllerElement`](https://developer.apple.com/documentation/GameController/GCControllerElement). + +**Map controller buttons to expected UI behavior.** Outside of gameplay, players expect to navigate your gameโ€™s UI in a way that matches the familiar behavior of the platform theyโ€™re playing on. When not controlling gameplay, follow these conventions across all Apple platforms: + +Button| Expected behavior for UI +---|--- +A| Activates a control +B| Cancels an action or returns to previous screen +X| โ€” +Y| โ€” +Left shoulder| Navigates left to a different screen or section +Right shoulder| Navigates right to a different screen or section +Left trigger| โ€” +Right trigger| โ€” +Left/right thumbstick| Moves selection +Directional pad| Moves selection +Home/logo| Reserved for system controls +Menu| Opens game settings or pauses gameplay + +**Support multiple connected controllers.** If there are multiple controllers connected, use labels and glyphs that match the one that the player is actively using. If your game supports multiplayer, use the appropriate labels and symbols when referring to a specific playerโ€™s controller. If you need to refer to buttons on multiple controllers, consider listing them together. + +**Prefer using symbols, not text, to refer to game controller elements.** The Game Controller framework makes [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols) available for most elements, including the buttons on various brands of game controllers. Using symbols instead of text descriptions can be especially helpful for players who arenโ€™t experienced with controllers because it doesnโ€™t require them to hunt for a specific button label during gameplay. + +![A screenshot of the SF Symbols app showing symbols in the Gaming category.](https://docs-assets.developer.apple.com/published/c76627e659aa17ab46a638437cc5d33c/game-controls-sf-symbols-gaming-category%402x.png) + +## [Keyboards](https://developer.apple.com/design/human-interface-guidelines/game-controls#Keyboards) + +Keyboard players appreciate using keyboard bindings to speed up their interactions with apps and games. + +**Prioritize single-key commands.** Single-key commands are generally easier and faster for players to perform, especially while theyโ€™re simultaneously using a mouse or trackpad. For example, you might use the first letter of a menu item as a shortcut, such as I for Inventory or M for Map; you might also map the gameโ€™s main action to the Space bar, taking advantage of the keyโ€™s relatively large size. + +**Test key binding comfort game using an Apple keyboard.** For example, if a key binding uses the Control key (^) on a non-Apple keyboard, consider remapping it to the Command key (โŒ˜) on an Apple keyboard. On Apple keyboards, the Command key is conveniently located next to the Space bar, making it especially easy to reach when players are using the W, A, S, and D keys. + +**Take the proximity of keys into account.** For example, if players navigate using the W, A, S, and D keys, consider using nearby keys to define other high-value commands. Similarly, if thereโ€™s a group of closely related actions, it can work well to map their bindings to keys that are physically close together, such as using the number keys for inventory categories. + +**Let players customize key bindings.** Although players tend to expect a reasonable set of defaults, many people need to customize a gameโ€™s key bindings for personal comfort and play style. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/game-controls#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, or tvOS. Not supported in watchOS._ + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/game-controls#visionOS) + +**Match spatial game controller behavior to hand input.** In addition to supporting a wide array of wireless game controllers, your visionOS game can also support spatial game controllers such as PlayStation VR2 Sense controller. Allow players to interact with your game in a similar manner to how they interact using their hands. Specifically, support looking at an object and pressing the controllerโ€™s left or right trigger button to indirectly interact, or reaching out and pressing the left or right trigger button to directly interact. For more information, see [visionOS](https://developer.apple.com/design/human-interface-guidelines/gestures#visionOS). + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/game-controls#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/game-controls#Related) + +[Designing for games](https://developer.apple.com/design/human-interface-guidelines/designing-for-games) + +[Gestures](https://developer.apple.com/design/human-interface-guidelines/gestures) + +[Keyboards](https://developer.apple.com/design/human-interface-guidelines/keyboards) + +[Playing haptics](https://developer.apple.com/design/human-interface-guidelines/playing-haptics) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/game-controls#Developer-documentation) + +[Create games for Apple platforms](https://developer.apple.com/games/) + +[Touch Controller](https://developer.apple.com/documentation/TouchController) + +[Game Controller](https://developer.apple.com/documentation/GameController) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/game-controls#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/2DB746B8-E0B0-4ED1-B250-902DB7A0F3E7/9196_wide_250x141_1x.jpg) Design advanced games for Apple platforms ](https://developer.apple.com/videos/play/wwdc2024/10085) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/AD3141F9-6984-4328-9388-551C8677F6A2/4973_wide_250x141_1x.jpg) Tap into virtual and physical game controllers ](https://developer.apple.com/videos/play/wwdc2021/10081) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/51863C09-0E96-4230-91A3-B85E950FBF3D/9205_wide_250x141_1x.jpg) Explore game input in visionOS ](https://developer.apple.com/videos/play/wwdc2024/10094) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/game-controls#Change-log) + +Date| Changes +---|--- +June 9, 2025| Updated touch control best practices, updated game controller mapping for UI, and added guidance for spatial game controller support in visionOS. +June 10, 2024| Added guidance for supporting touch controls and changed title from Game controllers. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/gestures.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/gestures.md new file mode 100644 index 00000000..5dd2b692 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/gestures.md @@ -0,0 +1,208 @@ +--- +title: "Gestures | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/gestures + +# Gestures + +A gesture is a physical motion that a person uses to directly affect an object in an app or game on their device. + +![A sketch of a pointing hand swiping in a curved motion toward the right, suggesting touch interaction with a device. The image is overlaid with rectangular and circular grid lines and is tinted purple to subtly reflect the purple in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/21ef165b3e1da4255ee2a9a55796afc0/inputs-gestures-intro%402x.png) + +Depending on the device theyโ€™re using, people can make gestures on a touchscreen, in the air, or on a range of input devices such as a trackpad, mouse, remote, or game controller that includes a touch surface. + +Every platform supports basic gestures like tap, swipe, and drag. Although the precise movements that make up basic gestures can vary per platform and input device, people are familiar with the underlying functionality of these gestures and expect to use them everywhere. For a list of these gestures, see [Standard gestures](https://developer.apple.com/design/human-interface-guidelines/gestures#Standard-gestures). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/gestures#Best-practices) + +**Give people more than one way to interact with your app.** People commonly prefer or need to use other inputs โ€” such as their voice, keyboard, or Switch Control โ€” to interact with their devices. Donโ€™t assume that people can use a specific gesture to perform a given task. For guidance, see [Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility). + +**In general, respond to gestures in ways that are consistent with peopleโ€™s expectations.** People expect most gestures to work the same regardless of their current context. For example, people expect tap to activate or select an object. Avoid using a familiar gesture like tap or swipe to perform an action thatโ€™s unique to your app; similarly, avoid creating a unique gesture to perform a standard action like activating a button or scrolling a long view. + +**Handle gestures as responsively as possible.** Useful gestures enhance the experience of direct manipulation and provide immediate feedback. As people perform a gesture in your app, provide feedback that helps them predict its results and, if necessary, communicates the extent and type of movement required to complete the action. + +**Indicate when a gesture isnโ€™t available.** If you donโ€™t clearly communicate why a gesture doesnโ€™t work, people might think your app has frozen or they arenโ€™t performing the gesture correctly, leading to frustration. For example, if someone tries to drag a locked object, the UI may not indicate that the objectโ€™s position has been locked; or if they try to activate an unavailable button, the buttonโ€™s unavailable state may not be clearly distinct from its available state. + +## [Custom gestures](https://developer.apple.com/design/human-interface-guidelines/gestures#Custom-gestures) + +**Add custom gestures only when necessary.** Custom gestures work best when you design them for specialized tasks that people perform frequently and that arenโ€™t covered by existing gestures, like in a game or drawing app. If you decide to implement a custom gesture, make sure itโ€™s: + + * Discoverable + + * Straightforward to perform + + * Distinct from other gestures + + * Not the only way to perform an important action in your app or game + + + + +**Make custom gestures easy to learn.** Offer moments in your app to help people quickly learn and perform custom gestures, and make sure to test your interactions in real use scenarios. If youโ€™re finding it difficult to use simple language and graphics to describe a gesture, it may mean people will find the gesture difficult to learn and perform. + +**Use shortcut gestures to supplement standard gestures, not replace them.** While you may supply a custom gesture to quickly access parts of your app, people also need simple, familiar ways to navigate and perform actions, even if it means an extra tap or two. For example, in an app that supports navigation through a hierarchy of views, people expect to find a Back button in a top toolbar that lets them return to the previous view with a single tap. To help accelerate this action, many apps also offer a shortcut gesture โ€” such as swiping from the side of a window or touchscreen โ€” while continuing to provide the Back button. + +**Avoid conflicting with gestures that access system UI.** Several platforms offer gestures for accessing system behaviors, like edge swiping in watchOS or rolling your hand over to access system overlays in visionOS. Itโ€™s important to avoid defining custom gestures that might conflict with these interactions, as people expect these controls to work consistently. In specific circumstances within games or immersive experiences, developers can work around this area by deferring the system gesture. For more information, see the platform considerations for iOS, iPadOS, watchOS, and visionOS. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/gestures#Platform-considerations) + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/gestures#iOS-iPadOS) + +In addition to the [standard gestures](https://developer.apple.com/design/human-interface-guidelines/gestures#Standard-gestures) supported in all platforms, iOS and iPadOS support a few other gestures that people expect. + +Gesture| Common action +---|--- +Three-finger swipe| Initiate undo (left swipe); initiate redo (right swipe). +Three-finger pinch| Copy selected text (pinch in); paste copied text (pinch out). +Four-finger swipe (iPadOS only)| Switch between apps. +Shake| Initiate undo; initiate redo. + +**Consider allowing simultaneous recognition of multiple gestures if it enhances the experience.** Although simultaneous gestures are unlikely to be useful in nongame apps, a game might include multiple onscreen controls โ€” such as a joystick and firing buttons โ€” that people can operate at the same time. For guidance on integrating touchscreen input with Apple Pencil input in your iPadOS app, see [Apple Pencil and Scribble](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble). + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/gestures#macOS) + +People primarily interact with macOS using a [keyboard](https://developer.apple.com/design/human-interface-guidelines/keyboards) and mouse. In addition, they can make [standard gestures](https://developer.apple.com/design/human-interface-guidelines/gestures#Standard-gestures) on a Magic Trackpad, Magic Mouse, or a [game controller](https://developer.apple.com/design/human-interface-guidelines/game-controls) that includes a touch surface. + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/gestures#tvOS) + +People expect to use [standard gestures](https://developer.apple.com/design/human-interface-guidelines/gestures#Standard-gestures) to navigate tvOS apps and games with a compatible remote, Siri Remote, or [game controller](https://developer.apple.com/design/human-interface-guidelines/game-controls) that includes a touch surface. For guidance, see [Remotes](https://developer.apple.com/design/human-interface-guidelines/remotes). + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/gestures#visionOS) + +visionOS supports two categories of gestures: indirect and direct. + +People use an _indirect_ gesture by looking at an object to target it, and then manipulating that object from a distance โ€” indirectly โ€” with their hands. For example, a person can look at a button to focus it and select it by quickly tapping their finger and thumb together. Indirect gestures are comfortable to perform at any distance, and let people quickly change focus between different objects and select items with minimal movement. + +Video with custom controls. + +Content description: A recording showing a closeup view of the top portion of a window in visionOS. A button in the window becomes highlighted. A picture-in-picture window is visible in the bottom-right corner of the recording. It shows a person's hand performing the indirect tap gesture. In response to the gesture, the highlighted button in the window activates. + +Play + +People use a _direct_ gesture to physically touch an interactive object. For example, people can directly type on the visionOS keyboard by tapping the virtual keys. Direct gestures work best when they are within reach. Because people may find it tiring to keep their arms raised for extended periods, direct gestures are best for infrequent use. visionOS also supports direct versions of all standard gestures, allowing people the choice to interact directly or indirectly with any standard component. + +Video with custom controls. + +Content description: A recording showing a table with a vertical stack of three virtual cubic blocks on it in visionOS. A person moves their hand toward the blocks from right to left, and their extended fingers touch and push aside the center block. The center block falls to the side, and the other block also tumbles onto the tabletop. + +Play + +Here are the standard direct gestures people use in visionOS; see [Specifications](https://developer.apple.com/design/human-interface-guidelines/gestures#Specifications) for a list of standard indirect gestures. + +Direct gesture| Common use +---|--- +Touch| Directly select or activate an object. +Touch and hold| Open a contextual menu. +Touch and drag| Move an object to a new location. +Double touch| Preview an object or file; select a word in an editing context. +Swipe| Reveal actions and controls; dismiss views; scroll. +With two hands, pinch and drag together or apart| Zoom in or out. +With two hands, pinch and drag in a circular motion| Rotate an object. + +**Support standard gestures everywhere you can.** For example, as soon as someone looks at an object in your app or game, tap is the first gesture theyโ€™re likely to make when they want to select or activate it. Even if you also support custom gestures, supporting standard gestures such as tap helps people get comfortable with your app or game quickly. + +**Offer both indirect and direct interactions when possible.** Prefer indirect gestures for UI and common components like buttons. Reserve direct gestures and custom gestures for objects that invite close-up interaction or specific motions in a game or interactive experience. + +**Avoid requiring specific body movements or positions for input.** Not all people can perform specific body movements or position themselves in certain ways at all times, whether due to disability, spatial constraints, or other environmental factors. If your experience requires movement, consider supporting alternative inputs to let people choose the interaction method that works best for them. + +#### [Designing custom gestures in visionOS](https://developer.apple.com/design/human-interface-guidelines/gestures#Designing-custom-gestures-in-visionOS) + +If you want to offer a specific interaction for your experience that people canโ€™t perform using an existing system gesture, consider designing a custom gesture. To offer this type of interaction, your app needs to be running in a Full Space, and you must request peopleโ€™s permission to access information about their hands. For developer guidance, see [Setting up access to ARKit data](https://developer.apple.com/documentation/visionOS/setting-up-access-to-arkit-data). + +![A screenshot of a person's hands performing a custom gesture, placing the two hands together to form a heart, while playing a visionOS game.](https://docs-assets.developer.apple.com/published/363ecbc8eeb441809f62ae935e13fbdc/visionos-custom-spatial-gesture-happy-beam%402x.png) + +**Prioritize comfort.** Continually test ergonomics of all interactions that require custom gestures. A custom interaction that requires people to keep their arms raised for even a little while can be physically tiring, and repeating very similar movements many times in succession can stress peopleโ€™s muscles and joints. + +**Carefully consider complex custom gestures that involve multiple fingers or both hands.** People may not always have both hands available when using your app or game. If you require a more complex gesture for your experience, consider also offering an alternative that requires less movement. + +**Avoid custom gestures that require using a specific hand.** It can increase someoneโ€™s cognitive load if they need to remember which hand to use to trigger a custom gesture. It may also make your experience less welcoming to people with strong hand-dominance or limb differences. + +#### [Working with system overlays in visionOS](https://developer.apple.com/design/human-interface-guidelines/gestures#Working-with-system-overlays-in-visionOS) + +In visionOS 2 and later, people can look at the palm of one hand and use gestures to quickly access system overlays for Home and Control Center. These interactions are available systemwide, and are reserved solely for accessing system overlays. + +Note + +The system overlay is the default method of accessing Control Center in visionOS 2 and later. The visionOS 1 behavior (looking upward) remains available as an accessibility setting. + +When designing apps and games that use custom gestures or anchor content to a personโ€™s hands, itโ€™s important to take interactions with the system overlays into consideration. + +**Reserve the area around a personโ€™s hand for system overlays and their related gestures.** If possible, donโ€™t anchor content to a personโ€™s hands or wrists. If youโ€™re designing a game that involves hand-anchored content, place it outside of the immediate area of someoneโ€™s hand to avoid colliding with the Home indicator. + +![An illustration of a person's open hand with the palm facing upward. A dashed circular line above the hand indicates the area reserved for system overlays.](https://docs-assets.developer.apple.com/published/de8c04a523a3e225c723c5c09c458e1c/visionos-hand-area-of-focus%402x.png)The area reserved for interacting with system overlays. + +![An illustration of a person's open hand with the palm facing upward. A button with a circle icon representing the Home indicator appears above the palm.](https://docs-assets.developer.apple.com/published/961d33f07da24b20848f3502d2cea134/visionos-spatial-gesture-home-indicator%402x.png)A person looks at their palm to reveal the Home indicator. + +![An illustration of a person's open hand with the palm facing downward. An overlay with the status bar appears above the hand.](https://docs-assets.developer.apple.com/published/f1d5a8816f65f35853ccd513355272d8/visionos-spatial-gesture-control-center%402x.png)A person turns their hand to reveal the status bar, and can tap to open Control Center. + +**Consider deferring the system overlay behavior when designing an immersive app or game.** In certain circumstances, you may not want the Home indicator to appear when someone looks at the palm of their hand. For example, a game that uses virtual hands or gloves may want to keep someone within the world of the story, even if they happen to look at their hands from different angles. In such cases, when your app is running in a Full Space, you can choose to require a tap to reveal the Home indicator instead. For developer guidance, see [`persistentSystemOverlays(_:)`](https://developer.apple.com/documentation/SwiftUI/View/persistentSystemOverlays\(_:\)). + +![An image of a person's open hand with the palm facing upward, shown from the person's perspective. A button with a circle icon representing the Home indicator appears above the palm. The image background shows the room that's the person's surroundings.](https://docs-assets.developer.apple.com/published/dc6b4a94633c063ddd432dcc8043cae3/gestures-default-home-indicator%402x.png)Default behavior in the Shared Space + +![An image of a person's open hand with the palm facing upward, shown from the person's perspective. A button with a circle icon representing the Home indicator appears above the palm. The image background shows a forest in a fully immersive space.](https://docs-assets.developer.apple.com/published/96cb708d391f1ab78a77d23c7f2e0442/gestures-home-indicator-in-immersive-space%402x.png)Default behavior in a Full Space + +![An image of a person's open hand wearing a bulky space suit glove, shown from the person's perspective. The palm faces upward, and no button appears above it. The image background shows a starry sky in a fully immersive space.](https://docs-assets.developer.apple.com/published/b978fe99b00df892890e1d194f704a83/gestures-fully-immersive-game-with-glove%402x.png)Deferred behavior in a Full Space + +Note + +Apps and games that you built for visionOS 1 defer the system overlay behavior by default. When a person looks at their palm with your app running in a Full Space, the Home indicator wonโ€™t appear unless they tap first. + +**Use caution when designing custom gestures that involve a rolling motion of the hand, wrist, and forearm.** This specific motion is reserved for revealing system overlays. Since system overlays always display on top of app content and your app isnโ€™t aware of when theyโ€™re visible, itโ€™s important to test any custom gestures or content that might conflict. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/gestures#watchOS) + +#### [Double tap](https://developer.apple.com/design/human-interface-guidelines/gestures#Double-tap) + +In watchOS 11 and later, people can use the double-tap gesture to scroll through lists and scroll views, and to advance between vertical tab views. Additionally, you can specify a toggle or button as the primary action in your app, or in your widget or Live Activity when the system displays it in the Smart Stack. Double-tapping in a view with a primary action highlights the control and then performs the action. The system also supports double tap for custom actions that you offer in [notifications](https://developer.apple.com/design/human-interface-guidelines/notifications), where it acts on the first nondestructive action in the notification. + +**Avoid setting a primary action in views with lists, scroll views, or vertical tabs.** This conflicts with the default navigation behaviors that people expect when they double-tap. + +**Choose the button that people use most commonly as the primary action in a view.** Double tap is helpful in a nonscrolling view when it performs the action that people use the most. For example, in a media controls view, you could assign the primary action to the play/pause button. For developer guidance, see [`handGestureShortcut(_:isEnabled:)`](https://developer.apple.com/documentation/SwiftUI/View/handGestureShortcut\(_:isEnabled:\)) and [`primaryAction`](https://developer.apple.com/documentation/SwiftUI/HandGestureShortcut/primaryAction). + +## [Specifications](https://developer.apple.com/design/human-interface-guidelines/gestures#Specifications) + +### [Standard gestures](https://developer.apple.com/design/human-interface-guidelines/gestures#Standard-gestures) + +The system provides APIs that support the familiar gestures people use with their devices, whether they use a touchscreen, an indirect gesture in visionOS, or an input device like a trackpad, mouse, remote, or game controller. For developer guidance, see [Gestures](https://developer.apple.com/documentation/SwiftUI/Gestures). + +Gesture| Supported in| Common action +---|---|--- +Tap| iOS, iPadOS, macOS, tvOS, visionOS, watchOS| Activate a control; select an item. +Swipe| iOS, iPadOS, macOS, tvOS, visionOS, watchOS| Reveal actions and controls; dismiss views; scroll. +Drag| iOS, iPadOS, macOS, tvOS, visionOS, watchOS| Move a UI element. +Touch (or pinch) and hold| iOS, iPadOS, tvOS, visionOS, watchOS| Reveal additional controls or functionality. +Double tap| iOS, iPadOS, macOS, tvOS, visionOS, watchOS| Zoom in; zoom out if already zoomed in; perform a primary action on Apple Watch Series 9 and Apple Watch Ultra 2. +Zoom| iOS, iPadOS, macOS, tvOS, visionOS| Zoom a view; magnify content. +Rotate| iOS, iPadOS, macOS, tvOS, visionOS| Rotate a selected item. + +For guidance on supporting additional gestures and button presses on specific input devices, see [Pointing devices](https://developer.apple.com/design/human-interface-guidelines/pointing-devices), [Remotes](https://developer.apple.com/design/human-interface-guidelines/remotes), and [Game controls](https://developer.apple.com/design/human-interface-guidelines/game-controls). + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/gestures#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/gestures#Related) + +[Feedback](https://developer.apple.com/design/human-interface-guidelines/feedback) + +[Eyes](https://developer.apple.com/design/human-interface-guidelines/eyes) + +[Playing haptics](https://developer.apple.com/design/human-interface-guidelines/playing-haptics) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/gestures#Developer-documentation) + +[Gestures](https://developer.apple.com/documentation/SwiftUI/Gestures) โ€” SwiftUI + +[`UITouch`](https://developer.apple.com/documentation/UIKit/UITouch) โ€” UIKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/gestures#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/B38CC217-7635-48EF-B8C9-F7954F390CCE/9273_wide_250x141_1x.jpg) Enhance your UI animations and transitions ](https://developer.apple.com/videos/play/wwdc2024/10145) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/C6CDCC79-CCD0-4D2F-A4D1-8FC70DC663DB/8127_wide_250x141_1x.jpg) Design for spatial input ](https://developer.apple.com/videos/play/wwdc2023/10073) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/gestures#Change-log) + +Date| Changes +---|--- +September 9, 2024| Added guidance for working with system overlays in visionOS and made organizational updates. +September 15, 2023| Updated specifications to include double tap in watchOS. +June 21, 2023| Changed page title from Touchscreen gestures and updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/gyro-and-accelerometer.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/gyro-and-accelerometer.md new file mode 100644 index 00000000..ac65f94d --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/gyro-and-accelerometer.md @@ -0,0 +1,40 @@ +--- +title: "Gyroscope and accelerometer | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/gyro-and-accelerometer + +# Gyroscope and accelerometer + +On-device gyroscopes and accelerometers can supply data about a deviceโ€™s movement in the physical world. + +![A sketch of a gyroscope, suggesting movement. The image is overlaid with rectangular and circular grid lines and is tinted purple to subtly reflect the purple in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/d095e989767ecf0537fa99b6ea46b50a/inputs-gyroscope-intro%402x.png) + +You can use accelerometer and gyroscope data to provide experiences based on real-time, motion-based information in apps and games that run in iOS, iPadOS, and watchOS. tvOS apps can use gyroscope data from the Siri Remote. For developer guidance, see [Core Motion](https://developer.apple.com/documentation/CoreMotion). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/gyro-and-accelerometer#Best-practices) + +**Use motion data only to offer a tangible benefit to people.** For example, a fitness app might use the data to provide feedback about peopleโ€™s activity and general health, and a game might use the data to enhance gameplay. Avoid gathering data simply to have the data. + +Important + +If your experience needs to access motion data from a device, you must provide copy that explains why. The first time your app or game tries to access this type of data, the system includes your copy in a permission request, where people can grant or deny access. + +**Outside of active gameplay, avoid using accelerometers or gyroscopes for the direct manipulation of your interface.** Some motion-based gestures may be difficult to replicate precisely, may be physically challenging for some people to perform, and may affect battery usage. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/gyro-and-accelerometer#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/gyro-and-accelerometer#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/gyro-and-accelerometer#Related) + +[Feedback](https://developer.apple.com/design/human-interface-guidelines/feedback) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/gyro-and-accelerometer#Developer-documentation) + +[Getting processed device-motion data](https://developer.apple.com/documentation/CoreMotion/getting-processed-device-motion-data) โ€” Core Motion + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/gyro-and-accelerometer#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/5077B5B0-643B-4E31-9C5E-6E766326D3F3/5225_wide_250x141_1x.jpg) Measure health with motion ](https://developer.apple.com/videos/play/wwdc2021/10287) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/keyboards.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/keyboards.md new file mode 100644 index 00000000..7e85e5df --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/keyboards.md @@ -0,0 +1,234 @@ +--- +title: "Keyboards | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/keyboards + +# Keyboards + +A physical keyboard can be an essential input device for entering text, playing games, controlling apps, and more. + +![A sketch of a keyboard, suggesting keyboard input. The image is overlaid with rectangular and circular grid lines and is tinted purple to subtly reflect the purple in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/041dcf36a378d11a3727a6ff04989365/inputs-keyboard-intro%402x.png) + +People can connect a physical keyboard to any device except Apple Watch. Mac users tend to use a physical keyboard all the time and iPad users often do. Many games work well with a physical keyboard, and people can prefer using one instead of a [virtual keyboard](https://developer.apple.com/design/human-interface-guidelines/virtual-keyboards) when entering a lot of text. + +Keyboard users often appreciate using keyboard shortcuts to speed up their interactions with apps and games. A _keyboard shortcut_ is a combination of a primary key and one or more modifier keys (Control, Option, Shift, and Command) that map to a specific command. A keyboard shortcut in a game โ€” called a _key binding_ โ€” often consists of a single key. + +Apple defines standard keyboard shortcuts to work consistently across the system and most apps, helping people transfer their knowledge to new experiences. Some apps define custom keyboard shortcuts for the app-specific commands people use most; most games define custom key bindings that make it quick and efficient to use the keyboard to control the game. For guidance, see [Game controls](https://developer.apple.com/design/human-interface-guidelines/game-controls#Keyboards). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/keyboards#Best-practices) + +**Support Full Keyboard Access when possible.** Available in iOS, iPadOS, macOS, and visionOS, Full Keyboard Access lets people navigate and activate windows, menus, controls, and system features using only the keyboard. To test Full Keyboard Access in your app or game, turn it on in the Accessibility area of the system-supplied Settings app. For developer guidance, see [Support Full Keyboard Access in your iOS app](https://developer.apple.com/videos/play/wwdc2021/10120/) and [`isFullKeyboardAccessEnabled`](https://developer.apple.com/documentation/AppKit/NSApplication/isFullKeyboardAccessEnabled). + +Important + +Although iPadOS supports keyboard navigation in text fields, text views, and sidebars, and provides APIs you can use to support it in collection views and other custom views, avoid supporting keyboard navigation for controls, such as buttons, segmented controls, and switches. Instead, let people use Full Keyboard Access to activate controls, navigate to all onscreen components, and perform gesture-based interactions like drag and drop. For guidance, see [iPadOS](https://developer.apple.com/design/human-interface-guidelines/focus-and-selection#iPadOS); for developer guidance, see [Focus-based navigation](https://developer.apple.com/documentation/uikit/focus-based_navigation). + +**Respect standard keyboard shortcuts.** While using most apps, people generally expect to rely on the standard keyboard shortcuts that work in other apps and throughout the system. If your app offers a unique action that people perform frequently, prefer creating a [custom](https://developer.apple.com/design/human-interface-guidelines/keyboards#Custom-keyboard-shortcuts) shortcut for it instead of repurposing a standard one that people associate with a different action. While playing a game, people may expect to use certain standard keyboard shortcuts โ€” such as Commandโ€“Q to quit the game โ€” but they also expect to be able to modify each gameโ€™s key bindings to fit their personal play style. For guidance, see [Game controls](https://developer.apple.com/design/human-interface-guidelines/game-controls#Keyboards). + +## [Standard keyboard shortcuts](https://developer.apple.com/design/human-interface-guidelines/keyboards#Standard-keyboard-shortcuts) + +**In general, donโ€™t repurpose standard keyboard shortcuts for custom actions.** People can get confused when the shortcuts they know work differently in your app or game. Only consider redefining a standard shortcut if its action doesnโ€™t make sense in your experience. For example, if your app doesnโ€™t support text editing, it doesnโ€™t need a text-styling command like Italic, so you might repurpose Commandโ€“I for an action that has more relevance, like Get Info. + +People expect each of the following standard keyboard shortcuts to perform the action listed in the table below. + +Primary key| Keyboard shortcut| Action +---|---|--- +Space| Command-Space| Show or hide the Spotlight search field. +| Shift-Command-Space| Varies. +| Option-Command-Space| Show the Spotlight search results window. +| Control-Command-Space| Show the Special Characters window. +Tab| Shift-Tab| Navigate through controls in a reverse direction. +| Command-Tab| Move forward to the next most recently used app in a list of open apps. +| Shift-Command-Tab| Move backward through a list of open apps (sorted by recent use). +| Control-Tab| Move focus to the next group of controls in a dialog or the next table (when Tab moves to the next cell). +| Control-Shift-Tab| Move focus to the previous group of controls. +Esc| Esc| Cancel the current action or process. +Esc| Option-Command-Esc| Open the Force Quit dialog. +Eject| Control-Command-Eject| Quit all apps (after changes have been saved to open documents) and restart the computer. +| Control-Option-Command-Eject| Quit all apps (after changes have been saved to open documents) and shut the computer down. +F1| Control-F1| Toggle full keyboard access on or off. +F2| Control-F2| Move focus to the menu bar. +F3| Control- F3| Move focus to the Dock. +F4| Control-F4| Move focus to the active (or next) window. +| Control-Shift-F4| Move focus to the previously active window. +F5| Control-F5| Move focus to the toolbar. +| Command-F5| Turn VoiceOver on or off. +F6| Control-F6| Move focus to the first (or next) panel. +| Control-Shift-F6| Move focus to the previous panel. +F7| Control-F7| Temporarily override the current keyboard access mode in windows and dialogs. +F8| | Varies. +F9| | Varies. +F10| | Varies. +F11| | Show desktop. +F12| | Hide or display Dashboard. +Grave accent (`)| Command-Grave accent| Activate the next open window in the frontmost app. +| Shift-Command-Grave accent| Activate the previous open window in the frontmost app. +| Option-Command-Grave accent| Move focus to the window drawer. +Hyphen (-)| Command-Hyphen| Decrease the size of the selection. +| Option-Command-Hyphen| Zoom out when screen zooming is on. +Left bracket ({)| Command-Left bracket| Left-align a selection. +Right bracket (})| Command-Right bracket| Right-align a selection. +Pipe (|)| Command-Pipe| Center-align a selection. +Colon (:)| Command-Colon| Display the Spelling window. +Semicolon (;)| Command-Semicolon| Find misspelled words in the document. +Comma (,)| Command-Comma| Open the appโ€™s settings window. +| Control-Option-Command-Comma| Decrease screen contrast. +Period (.)| Command-Period| Cancel an operation. +| Control-Option-Command-Period| Increase screen contrast. +Question mark (?)| Command-Question mark| Open the appโ€™s Help menu. +Forward slash (/)| Option-Command-Forward slash| Turn font smoothing on or off. +Equal sign (=)| Shift-Command-Equal sign| Increase the size of the selection. +| Option-Command-Equal sign| Zoom in when screen zooming is on. +3| Shift-Command-3| Capture the screen to a file. +| Control-Shift-Command-3| Capture the screen to the Clipboard. +4| Shift-Command-4| Capture a selection to a file. +| Control-Shift-Command-4| Capture a selection to the Clipboard. +8| Option-Command-8| Turn screen zooming on or off. +| Control-Option-Command-8| Invert the screen colors. +A| Command-A| Select every item in a document or window, or all characters in a text field. +| Shift-Command-A| Deselect all selections or characters. +B| Command-B| Boldface the selected text or toggle boldfaced text on and off. +C| Command-C| Copy the selection to the Clipboard. +| Shift-Command-C| Display the Colors window. +| Option-Command-C| Copy the style of the selected text. +| Control-Command-C| Copy the formatting settings of the selection and store on the Clipboard. +D| Option-Command-D| Show or hide the Dock. +| Control-Command-D| Display the definition of the selected word in the Dictionary app. +E| Command-E| Use the selection for a find operation. +F| Command-F| Open a Find window. +| Option-Command-F| Jump to the search field control. +| Control-Command-F| Enter full screen. +G| Command-G| Find the next occurrence of the selection. +| Shift-Command-G| Find the previous occurrence of the selection. +H| Command-H| Hide the windows of the currently running app. +| Option-Command-H| Hide the windows of all other running apps. +I| Command-I| Italicize the selected text or toggle italic text on or off. +| Command-I| Display an Info window. +| Option-Command-I| Display an inspector window. +J| Command-J| Scroll to a selection. +M| Command-M| Minimize the active window to the Dock. +| Option-Command-M| Minimize all windows of the active app to the Dock. +N| Command-N| Open a new document. +O| Command-O| Display a dialog for choosing a document to open. +P| Command-P| Display the Print dialog. +| Shift-Command-P| Display the Page Setup dialog. +Q| Command-Q| Quit the app. +| Shift-Command-Q| Log out the person currently logged in. +| Option-Shift-Command-Q| Log out the person currently logged in without confirmation. +S| Command-S| Save a new document or save a version of a document. +| Shift-Command-S| Duplicate the active document or initiate a Save As. +T| Command-T| Display the Fonts window. +| Option-Command-T| Show or hide a toolbar. +U| Command-U| Underline the selected text or turn underlining on or off. +V| Command-V| Paste the Clipboard contents at the insertion point. +| Shift-Command-V| Paste as (Paste as Quotation, for example). +| Option-Command-V| Apply the style of one object to the selection. +| Option-Shift-Command-V| Paste the Clipboard contents at the insertion point and apply the style of the surrounding text to the inserted object. +| Control-Command-V| Apply formatting settings to the selection. +W| Command-W| Close the active window. +| Shift-Command-W| Close a file and its associated windows. +| Option-Command-W| Close all windows in the app. +X| Command-X| Remove the selection and store on the Clipboard. +Z| Command-Z| Undo the previous operation. +| Shift-Command-Z| Redo (when Undo and Redo are separate commands rather than toggled using Command-Z). +Right arrow| Command-Right arrow| Change the keyboard layout to current layout of Roman script. +| Shift-Command-Right arrow| Extend selection to the next semantic unit, typically the end of the current line. +| Shift-Right arrow| Extend selection one character to the right. +| Option-Shift-Right arrow| Extend selection to the end of the current word, then to the end of the next word. +| Control-Right arrow| Move focus to another value or cell within a view, such as a table. +Left arrow| Command-Left arrow| Change the keyboard layout to current layout of system script. +| Shift-Command-Left arrow| Extend selection to the previous semantic unit, typically the beginning of the current line. +| Shift-Left arrow| Extend selection one character to the left. +| Option-Shift-Left arrow| Extend selection to the beginning of the current word, then to the beginning of the previous word. +| Control-Left arrow| Move focus to another value or cell within a view, such as a table. +Up arrow| Shift-Command-Up arrow| Extend selection upward in the next semantic unit, typically the beginning of the document. +| Shift-Up arrow| Extend selection to the line above, to the nearest character boundary at the same horizontal location. +| Option-Shift-Up arrow| Extend selection to the beginning of the current paragraph, then to the beginning of the next paragraph. +| Control-Up arrow| Move focus to another value or cell within a view, such as a table. +Down arrow| Shift-Command-Down arrow| Extend selection downward in the next semantic unit, typically the end of the document. +| Shift-Down arrow| Extend selection to the line below, to the nearest character boundary at the same horizontal location. +| Option-Shift-Down arrow| Extend selection to the end of the current paragraph, then to the end of the next paragraph (include the paragraph terminator, such as Return, in cut, copy, and paste operations). +| Control-Down arrow| Move focus to another value or cell within a view, such as a table. + +The system also defines several keyboard shortcuts for use with localized versions of the system, localized keyboards, keyboard layouts, and input methods. These shortcuts donโ€™t correspond directly to menu commands. + +Keyboard shortcut| Action +---|--- +Control-Space| Toggle between the current and last input source. +Control-Option-Space| Switch to the next input source in the list. +[Modifier key]-Command-Space| Varies. +Command-Right arrow| Change keyboard layout to current layout of Roman script. +Command-Left arrow| Change keyboard layout to current layout of system script. + +## [Custom keyboard shortcuts](https://developer.apple.com/design/human-interface-guidelines/keyboards#Custom-keyboard-shortcuts) + +**Define custom keyboard shortcuts for only the most frequently used app-specific commands.** People appreciate using keyboard shortcuts for actions they perform frequently, but defining too many new shortcuts can make your app seem difficult to learn. + +**Use modifier keys in ways that people expect.** For example, pressing Command while dragging moves items as a group, and pressing Shift while drag-resizing constrains resizing to the itemโ€™s aspect ratio. In addition, holding an arrow key moves the selected item by the smallest app-defined unit of distance until people release the key. + +Here are the modifier keys and the symbols that represent them. + +Modifier key| Symbol| Recommended usage +---|---|--- +Command| ![Outline of a stylized clover shape.](https://docs-assets.developer.apple.com/published/43dd468e7f303fbaa3abbf3935292ae2/Keyboard_Command.svg)| Prefer the Command key as the main modifier key in a custom keyboard shortcut. +Shift| ![Outline of an upward-pointing arrow.](https://docs-assets.developer.apple.com/published/3a7e5aed7275031a8c41a7fb7789e41f/Keyboard_Shift.svg)| Prefer the Shift key as a secondary modifier that complements a related shortcut. +Option| ![Line segments that suggest a horizontally transformed Z shape combined with a short horizontal segment aligned with the top of the Z.](https://docs-assets.developer.apple.com/published/8b064ad029d2012128a6aaeb1322b290/Keyboard_Option.svg)| Use the Option modifier sparingly for less-common commands or power features. +Control| ![A shallow, upside-down V shape.](https://docs-assets.developer.apple.com/published/5c92c8350588d52ff786bf763b18e9e7/Keyboard_Control.svg)| Avoid using the Control key as a modifier. The system uses Control in many systemwide features and shortcuts, like moving focus or capturing screenshots. + +Tip + +Some languages require modifier keys to generate certain characters. For example, on a French keyboard, Option-5 generates the โ€œ{โ€œ character. Itโ€™s usually safe to use the Command key as a modifier, but avoid using an additional modifier with characters that arenโ€™t available on all keyboards. If you must use a modifier other than Command, prefer using it only with the alphabetic characters. + +**List modifier keys in the correct order.** If you use more than one modifier key in a custom shortcut, always list them in this order: Control, Option, Shift, Command. + +**Avoid adding Shift to a shortcut that uses the upper character of a two-character key.** People already understand that they must hold the Shift key to type the upper character of a two-character key, so itโ€™s clearer to simply list the upper character in the shortcut. For example, the keyboard shortcut for Hide Status Bar is Command-Slash, whereas the keyboard shortcut for Help is Command-Question mark, not Shift-Command-Slash. + +**Let the system localize and mirror your keyboard shortcuts as needed.** The system automatically localizes a shortcutโ€™s primary and modifier keys to support the currently connected keyboard; if your app or game switches to a right-to-left layout, the system automatically mirrors the shortcut. For guidance, see [Right to left](https://developer.apple.com/design/human-interface-guidelines/right-to-left). + +**Avoid creating a new shortcut by adding a modifier to an existing shortcut for an unrelated command.** For example, because people are accustomed to using Command-Z for undoing an action, it would be confusing to use Shift-Command-Z as the shortcut for a command thatโ€™s unrelated to undo and redo. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/keyboards#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, or tvOS. Not supported in watchOS._ + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/keyboards#visionOS) + +In visionOS, an appโ€™s keyboard shortcuts appear in the shortcut interface that displays when people hold the Command key on a connected keyboard. Similar in organization to an appโ€™s [menu bar menus](https://developer.apple.com/design/human-interface-guidelines/the-menu-bar) on iPad or Mac, the shortcut interface on Apple Vision Pro displays app commands in familiar system-defined menu categories such as File, Edit, and View. Unlike menu bar menus, the shortcut interface displays all relevant categories in one view, listing within each category only available commands that also have shortcuts. + +**Write descriptive shortcut titles.** Because the shortcut interface displays a flat list of all items in each category, submenu titles arenโ€™t available to provide context for their child items. Make sure each shortcut title is descriptive enough to convey its action without the additional context a submenu title might provide. For developer guidance, see [`discoverabilityTitle`](https://developer.apple.com/documentation/UIKit/UIKeyCommand/discoverabilityTitle). + +**Recognize that people see an overlay when they use a physical keyboard with your visionOS app or game.** When people connect a physical keyboard while using your visionOS app or game, the system displays a virtual keyboard overlay that provides typing completion and other controls. + +Video with custom controls. + +Content description: A recording that shows two hands typing on a physical keyboard while the person runs an app in visionOS. A virtual window is visible above the physical keyboard, and displays the entered text and suggestions. + +Play + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/keyboards#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/keyboards#Related) + +[Virtual keyboards](https://developer.apple.com/design/human-interface-guidelines/virtual-keyboards) + +[Entering data](https://developer.apple.com/design/human-interface-guidelines/entering-data) + +[Pointing devices](https://developer.apple.com/design/human-interface-guidelines/pointing-devices) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/keyboards#Developer-documentation) + +[`KeyboardShortcut`](https://developer.apple.com/documentation/SwiftUI/KeyboardShortcut) โ€” SwiftUI + +[Input events](https://developer.apple.com/documentation/SwiftUI/Input-events) โ€” SwiftUI + +[Handling key presses made on a physical keyboard](https://developer.apple.com/documentation/UIKit/handling-key-presses-made-on-a-physical-keyboard) โ€” UIKit + +[Mouse, Keyboard, and Trackpad](https://developer.apple.com/documentation/AppKit/mouse-keyboard-and-trackpad) โ€” AppKit + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/keyboards#Change-log) + +Date| Changes +---|--- +June 9, 2025| Moved game-specific key bindings guidance to the Game controls page. +June 10, 2024| Added game-specific guidance and made organizational updates. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/nearby-interactions.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/nearby-interactions.md new file mode 100644 index 00000000..b68624b1 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/nearby-interactions.md @@ -0,0 +1,70 @@ +--- +title: "Nearby interactions | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/nearby-interactions + +# Nearby interactions + +Nearby interactions support on-device experiences that integrate the presence of people and objects in the nearby environment. + +![A sketch of curved lines beside a circular area containing a smaller circle, suggesting audio approaching a person in a room from a specific direction. The image is overlaid with rectangular and circular grid lines and is tinted purple to subtly reflect the purple in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/4ee9418314d3a8bbdc8e7586a9e3c787/inputs-nearby-interactions-intro%402x.png) + +A great nearby interaction feels intuitive and natural to people, because it builds on their innate awareness of the world around them. For example, a person playing music on their iPhone can continue listening on their HomePod mini when they bring the devices close together, simply by transferring the audio output from their iPhone to the HomePod mini. + +Nearby interactions are available on devices that support Ultra Wideband technology (to learn more, see [Ultra Wideband availability](https://support.apple.com/en-us/HT212274)), and rely on the [Nearby Interaction](https://developer.apple.com/documentation/NearbyInteraction) framework. Before participating in nearby interaction experiences, people grant permission for their device to interact while theyโ€™re using your app. The Nearby Interaction APIs help you preserve peopleโ€™s privacy by relying on randomly generated device identifiers that last only as long as the interaction session your app initiates. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Best-practices) + +**Consider a task from the perspective of the physical world to find inspiration for a nearby interaction.** For example, although people can easily use your appโ€™s UI to transfer a song from their iPhone to their HomePod mini, initiating the transfer by bringing the devices close together makes the task feel rooted in the physical world. Discovering the physical actions that inform the concept of a task can help you create an engaging experience that makes performing it feel easy and natural. + +**Use distance, direction, and context to inform an interaction.** Although your app may get information from a variety of sources, prioritizing nearby, contextually relevant information can help you deliver experiences that feel organic. For example, if people want to share content with a friend in a crowded room, the iOS share sheet can suggest a likely recipient by using on-device knowledge about the personโ€™s most frequent and recent contacts. Combining this knowledge with information from nearby devices that include the U1 chip can let the share sheet improve the experience by suggesting the closest contact the person is facing. + +**Consider how changes in physical distance can guide a nearby interaction.** In the physical world, people generally expect their perception of an object to sharpen as they get closer to it. A nearby interaction can mirror this experience by providing feedback that changes with the proximity of an object. For example, when people use iPhone to find an AirTag, the display transitions from a directional arrow to a pulsing circle as they get closer. + +**Provide continuous feedback.** Continuous feedback reflects the dynamism of the physical world and strengthens the connection between a nearby interaction and the task people are performing. For example, when looking for a lost item in Find My, people get continuous updates that communicate the itemโ€™s direction and proximity. Keep people engaged by providing uninterrupted feedback that responds to their movements. + +**Consider using multiple feedback types to create a holistic experience.** Fluidly transitioning among visual, audible, and haptic feedback can help a nearby interactionโ€™s task feel more engaging and real. Using more than one type of feedback also lets you vary the experience to coordinate with both the task and the current context. For example, while people are interacting with the device screen, visual feedback makes sense; while people are interacting with their environment, audible and haptic feedback often work better. + +**Avoid using a nearby interaction as the only way to perform a task.** You canโ€™t assume that everyone can experience a nearby interaction, so itโ€™s essential to provide alternative ways to get things done in your app. + +## [Device usage](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Device-usage) + +**Encourage people to hold the device in portrait orientation.** Holding a device in landscape can decrease the accuracy and availability of information about the distance and relative direction of other devices. If you support only portrait orientation while your nearby interaction feature runs, prefer giving people implicit, visual feedback on how to hold the device for an optimal experience; when possible, avoid explicitly telling people to hold the device in portrait. + +**Design for the deviceโ€™s directional field of view.** Nearby interaction relies on a hardware sensor with a specific field of view similar to that of the Ultra Wide camera in iPhone 11 and later. If a participating device is outside of this field of view, your app might receive information about its distance, but not its relative direction. + +**Help people understand how intervening objects can affect the nearby interaction experience in your app.** When other people, animals, or sufficiently large objects come between two participating devices, the accuracy or availability of distance and direction information can decrease. Consider adding advice on avoiding this situation to onboarding or tutorial content you present. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Platform-considerations) + + _No additional considerations for iPadOS. Not supported in macOS, tvOS, or visionOS._ + +### [iOS](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#iOS) + +On iPhone, Nearby Interaction APIs provide a peer deviceโ€™s distance and direction. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#watchOS) + +On Apple Watch, Nearby Interaction APIs provide a peer deviceโ€™s distance. Also, all watchOS apps participating in a nearby interaction experience must be in the foreground. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Related) + +[Feedback](https://developer.apple.com/design/human-interface-guidelines/feedback) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Developer-documentation) + +[Nearby Interaction](https://developer.apple.com/documentation/NearbyInteraction) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/0F487599-C14E-48B0-AEB0-A752DFF26E95/5165_wide_250x141_1x.jpg) Design for spatial interaction ](https://developer.apple.com/videos/play/wwdc2021/10245) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/49/E6812719-14BF-4392-84FC-E1CFC1650B71/3558_wide_250x141_1x.jpg) Meet Nearby Interaction ](https://developer.apple.com/videos/play/wwdc2020/10668) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Change-log) + +Date| Changes +---|--- +June 21, 2023| Changed page title from Spatial interactions. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/pointing-devices.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/pointing-devices.md new file mode 100644 index 00000000..8a5362d8 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/pointing-devices.md @@ -0,0 +1,237 @@ +--- +title: "Pointing devices | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/pointing-devices + +# Pointing devices + +People can use a pointing device like a trackpad or mouse to navigate the interface and initiate actions. + +![A sketch of an arrow-shaped pointer, suggesting use of a mouse or trackpad. The image is overlaid with rectangular and circular grid lines and is tinted purple to subtly reflect the purple in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/d62ce652f0470403da6dfbad1b1ad2b0/inputs-pointing-devices-intro%402x.png) + +People appreciate the precision and flexibility that pointing devices offer. On a Mac, people typically expect to combine a pointing device with a keyboard as they navigate apps and the system. On iPad and Apple Vision Pro, a pointing device gives people an additional way to interact with apps and content, without replacing touch, eyes, or gestures. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#Best-practices) + +**Be consistent when responding to mouse and trackpad gestures.** People expect most gestures to work the same throughout the system, regardless of the app or game theyโ€™re using. On a Mac, for example, people rely on the โ€œSwipe between pagesโ€ gesture to behave the same way whether theyโ€™re browsing individual document pages, webpages, or images. + +**Avoid redefining systemwide trackpad gestures.** Even in a game that uses app-specific gestures in a custom way, people expect systemwide gestures to be available; for example, people expect to make familiar gestures to reveal the Dock or Mission Control in macOS. Remember that Mac users can customize the gestures for performing systemwide actions. + +**Provide a consistent experience in your app, whether people are using gestures, eyes, a pointing device, or a keyboard.** People expect to move fluidly between multiple types of input, and they donโ€™t want to learn different interactions for each mode or for each app they use. + +**Let people use the pointer to reveal and hide controls that automatically minimize or fade out.** In iPadOS, for example, people can reveal the minimized Safari toolbar by holding the pointer over it (the toolbar minimizes again when the pointer moves away). People can also move the pointer to reveal or hide playback controls while they watch a full-screen video. + +**Provide a consistent experience when people press and hold a modifier key while interacting with objects in your app.** For example, if people can duplicate an object by pressing and holding the Option key while they drag that object, ensure the result is the same whether they drag using touch or the pointer. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#Platform-considerations) + + _No additional considerations for iOS. Not supported in tvOS or watchOS._ + +### [iPadOS](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#iPadOS) + +iPadOS builds on the traditional pointer experience, automatically adapting the pointer to the current context and providing rich visual feedback at a level of precision that enhances productivity and simplifies common tasks on a touchscreen device. The iPadOS pointing system gives people an additional way to interact with apps and content โ€” it doesnโ€™t replace touch. + +**Allow multiple selection in custom views when necessary.** In iPadOS 15 and later, people can click and drag the pointer over multiple items to select them. As people use the pointer in this way, it expands into a visible rectangle that selects the items it encompasses. Standard nonlist collection views support this interaction by default; if you want to support multiple selection in custom views, you need to implement it yourself. For developer guidance, see [`UIBandSelectionInteraction`](https://developer.apple.com/documentation/UIKit/UIBandSelectionInteraction). + +**Distinguish between pointer and finger input only if it provides value.** For example, a scrubber can give people an additional way to target a location in a video when theyโ€™re using the pointer. In this scenario, people can drag the playhead using either the pointer or touch, but they can use the pointer to click a precise seek destination. + +#### [Pointer shape and content effects](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#Pointer-shape-and-content-effects) + +iPadOS integrates the appearance and behavior of both the pointer and the element it moves over, bringing focus to the item the pointer is targeting. You can support the system-provided pointer effects or modify them to suit your experience. + +By default, the pointerโ€™s shape is a circle, but it can display a system-defined or custom shape when people move it over specific elements or regions. For example, the pointer automatically uses the familiar I-beam shape when people move it over a text-entry area. + +Video with custom controls. + +Content description: A video snippet showing the bottom half of a new event popover in Calendar. At the beginning of the video, the pointer is within the URL field and it uses the I-beam shape. As the pointer moves between the URL and Notes fields, it briefly reverts to its default circular shape; when the pointer enters the Notes field, it uses the I-beam shape again. + +Play + +With a _content effect_ , the UI element or region beneath the pointer can also change its appearance when people hold the pointer over it. Depending on the type of content effect, the pointer can retain its current shape or transform into a shape that integrates with the elementโ€™s new appearance. + +iPadOS defines three content effects that bring focus to different types of interactive elements in your app: highlight, lift, and hover. + +The _highlight_ effect transforms the pointer into a translucent, rounded rectangle that acts as a background for a control and includes a gentle parallax. The subtle highlighting and movement bring focus to the control without distracting people from their task. By default, iPadOS applies the highlight effect to bar buttons, tab bars, segmented controls, and edit menus. + +Video with custom controls. + +Content description: A video snippet showing a small area at the bottom of a Photos window. Nature photos that show purple flowers, rocks in a stream, and grass are visible just above the tab bar, which shows the Photos and For You tabs. At the beginning of the video, the Photos tab is highlighted. Because bar items receive the highlight effect, the pointer becomes the highlighted rounded rectangle that surrounds the tabโ€™s glyph and title. The highlighted rounded rectangle slides from one tab to the other as the pointer moves. + +Play + +The _lift_ effect combines a subtle parallax with the appearance of elevation to make an element seem like itโ€™s floating above the screen. As the pointer fades out beneath the element, iPadOS creates the illusion of lift by scaling the element up while adding a shadow below it and a soft specular highlight on top of it. By default, iPadOS applies the lift effect to app icons and to buttons in Control Center. + +Video with custom controls. + +Content description: A video snippet showing the left end of the Dock in front of the Home Screen. From the left, the visible app icons are Messages, Safari, Music, Mail, and Files. As the pointer moves across the first three icons from the left, it disappears beneath each icon in turn, lifting it slightly and letting it return to its original position before moving to the next icon. + +Play + +_Hover_ is a generic effect that lets you apply custom scale, tint, or shadow values to an element as the pointer moves over it. The hover effect combines your custom values to bring focus to an item, but it doesnโ€™t transform the default pointer shape. + +Video with custom controls. + +Content description: A video snippet showing an alert floating above the top half of a new event popover in Calendar. The alert contains text that reads Are you sure you want to discard this new event? and a button titled Discard Changes. As the pointer moves into the alert button, the button background darkens. + +Play + +#### [Pointer accessories](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#Pointer-accessories) + +Pointer accessories are visual indicators that help people understand how they can use the pointer to interact with the current UI element. For example, a pointer approaching a resizable element might display small arrows to indicate that the element allows resizing along a certain axis. + +Unlike pointer shapes and content effects, accessories are secondary items that can combine with any pointer to communicate additional information. For developer guidance, see [`UIPointerAccessory`](https://developer.apple.com/documentation/UIKit/UIPointerAccessory). + +**Use clear, simple images to create custom accessories.** A pointer accessory is small, so itโ€™s essential to create an image that communicates the pointer interaction without using too many details. + +**Consider using the accessory transition to signal a change in an elementโ€™s state or behavior.** In addition to animating the appearance and disappearance of pointer accessories, the system also animates the transitions among accessory shapes and positions that can accompany content effects. For example, you could communicate that an add action has become unavailable by transitioning the pointer accessory from the `plus` symbol to the `circle.slash` symbol. + +#### [Pointer magnetism](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#Pointer-magnetism) + +iPadOS helps people use the pointer to target an element by making the element appear to attract the pointer. People can experience this magnetic effect when they move the pointer close to an element and when they flick the pointer toward an element. + +When people move the pointer close to an element, the system starts transforming the pointerโ€™s shape as soon as it reaches the elementโ€™s hit region. Because an elementโ€™s hit region typically extends beyond its visible boundaries, the pointer begins to transform before it appears to touch the element itself, creating the illusion that the element is pulling the pointer toward it. + +Video with custom controls. + +Content description: A video snippet showing an area at the bottom of Clock. The World Clock tab is selected and clock images and information for San Francisco, New York, and London are partially visible in the window. As the pointer moves in the tab bar, its highlighted rounded rectangle appearance seems to show a slight resistance as it slides from the World Clock tab to the Alarm tab and back again. + +Play + +When people flick the pointer toward an element, iPadOS examines the pointerโ€™s trajectory to discover the element thatโ€™s the most likely target. When thereโ€™s an element in the pointerโ€™s path, the system uses magnetism to pull the pointer toward the elementโ€™s center. + +By default, iPadOS applies magnetism to elements that use the lift effect (like app icons) and the highlight effect (like bar buttons), but not to elements that use hover. Because an element that supports hover doesnโ€™t transform the default pointer shape, adding magnetism could be jarring and might make people feel that theyโ€™ve lost control of the pointer. + +The system also applies magnetism to text-entry areas, where it can help people avoid skipping to another line if they make unintended vertical movements while theyโ€™re selecting text. + +#### [Standard pointers and effects](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#Standard-pointers-and-effects) + +**When possible, support the system-provided content effects.** People quickly become accustomed to the content effects they see throughout the system and generally expect their experience to apply to every app they use. To provide a consistent user experience, align your interactions with the design intent of each effect. Specifically: + + * Use highlight for a small element that has a transparent background. + + * Use lift for a small element that has an opaque background. + + * Use hover for large elements and customize the scale, tint, and shadow attributes as needed (for guidance, see [Customizing pointers](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#Customizing-pointers)). + + + + +**Prefer the system-provided pointer appearances for standard buttons and text-entry areas.** You can help people feel more comfortable with your app when the pointer behaves in ways they expect. + +**Add padding around interactive elements to create comfortable hit regions.** You might need to experiment to determine the right size for an elementโ€™s hit region. If the hit region is too small, it can make people feel that they have to be extra precise when interacting with the element. On the other hand, when an elementโ€™s hit region is too large, people can feel that it takes a lot of effort to pull the pointer away from the element. In general, it works well to add about 12 points of padding around elements that include a bezel; for elements without a bezel, it works well to add about 24 points of padding around the elementโ€™s visible edges. + +![An illustration of a button that has a filled, rounded-rectangle bezel. The button is centered on top of a shaded rectangle that extends beyond the button by the same distance on all sides. Centered on each side, a callout indicates that the padding between the button and each edge of the shaded rectangle is 12 points.](https://docs-assets.developer.apple.com/published/3993cfe0b8ec1f79e7c27496d92b240e/padding-for-button-with-bezel%402x.png) + +![An illustration of a symbol centered on top of a shaded rectangle that extends beyond the symbol by the same distance on all sides. Centered on each side, a callout indicates that the padding between the symbol and each edge of the shaded rectangle is 24 points.](https://docs-assets.developer.apple.com/published/58bee8289c0508cc5b9e83f030925cb6/padding-for-glyph%402x.png) + +![An illustration of a button without a bezel, centered on top of a shaded rectangle that extends beyond the button by the same distance on all sides. Centered on each side, a callout indicates that the padding between the button and each edge of the shaded rectangle is 24 points.](https://docs-assets.developer.apple.com/published/5a79ca3d0a9d4bbd3bf71c23bf8c5da3/padding-for-button-without-bezel%402x.png) + +**Create contiguous hit regions for custom bar buttons.** If thereโ€™s space between the hit regions of adjacent buttons in a bar, people may experience a distracting motion when the pointer reverts briefly to its default shape as it moves between buttons. + +**Specify the corner radius of a nonstandard element that receives the lift effect.** With the system-provided lift effect, the pointer transforms to match the elementโ€™s shape as it fades out. By default, the pointer uses the system-defined corner radius to transform into a rounded rectangle. If your element is a different shape โ€” if itโ€™s a circle, for example โ€” you need to provide the radius so the pointer can animate seamlessly into the shape of the element. For developer guidance, see [`UIPointerShape.roundedRect(_:radius:)`](https://developer.apple.com/documentation/UIKit/UIPointerShape-swift.enum/roundedRect\(_:radius:\)). + +#### [Customizing pointers](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#Customizing-pointers) + +**Prefer system-provided pointer effects for custom elements that behave like standard elements.** When a custom element behaves like a standard one, people generally expect to interact with it using familiar pointer interactions. For example, if buttons in a custom toolbar donโ€™t use the standard highlight effect, people might think theyโ€™re broken. + +**Use pointer effects in consistent ways throughout your app.** For example, if your app helps people draw, provide a similar pointer experience for every drawing area in your app so that people can apply the knowledge they gain in one area to the others. + +**Avoid creating gratuitous pointer and content effects.** People notice when the appearance of the pointer or the UI element beneath it changes, and they expect the changes to be useful. Creating a purely decorative pointer effect can distract and even irritate people without providing any practical value. + +**Keep custom pointer shapes simple.** Ideally, the pointerโ€™s shape signals the action people can take in the current context without drawing too much attention to itself. If people donโ€™t instantly understand your custom pointer shape, theyโ€™re likely to waste time trying to discover what the shape means. + +**Consider enhancing the pointer experience by displaying custom annotations that provide useful information.** For example, you could display X and Y values when people hold the pointer over a graphing area in your app. Keynote uses annotations to display the current width and height of a resizable image. + +![An illustration of a custom pointer hovering over a resize handle on the edge of a shaded rectangle. Above the pointer is a small annotation that displays the imageโ€™s width and height values against a dark background.](https://docs-assets.developer.apple.com/published/291aebad59eee8712e94047fcca4e7cf/useful-pointer-annotation%402x.png) + +**Avoid displaying instructional text with a pointer.** A pointer that displays instructional text can make an app seem complicated and difficult to use. Instead of providing instructions, prioritize clarity and simplicity in your interface, so that people can quickly grasp how to use your app whether theyโ€™re using the pointer or touching the screen. + +**Consider the interplay of shadow, scale, and element spacing when defining custom hover effects.** In general, reserve scaling for elements that can increase in size without crowding nearby elements. For example, scaling doesnโ€™t work well for a table row because a row canโ€™t expand without overlapping adjacent rows. For an element that has little space around it, consider using a hover effect that includes tint, but not scale and shadow. Note that it doesnโ€™t work well to use shadow without including scale, because an unscaled element doesnโ€™t appear to get closer to the viewer even when its shadow implies that itโ€™s elevated above the screen. + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#macOS) + +macOS supports a wide range of standard mouse and trackpad interactions that people can customize. For example, when a click or gesture isnโ€™t a primary way to interact with content, people can often turn it on or off based on their current workflow. People can also choose specific regions of a mouse or trackpad to invoke secondary clicks, and select specific finger combinations and movements for certain gestures. + +Click or gesture| Expected behavior| Mouse| Trackpad +---|---|---|--- +Primary click| Select or activate an item, such as a file or button.| โ—| โ— +Secondary click| Reveal contextual menus.| โ—| โ— +Scrolling| Move content up, down, left, or right within a view.| โ—| โ— +Smart zoom| Zoom in or out on content, such as a web page or PDF.| โ—| โ— +Swipe between pages| Navigate forward or backward between individually displayed pages.| โ—| โ— +Swipe between full-screen apps| Navigate forward or backward between full-screen apps and spaces.| โ—| โ— +Mission Control (double-tap the mouse with two fingers or swipe up on the trackpad with three or four fingers)| Activate Mission Control.| โ—| โ— +Lookup and data detectors (force click with one finger or tap with three fingers)| Display a lookup window above selected content.| | โ— +Tap to click| Perform the primary click action using a tap rather than a click.| | โ— +Force click| Click then press firmly to display a Quick Look window or lookup window above selected content. Apply a variable amount of pressure to affect pressure-sensitive controls, such as variable speed media controls.| | โ— +Zoom in or out (pinch with two fingers)| Zoom in or out.| | โ— +Rotate (move two fingers in a circular motion)| Rotate content, such as an image.| | โ— +Notification Center (swipe from the edge of the trackpad)| Display Notification Center.| | โ— +App Exposรฉ (swipe down with three or four fingers)| Display the current appโ€™s windows in Exposรฉ.| | โ— +Launchpad (pinch with thumb and three fingers)| Display the Launchpad.| | โ— +Show Desktop (spread with thumb and three fingers)| Slide all windows out of the way to reveal the desktop.| | โ— + +#### [Pointers](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#Pointers) + +macOS offers a variety of standard pointer styles, which your app can use to communicate the interactive state of an interface element or the result of a drag operation. + +Pointer| Name| Meaning| AppKit API +---|---|---|--- +![A pointer that resembles a diagonal arrow pointing up and to the left.](https://docs-assets.developer.apple.com/published/5be2c381c17d5d868866b3a5de1013f8/pointers-arrow%402x.png)| Arrow| Standard pointer for selecting and interacting with content and interface elements.| [`arrow`](https://developer.apple.com/documentation/AppKit/NSCursor/arrow) +![A closed, gloved hand.](https://docs-assets.developer.apple.com/published/6680cdb870edf5364f84a483fd2bead9/pointers-closed-hand%402x.png)| Closed hand| Dragging to reposition the display of content within a viewโ€”for example, dragging a map around in Maps.| [`closedHand`](https://developer.apple.com/documentation/AppKit/NSCursor/closedHand) +![A pointer arrow with a small menu-like square to the right of the arrow.](https://docs-assets.developer.apple.com/published/0cb033cee3b55bd4be661b28b928fdc1/pointers-contextual-menu%402x.png)| Contextual menu| A contextual menu is available for the content below the pointer. This pointer is generally shown only when the Control key is pressed.| [`contextualMenu`](https://developer.apple.com/documentation/AppKit/NSCursor/contextualMenu) +![A plus symbol.](https://docs-assets.developer.apple.com/published/d55eabe14365af873000aa389e5fad6c/pointers-crosshair%402x.png)| Crosshair| Precise rectangular selection is possible, such as when viewing an image in Preview.| [`crosshair`](https://developer.apple.com/documentation/AppKit/NSCursor/crosshair) +![A small pointer arrowhead with a circle underneath; the circle contains an Ex.](https://docs-assets.developer.apple.com/published/528819d511869de26beb1fd5008ac773/pointers-disappearing-item%402x.png)| Disappearing item| A dragged item will disappear when dropped. If the item references an original item, the original is unaffected. For example, when dragging a mailbox out of the favorites bar in Mail, the original mailbox isnโ€™t removed.| [`disappearingItem`](https://developer.apple.com/documentation/AppKit/NSCursor/disappearingItem) +![A small pointer arrowhead with a circle underneath; the circle contains a plus symbol.](https://docs-assets.developer.apple.com/published/ccc7052f9bc6fb302d913633c648adcd/pointers-drag-copy%402x.png)| Drag copy| Duplicates a draggedโ€”not movedโ€”item when dropped into the destination. Appears when pressing the Option key during a drag operation.| [`dragCopy`](https://developer.apple.com/documentation/AppKit/NSCursor/dragCopy) +![A curved arrow, pointing up and to the right.](https://docs-assets.developer.apple.com/published/47dfbfd5f1bf3141dbf875f47446d1fd/pointers-drag-link%402x.png)| Drag link| During a drag and drop operation, creates an alias of the selected file when dropped. The alias points to the original file, which remains unmoved. Appears when pressing the Option and Command keys during a drag operation.| [`dragLink`](https://developer.apple.com/documentation/AppKit/NSCursor/dragLink) +![Opposing veritcal braces, used to form an insertion marker.](https://docs-assets.developer.apple.com/published/060f443dee8d260a1a1191d7831e36b7/pointers-horizontal-beam%402x.png)| Horizontal I beam| Selection and insertion of text is possible in a horizontal layout, such as a TextEdit or Pages document.| [`iBeam`](https://developer.apple.com/documentation/AppKit/NSCursor/iBeam) +![An open, gloved hand.](https://docs-assets.developer.apple.com/published/a5daee642ccc8fb3ac550d176b2d1932/pointers-open-hand%402x.png)| Open hand| Dragging to reposition content within a view is possible.| [`openHand`](https://developer.apple.com/documentation/AppKit/NSCursor/openHand) +![A small pointer arrowhead with a do not enter symbol underneath.](https://docs-assets.developer.apple.com/published/2daaf47bef26569f92f30a9016095dde/pointers-operation-not-allowed%402x.png)| Operation not allowed| A dragged item canโ€™t be dropped in the current location.| [`operationNotAllowed`](https://developer.apple.com/documentation/AppKit/NSCursor/operationNotAllowed) +![A gloved hand, with the index finger extended.](https://docs-assets.developer.apple.com/published/25193808b5e72d5983ff26764889718a/pointers-pointing-hand%402x.png)| Pointing hand| The content beneath the pointer is a URL link to a webpage, document, or other item.| [`pointingHand`](https://developer.apple.com/documentation/AppKit/NSCursor/pointingHand) +![A horizontal bar with a downward-pointing arrow at its midpoint.](https://docs-assets.developer.apple.com/published/328443ed3b5dd85c84de91a60ed30b43/pointers-resize-down%402x.png)| Resize down| Resize or move a window, view, or element downward.| [`resizeDown`](https://developer.apple.com/documentation/AppKit/NSCursor/resizeDown) +![A vertical bar with a left-pointing arrow at its midpoint.](https://docs-assets.developer.apple.com/published/34113d73f24c003f4b3715e0cef8fbf6/pointers-resize-left%402x.png)| Resize left| Resize or move a window, view, or element to the left.| [`resizeLeft`](https://developer.apple.com/documentation/AppKit/NSCursor/resizeLeft) +![A vertical bar with left- and right-pointing arrows extending from its midpoint.](https://docs-assets.developer.apple.com/published/478726bb1a630013de1f77b3bccde9e0/pointers-resize-left-right%402x.png)| Resize left/right| Resize or move a window, view, or element to the left or right.| [`resizeLeftRight`](https://developer.apple.com/documentation/AppKit/NSCursor/resizeLeftRight) +![A vertical bar with a right-pointing arrow at its midpoint.](https://docs-assets.developer.apple.com/published/6045fce093cc242bf438393155b77992/pointers-resize-right%402x.png)| Resize right| Resize or move a window, view, or element to the right.| [`resizeRight`](https://developer.apple.com/documentation/AppKit/NSCursor/resizeRight) +![A horizontal bar with an up-pointing arrow at its midpoint.](https://docs-assets.developer.apple.com/published/34576a4ab42dea114abf11b3ee57a4f8/pointers-resize-up%402x.png)| Resize up| Resize or move a window, view, or element upward.| [`resizeUp`](https://developer.apple.com/documentation/AppKit/NSCursor/resizeUp) +![A horizontal bar with up- and down-pointing arrows extending from its midpoint.](https://docs-assets.developer.apple.com/published/d55d0d01c955105a957231266affb447/pointers-resize-up-down%402x.png)| Resize up/down| Resize or move a window, view, or element upward or downward.| [`resizeUpDown`](https://developer.apple.com/documentation/AppKit/NSCursor/resizeUpDown) +![Opposing horizontal braces, used to form an insertion marker.](https://docs-assets.developer.apple.com/published/15923a8cac833b5bb1fd69b4a395c4a9/pointers-vertical-beam%402x.png)| Vertical I beam| Selection and insertion of text is possible in a vertical layout.| [`iBeamCursorForVerticalLayout`](https://developer.apple.com/documentation/AppKit/NSCursor/iBeamCursorForVerticalLayout) + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#visionOS) + +In visionOS, people can attach an external pointing device or keyboard, and use both devices while they continue to use their eyes and hands. If people look at an element and then move the pointer, the system brings focus to the element under the pointer. Your app doesnโ€™t have to do anything to support this behavior. + +When a pointing device is attached, the area people are looking at determines the pointerโ€™s context. For example, when people shift their eyes from one window to another, the pointerโ€™s context seamlessly transitions to the new window. + +Video with custom controls. + +Content description: A recording that shows a pointer moving around, highlighting items, and scrolling content within a Safari window in visionOS. A picture-in-picture window is visible in the bottom left corner of the recording. It shows a person's hand operating a trackpad next to a keyboard outside the field of view. The person's gestures on the trackpad correspond to the pointer movements. + +Play + +When people use an attached pointing device that supports gestures, like a trackpad or mouse, the pointer hides while people are gesturing, minimizing visual distraction. In this scenario, the pointer remains hidden until people move it, when it reappears in the location theyโ€™re looking at. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#Related) + +[Entering data](https://developer.apple.com/design/human-interface-guidelines/entering-data) + +[Keyboards](https://developer.apple.com/design/human-interface-guidelines/keyboards) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#Developer-documentation) + +[Input events](https://developer.apple.com/documentation/SwiftUI/Input-events) โ€” SwiftUI + +[Pointer interactions](https://developer.apple.com/documentation/UIKit/pointer-interactions) โ€” UIKit + +[Mouse, Keyboard, and Trackpad](https://developer.apple.com/documentation/AppKit/mouse-keyboard-and-trackpad) โ€” AppKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/49/F9A980A7-B00A-4856-9172-FDB610A419E5/3509_wide_250x141_1x.jpg) Design for the iPadOS pointer ](https://developer.apple.com/videos/play/wwdc2020/10640) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#Change-log) + +Date| Changes +---|--- +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/remotes.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/remotes.md new file mode 100644 index 00000000..57a455f9 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/remotes.md @@ -0,0 +1,67 @@ +--- +title: "Remotes | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/remotes + +# Remotes + +The Siri Remote is the primary input method for Apple TV, helping people feel connected to onscreen content from across the room. + +![A sketch of an Apple TV remote, suggesting interaction with onscreen content. The image is overlaid with rectangular and circular grid lines and is tinted purple to subtly reflect the purple in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/04cb8e9dcd1006a14957bda7627222ad/inputs-remotes-intro%402x.png) + +In addition to several specific buttons, the Siri Remote combines a clickpad and touch surface to support familiar gestures like swipe and press that people use to navigate tvOS apps, browse channels and content, play and pause media, and make selections. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/remotes#Best-practices) + +**Prefer using standard gestures to perform standard actions.** Unless people are actively playing a game, they expect the remote to behave in standard ways in every app they use. Redefining or repurposing standard remote behaviors can cause confusion and add complexity to your experience. For guidance, see [Gestures](https://developer.apple.com/design/human-interface-guidelines/remotes#Gestures). + +**Be consistent with the tvOS focus experience.** The [focus experience](https://developer.apple.com/design/human-interface-guidelines/focus-and-selection) forges a strong connection between people and the content theyโ€™re viewing. Reinforce this link in your app by ensuring that you combine gestures with the focus experience in ways that are familiar to people, such as always moving focus in the same direction as the gesture. + +**Provide clear feedback that shows people what happens when they make gestures in your app.** For example, lightly resting a thumb on the remote shows people where to swipe down so that they can reveal an info area. + +**Define new gestures only when it makes sense in your app.** Within gameplay, for example, custom gestures can be a fun part of the experience. In most other situations, people expect to use standard gestures and may not appreciate having to discover or remember new ones. + +**Differentiate between press and tap, and avoid responding to an inadvertent tap.** Pressing is an intentional action, and it works well for choosing a button, confirming a selection, and initiating an action during gameplay. Tap gestures are fine for navigation or showing additional information, but keep in mind that people might cause an inadvertent tap when they rest a thumb on the remote, pick it up, move it around, or hand it to someone else, so it often works well to avoid responding to taps during live video playback. + +**Consider using the position of a tap to aid with navigation or gameplay.** The remote can differentiate between up, down, left, and right tap gestures on the touch surface. Respond to positional taps only if it makes sense in the context of your app and if such behavior is intuitive and discoverable. + +**In almost all cases, open the parent of the current screen when people press the Back button.** At the top level of an app or game, the parent is the Apple TV Home Screen; within an app, the parent is defined by the app hierarchy, and isnโ€™t necessarily the previous screen. The exception to this standard behavior is when people are actively playing a game, where it can be easy to accidentally press the Back button repeatedly. To avoid disrupting gameplay in this scenario, respond to the Back button by opening an in-game pause menu that lets people use a different interaction to navigate back to the gameโ€™s main menu. When the in-game pause menu is open, respond to a Back-button press by closing the menu and resuming the game. Note that people press and hold the Back button to go to the Home Screen from any location. For guidance, see [Buttons](https://developer.apple.com/design/human-interface-guidelines/remotes#Buttons). + +**Respond correctly to the Play/Pause button during media playback.** When playing music or video, people expect pressing the Play/Pause button to play, pause, or resume playback. + +## [Gestures](https://developer.apple.com/design/human-interface-guidelines/remotes#Gestures) + +The clickpadโ€™s touch surface detects swipes and presses. + +**Swipe.** Swiping lets people scroll effortlessly through large numbers of items with movement that starts fast and then slows down, based on the strength of the swipe. When people swipe up or down on the edge of the remote, they can speed through items very quickly. + +**Press.** People press to activate a control or select an item. Also, people press before swiping to activate scrubbing mode. + +## [Buttons](https://developer.apple.com/design/human-interface-guidelines/remotes#Buttons) + +Ensure that your app or game responds to specific presses in the following ways. + +Button or area| Expected behavior in an app| Expected behavior in a game +---|---|--- +Touch surface (swipe)| Navigates. Changes focus.| Performs directional pad behavior. +Touch surface (press)| Activates a control or an item. Navigates deeper.| Performs primary button behavior. +Back| Returns to previous screen. Exits to Apple TV Home Screen.| Pauses/resumes gameplay. Returns to previous screen, exits to main game menu, or exits to Apple TV Home Screen. +Play/Pause| Activates media playback. Pauses/resumes media playback.| Performs secondary button behavior. Skips intro video. + +## [Compatible remotes](https://developer.apple.com/design/human-interface-guidelines/remotes#Compatible-remotes) + +Some remotes that are compatible with Apple TV include buttons for browsing live TV or other channel-based content. For example, a remote might include a button people can use to open an electronic program guide (EPG) and other buttons they can use to browse the guide or change channels. For developer guidance, see [Providing Channel Navigation](https://developer.apple.com/documentation/TVServices/providing-channel-navigation); for design guidance, see [EPG experience](https://developer.apple.com/design/human-interface-guidelines/live-viewing-apps#EPG-experience). + +**If your live-viewing app provides an EPG, respond to a remoteโ€™s EPG-browsing buttons in ways people expect.** When people press a โ€œguideโ€ or โ€œbrowseโ€ button, they expect your EPG to open. While theyโ€™re viewing your EPG, people expect to navigate through it by pressing a โ€œpage upโ€ or โ€œpage downโ€ button. Avoid responding to these buttons in other ways while people are browsing the EPG. On the Siri Remote and compatible remotes, people can also tap on the upper or lower area of the Touch surface to browse the EPG. If your app doesnโ€™t support an EPG experience, the system routes these button presses to the default guide app on the viewerโ€™s device. + +**While your content plays, respond to a compatible remoteโ€™s โ€œpage upโ€ or โ€œpage downโ€ button by changing the channel.** People expect these buttons to behave differently when they switch between viewing content and browsing an EPG. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/remotes#Platform-considerations) + + _Not supported in iOS, iPadOS, macOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/remotes#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/remotes#Related) + +[Use your Siri Remote or Apple TV Remote with Apple TV](https://support.apple.com/en-us/HT205305) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/spatial-interactions.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/spatial-interactions.md new file mode 100644 index 00000000..00edbfd8 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-inputs/references/spatial-interactions.md @@ -0,0 +1,70 @@ +--- +title: "Nearby interactions | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/spatial-interactions + +# Nearby interactions + +Nearby interactions support on-device experiences that integrate the presence of people and objects in the nearby environment. + +![A sketch of curved lines beside a circular area containing a smaller circle, suggesting audio approaching a person in a room from a specific direction. The image is overlaid with rectangular and circular grid lines and is tinted purple to subtly reflect the purple in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/4ee9418314d3a8bbdc8e7586a9e3c787/inputs-nearby-interactions-intro%402x.png) + +A great nearby interaction feels intuitive and natural to people, because it builds on their innate awareness of the world around them. For example, a person playing music on their iPhone can continue listening on their HomePod mini when they bring the devices close together, simply by transferring the audio output from their iPhone to the HomePod mini. + +Nearby interactions are available on devices that support Ultra Wideband technology (to learn more, see [Ultra Wideband availability](https://support.apple.com/en-us/HT212274)), and rely on the [Nearby Interaction](https://developer.apple.com/documentation/NearbyInteraction) framework. Before participating in nearby interaction experiences, people grant permission for their device to interact while theyโ€™re using your app. The Nearby Interaction APIs help you preserve peopleโ€™s privacy by relying on randomly generated device identifiers that last only as long as the interaction session your app initiates. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Best-practices) + +**Consider a task from the perspective of the physical world to find inspiration for a nearby interaction.** For example, although people can easily use your appโ€™s UI to transfer a song from their iPhone to their HomePod mini, initiating the transfer by bringing the devices close together makes the task feel rooted in the physical world. Discovering the physical actions that inform the concept of a task can help you create an engaging experience that makes performing it feel easy and natural. + +**Use distance, direction, and context to inform an interaction.** Although your app may get information from a variety of sources, prioritizing nearby, contextually relevant information can help you deliver experiences that feel organic. For example, if people want to share content with a friend in a crowded room, the iOS share sheet can suggest a likely recipient by using on-device knowledge about the personโ€™s most frequent and recent contacts. Combining this knowledge with information from nearby devices that include the U1 chip can let the share sheet improve the experience by suggesting the closest contact the person is facing. + +**Consider how changes in physical distance can guide a nearby interaction.** In the physical world, people generally expect their perception of an object to sharpen as they get closer to it. A nearby interaction can mirror this experience by providing feedback that changes with the proximity of an object. For example, when people use iPhone to find an AirTag, the display transitions from a directional arrow to a pulsing circle as they get closer. + +**Provide continuous feedback.** Continuous feedback reflects the dynamism of the physical world and strengthens the connection between a nearby interaction and the task people are performing. For example, when looking for a lost item in Find My, people get continuous updates that communicate the itemโ€™s direction and proximity. Keep people engaged by providing uninterrupted feedback that responds to their movements. + +**Consider using multiple feedback types to create a holistic experience.** Fluidly transitioning among visual, audible, and haptic feedback can help a nearby interactionโ€™s task feel more engaging and real. Using more than one type of feedback also lets you vary the experience to coordinate with both the task and the current context. For example, while people are interacting with the device screen, visual feedback makes sense; while people are interacting with their environment, audible and haptic feedback often work better. + +**Avoid using a nearby interaction as the only way to perform a task.** You canโ€™t assume that everyone can experience a nearby interaction, so itโ€™s essential to provide alternative ways to get things done in your app. + +## [Device usage](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Device-usage) + +**Encourage people to hold the device in portrait orientation.** Holding a device in landscape can decrease the accuracy and availability of information about the distance and relative direction of other devices. If you support only portrait orientation while your nearby interaction feature runs, prefer giving people implicit, visual feedback on how to hold the device for an optimal experience; when possible, avoid explicitly telling people to hold the device in portrait. + +**Design for the deviceโ€™s directional field of view.** Nearby interaction relies on a hardware sensor with a specific field of view similar to that of the Ultra Wide camera in iPhone 11 and later. If a participating device is outside of this field of view, your app might receive information about its distance, but not its relative direction. + +**Help people understand how intervening objects can affect the nearby interaction experience in your app.** When other people, animals, or sufficiently large objects come between two participating devices, the accuracy or availability of distance and direction information can decrease. Consider adding advice on avoiding this situation to onboarding or tutorial content you present. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Platform-considerations) + + _No additional considerations for iPadOS. Not supported in macOS, tvOS, or visionOS._ + +### [iOS](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#iOS) + +On iPhone, Nearby Interaction APIs provide a peer deviceโ€™s distance and direction. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#watchOS) + +On Apple Watch, Nearby Interaction APIs provide a peer deviceโ€™s distance. Also, all watchOS apps participating in a nearby interaction experience must be in the foreground. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Related) + +[Feedback](https://developer.apple.com/design/human-interface-guidelines/feedback) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Developer-documentation) + +[Nearby Interaction](https://developer.apple.com/documentation/NearbyInteraction) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/0F487599-C14E-48B0-AEB0-A752DFF26E95/5165_wide_250x141_1x.jpg) Design for spatial interaction ](https://developer.apple.com/videos/play/wwdc2021/10245) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/49/E6812719-14BF-4392-84FC-E1CFC1650B71/3558_wide_250x141_1x.jpg) Meet Nearby Interaction ](https://developer.apple.com/videos/play/wwdc2020/10668) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/nearby-interactions#Change-log) + +Date| Changes +---|--- +June 21, 2023| Changed page title from Spatial interactions. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/SKILL.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/SKILL.md new file mode 100644 index 00000000..1f00eb63 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/SKILL.md @@ -0,0 +1,99 @@ +--- +name: hig-patterns +description: Apple Human Interface Guidelines interaction and UX patterns. +risk: unknown +source: community +date_added: '2026-02-27' +--- + +# Apple HIG: Interaction Patterns + +Check for `.claude/apple-design-context.md` before asking questions. Use existing context and only ask for information not already covered. + +## Key Principles + +1. **Minimize modality.** Use modality only when it is critical to get attention, a task must be completed or abandoned, or saving changes is essential. Prefer non-modal alternatives. + +2. **Provide clear feedback.** Every action should produce visible, audible, or haptic response. Activity indicators for indeterminate waits, progress bars for determinate, haptics for physical confirmation. + +3. **Support undo over confirmation dialogs.** Destructive actions should be reversible when possible. Undo is almost always better than "Are you sure?" + +4. **Launch quickly.** Display a launch screen that transitions seamlessly into the first screen. No splash screens with logos. Restore previous state. + +5. **Defer sign-in.** Let users explore before requiring account creation. Support Sign in with Apple and passkeys. + +6. **Keep onboarding brief.** Three screens max. Let users skip. Teach through progressive disclosure and contextual hints. + +7. **Use progressive disclosure.** Show essentials first, let users drill into details. Don't overwhelm with every option on one screen. + +8. **Respect user attention.** Consolidate notifications, minimize interruptions, give users control over alerts. Never use notifications for marketing. + +## Reference Index + +| Reference | Topic | Key content | +|---|---|---| +| [charting-data.md](references/charting-data.md) | Charting Data | Data visualization patterns, accessible charts, interactive elements | +| [collaboration-and-sharing.md](references/collaboration-and-sharing.md) | Collaboration & Sharing | Share sheets, activity views, collaborative editing, SharePlay | +| [drag-and-drop.md](references/drag-and-drop.md) | Drag and Drop | Drag sources, drop targets, spring loading, multi-item drag, visual feedback | +| [entering-data.md](references/entering-data.md) | Entering Data | Text fields, pickers, steppers, input validation, keyboard types, autofill | +| [feedback.md](references/feedback.md) | Feedback | Alerts, action sheets, haptic patterns, sound feedback, visual indicators | +| [file-management.md](references/file-management.md) | File Management | Document browser, file providers, iCloud integration, document lifecycle | +| [going-full-screen.md](references/going-full-screen.md) | Going Full Screen | Full-screen transitions, immersive content, exiting full screen | +| [launching.md](references/launching.md) | Launching | Launch screens, state restoration, cold vs warm launch | +| [live-viewing-apps.md](references/live-viewing-apps.md) | Live Viewing Apps | Live content display, real-time updates, Live Activities, Dynamic Island | +| [loading.md](references/loading.md) | Loading | Activity indicators, progress views, skeleton screens, lazy loading, placeholders | +| [managing-accounts.md](references/managing-accounts.md) | Managing Accounts | Sign in with Apple, passkeys, account creation, credential autofill, account deletion | +| [managing-notifications.md](references/managing-notifications.md) | Managing Notifications | Permission requests, grouping, actionable notifications, provisional delivery | +| [modality.md](references/modality.md) | Modality | Sheets, alerts, popovers, full-screen modals, when to use each | +| [multitasking.md](references/multitasking.md) | Multitasking | iPad Split View, Slide Over, Stage Manager, responsive layout, size class transitions | +| [offering-help.md](references/offering-help.md) | Offering Help | Contextual tips, onboarding hints, help menus, support links | +| [onboarding.md](references/onboarding.md) | Onboarding | Welcome screens, feature highlights, progressive onboarding, skip options | +| [playing-audio.md](references/playing-audio.md) | Playing Audio | Audio sessions, background audio, Now Playing, audio routing, interruptions | +| [playing-haptics.md](references/playing-haptics.md) | Playing Haptics | Core Haptics, UIFeedbackGenerator, haptic patterns, custom haptics | +| [playing-video.md](references/playing-video.md) | Playing Video | Video player controls, picture-in-picture, AirPlay, full-screen video | +| [printing.md](references/printing.md) | Printing | Print dialogs, page setup, AirPrint integration | +| [ratings-and-reviews.md](references/ratings-and-reviews.md) | Ratings & Reviews | SKStoreReviewController, timing, frequency limits, in-app feedback | +| [searching.md](references/searching.md) | Searching | Search bars, suggestions, scoped search, results display, recents | +| [settings.md](references/settings.md) | Settings | In-app vs Settings app, preference organization, toggles, defaults | +| [undo-and-redo.md](references/undo-and-redo.md) | Undo and Redo | Shake to undo, undo/redo stack, multi-level undo | +| [workouts.md](references/workouts.md) | Workouts | Workout sessions, live metrics, Always On display, summaries, HealthKit | + +## Pattern Selection Guide + +| User Goal | Recommended Pattern | Avoid | +|---|---|---| +| First app experience | Brief onboarding (max 3 screens) + progressive disclosure | Long tutorials, mandatory sign-up | +| Waiting for content | Skeleton screens or progress indicators | Blocking spinners with no context | +| Confirming destructive action | Undo support | Excessive "Are you sure?" dialogs | +| Collecting user input | Inline validation, smart defaults, autofill | Modal forms for simple inputs | +| Requesting permissions | Contextual, just-in-time with explanation | Requesting all permissions at launch | +| Providing feedback | Haptics + visual indicator | Silent actions with no confirmation | +| Organizing preferences | In-app settings for frequent items | Burying all settings in system Settings app | + +## Output Format + +1. **Recommended pattern with rationale**, citing the relevant reference file. +2. **Step-by-step implementation** covering each screen or state. +3. **Platform variations** for targeted platforms. +4. **Common pitfalls** that violate HIG for this pattern. + +## Questions to Ask + +1. Where in the app does this pattern appear? What comes before and after? +2. Which platforms? +3. Designing from scratch or improving an existing flow? +4. Does this involve sensitive actions? (Destructive operations, payments, permissions) + +## Related Skills + +- **hig-foundations** -- Accessibility, color, typography, and privacy principles underlying every pattern +- **hig-platforms** -- Platform-specific pattern implementations +- **hig-components-layout** -- Structural components (tab bars, sidebars, split views) for navigation patterns +- **hig-components-content** -- Content display within patterns (charts, collections, search results) + +--- + +*Built by [Raintree Technology](https://raintree.technology) ยท [More developer tools](https://raintree.technology)* + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/charting-data.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/charting-data.md new file mode 100644 index 00000000..305eba09 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/charting-data.md @@ -0,0 +1,81 @@ +--- +title: "Charting data | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/charting-data + +# Charting data + +Presenting data in a chart can help you communicate information with clarity and appeal. + +![A sketch of a bar chart, suggesting data representation. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/bc4a2c4e929441fc51874ce49b4a5129/patterns-charting-data-intro%402x.png) + +Charts provide efficient ways to communicate complex information without requiring people to read and interpret a lot of text. The graphical nature of charts also gives you additional opportunities to express the personality of your experience and add visual interest to your interface. To learn about the components you use to create a chart, see [Charts](https://developer.apple.com/design/human-interface-guidelines/charts). + +A chart can range from a simple graphic that provides glanceable information to a rich, interactive experience that can form the centerpiece of your app and encourage people to explore the data from various perspectives. Whether simple or complex, you can use charts to help people perform data-driven tasks that are important to them, such as: + + * Analyzing trends based on historical or predicted values + + * Visualizing the current state of a process, system, or quantity that changes over time + + * Evaluating different items โ€” or the same item at different times โ€” by comparing data across multiple categories + + + + +Not every collection of data needs to be displayed in a chart. If you simply need to provide data โ€” and you donโ€™t need to convey information about it or help people analyze it โ€” consider offering the data in other ways, such as in a list or table that people can scroll, search, and sort. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/charting-data#Best-practices) + +**Use a chart when you want to highlight important information about a dataset.** Charts are visually prominent, so they tend to draw peopleโ€™s attention. Take advantage of this prominence by clearly communicating what people can learn from the data they care about. + +**Keep a chart simple, letting people choose when they want additional details.** Resist the temptation to pack as much data as possible into a chart. Too much data can make a chart visually overwhelming and difficult to use, obscuring the relationships and other information you want to convey. If you have a lot of data to present โ€” or a lot of functionality to provide โ€” consider giving people a way to reveal it gradually. For example, you might let people choose to view different levels of detail or subsets of data to match their interest. To help people learn how to use an interactive chart, you might offer several versions of the chart, each with more functionality than the last. + +**Make every chart in your app accessible.** A chart communicates visually through graphical representations of data and visual descriptions. In addition to the visual descriptions you display, itโ€™s crucial to provide both accessibility labels that describe chart values and components, and accessibility elements that help people interact with the chart. For guidance, see [Enhancing the accessibility of a chart](https://developer.apple.com/design/human-interface-guidelines/charts#Enhancing-the-accessibility-of-a-chart). + +## [Designing effective charts](https://developer.apple.com/design/human-interface-guidelines/charting-data#Designing-effective-charts) + +**In general, prefer using common chart types.** People tend to be familiar with common chart types โ€” such as bar charts and line charts โ€” so using one of these types in your app can make it more likely that people will already know how to read your chart. For guidance, see [Charts](https://developer.apple.com/design/human-interface-guidelines/charts). + +**If you need to create a chart that presents data in a novel way, help people learn how to interpret the chart.** For example, when a Watch pairs with iPhone, Activity introduces the Activity rings by animating them individually, showing people how each ring maps to the move, exercise, and stand metrics. + +**Examine the data from multiple levels or perspectives to find details you can display to enhance the chart.** For example, viewing the data from a macro level can help you determine high-level summaries that people might be interested in, like totals or averages. From a mid-level perspective, you might find ways to help people identify useful subsets of the data, whereas examining individual data points might help you find ways to draw peopleโ€™s attention to specific values or items. Displaying information that helps people view the chart from various perspectives can encourage them to engage with it. + +![A screenshot of the Stocks app on iPhone. The app uses a line chart to depict the performance of a stock over the currently chosen one-month period.](https://docs-assets.developer.apple.com/published/75cea76eff7e6ee353ad230c147be3da/charts-stocks%402x.png) + +![A screenshot of the Activity screen in the Health app on iPhone. The app uses a set of three bar charts to depict information from the three Activity Rings over the currently chosen one-day period.](https://docs-assets.developer.apple.com/published/b82e0965a76f384beca174253f9c6113/charts-activity%402x.png) + +**Aid comprehension by adding descriptive text to the chart.** Descriptive text titles, subtitles, and annotations help emphasize the most important information in a chart and can highlight actionable takeaways. You can also display brief descriptive text that serves as a headline or summary for a chart, helping people grasp essential information at a glance. For example, Weather displays text that summarizes the information people need right now โ€” such as โ€œChance of light rain in the next hourโ€ โ€” above the scrolling list of hourly forecasts for the next 24 hours. Although a descriptive headline or summary can make a chart more accessible, it doesnโ€™t take the place of accessibility labels. + +**Match the size of a chart to its functionality, topic, and level of detail.** In general, a chart needs to be large enough to comfortably display the details you need to include and expansive enough for the interactivity you want to support. For example, you always want to make it easy for people to read a chartโ€™s details and descriptive text โ€” like labels and annotations โ€” but you might also want to give people enough room to change the scope of a chart or investigate the data from different perspectives. On the other hand, you might want to use a small chart to offer glanceable information about an individual item or to provide a snapshot or preview of a larger version of the chart that people can reveal in a different view. + +**Prefer consistency across multiple charts, deviating only when you need to highlight differences.** If multiple charts in your app serve a similar purpose, you generally donโ€™t want to imply that the charts are unrelated by using a different type or style for each one. Also, using a consistent visual approach for the charts in your app lets people use what they learn about one chart to help them understand another. Consider using different chart types and styles when you need to highlight meaningful differences between charts. + +**Maintain continuity among multiple charts that use the same data.** When you use multiple charts to help people explore one dataset from different perspectives, itโ€™s important to use one chart type and consistent colors, annotations, layouts, and descriptive text to signal that the dataset remains the same. For example, the Health Trends screen shows small charts that each use a specific visual style to depict a recent trend in an area like steps or resting heart rate. When people choose a chart to reveal all their data in that area, the expanded version uses the same style, colors, marks, and annotations to strengthen the relationship between the versions. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/charting-data#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/charting-data#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/charting-data#Related) + +[Charts](https://developer.apple.com/design/human-interface-guidelines/charts) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/charting-data#Developer-documentation) + +[Swift Charts](https://developer.apple.com/documentation/Charts) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/charting-data#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/89D5888B-58DA-4F47-8E3C-998253F6BA98/9954_wide_250x141_1x.jpg) Bring Swift Charts to the third dimension ](https://developer.apple.com/videos/play/wwdc2025/313) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/124/FA764D2D-4E15-4E91-91BA-BDAC80FB901B/6694_wide_250x141_1x.jpg) Design app experiences with charts ](https://developer.apple.com/videos/play/wwdc2022/110342) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/124/4BBCB61E-65ED-43FE-8F7B-81524E0C96BE/6692_wide_250x141_1x.jpg) Design an effective chart ](https://developer.apple.com/videos/play/wwdc2022/110340) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/charting-data#Change-log) + +Date| Changes +---|--- +September 23, 2022| New page. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/collaboration-and-sharing.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/collaboration-and-sharing.md new file mode 100644 index 00000000..59b96f1a --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/collaboration-and-sharing.md @@ -0,0 +1,86 @@ +--- +title: "Collaboration and sharing | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/collaboration-and-sharing + +# Collaboration and sharing + +Great collaboration and sharing experiences are simple and responsive, letting people engage with the content while communicating effectively with others. + +![A sketch of a person with an overlapping checkmark, suggesting effective collaboration. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/ff23fbfe77df77f6bd5dd710a474b15b/patterns-collaboration-and-sharing-intro%402x.png) + +System interfaces and the Messages app can help you provide consistent and convenient ways for people to collaborate and share. For example, people can share content or begin a collaboration by dropping a document into a Messages conversation or selecting a destination in the familiar share sheet. + +After a collaboration begins, people can use the Collaboration button in your app to communicate with others, perform custom actions, and manage details. In addition, people can receive Messages notifications when collaborators mention them, make changes, join, or leave. + +You can take advantage of Messages integration and the system-provided sharing interfaces whether you implement collaboration and sharing through CloudKit, iCloud Drive, or a custom solution. To offer these features when you use a custom collaboration infrastructure, make sure your app also supports universal links (for developer guidance, see [Supporting universal links in your app](https://developer.apple.com/documentation/Xcode/supporting-universal-links-in-your-app)). + +In addition to helping people share and collaborate on documents, visionOS supports immersive sharing experiences through SharePlay. For guidance, see [SharePlay](https://developer.apple.com/design/human-interface-guidelines/shareplay). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/collaboration-and-sharing#Best-practices) + +**Place the Share button in a convenient location, like a toolbar, to make it easy for people to start sharing or collaborating.** In iOS 16, the system-provided share sheet includes ways to choose a file-sharing method and set permissions for a new collaboration; iPadOS 16 and macOS 13 introduce similar appearance and functionality in the sharing popover. In your SwiftUI app, you can also enable sharing by presenting a share link that opens the system-provided share sheet when people choose it; for developer guidance, see [`ShareLink`](https://developer.apple.com/documentation/SwiftUI/ShareLink). + +![An illustration of a Notes document on iPhone. The document toolbar prominently features the Share button next to the More button.](https://docs-assets.developer.apple.com/published/00e4629ef73463727909ceecefdf902f/collaboration-share-button%402x.png) + +**If necessary, customize the share sheet or sharing popover to offer the types of file sharing your app supports.** If you use CloudKit, you can add support for sending a copy of a file by passing both the file and your collaboration object to the share sheet. Because the share sheet has built-in support for multiple items, it automatically detects the file and makes the โ€œsend copyโ€ functionality available. With iCloud Drive, your collaboration object supports โ€œsend copyโ€ functionality by default. For custom collaboration, you can support โ€œsend copyโ€ functionality in the share sheet by including a file โ€” or a plain text representation of it โ€” in your collaboration object. + +**Write succinct phrases that summarize the sharing permissions you support.** For example, you might write phrases like โ€œOnly invited people can editโ€ or โ€œEveryone can make changes.โ€ The system uses your permission summary in a button that reveals a set of sharing options that people use to define the collaboration. + +![An illustration of a Notes document with the share sheet open on iPhone, with collaboration options set to indicate that only invited people can edit the selected document.](https://docs-assets.developer.apple.com/published/b30d247d67d1ba3841be8414b65b7320/collaboration-sharing-permission-invited%402x.png) + +![An illustration of a Notes document with the share sheet open on iPhone, with collaboration options set to indicate that everyone can make changes to the selected document.](https://docs-assets.developer.apple.com/published/2bb4701135ff5cf50b67122ea93ea1ef/collaboration-sharing-permission-everyone%402x.png) + +**Provide a set of simple sharing options that streamline collaboration setup.** You can customize the view that appears when people choose the permission summary button to provide choices that reflect your collaboration functionality. For example, you might offer options that let people specify who can access the content and whether they can edit it or just read it, and whether collaborators can add new participants. Keep the number of custom choices to a minimum and group them in ways that help people understand them at a glance. + +**Prominently display the Collaboration button as soon as collaboration starts.** The system-provided Collaboration button reminds people that the content is shared and identifies whoโ€™s sharing it. Because the Collaboration button typically appears after people interact with the share sheet or sharing popover, it works well to place it next to the Share button. + +![An illustration of a Notes document open on iPhone. The document toolbar prominently features the Collaboration button next to the Share button.](https://docs-assets.developer.apple.com/published/22a789e7f8d5b3b6a22b3763b21288bd/collaboration-status-active-collaboration-button%402x.png) + +**Provide custom actions in the collaboration popover only if needed.** Choosing the Collaboration button in your app reveals a popover that consists of three sections. The top section lists collaborators and provides communication buttons that can open Messages or FaceTime, the middle section contains your custom items, and the bottom section displays a button people use to manage the shared file. You donโ€™t want to overwhelm people with too much information, so itโ€™s crucial to offer only the most essential items that people need while they use your app to collaborate. For example, Notes summarizes the most recent updates and provides buttons that let people get more information about the updates or view more activities. + +![An illustration of a Notes document on iPhone. A menu is open from the Collaboration button in the document toolbar, with buttons to display the most recent updates and activities.](https://docs-assets.developer.apple.com/published/3ecf0a2e3ac684c0d5bcf8b7d54812bc/collaboration-custom-popover-notes%402x.png) + +**If it makes sense in your app, customize the title of the modal viewโ€™s collaboration-management button.** People choose this button โ€” titled โ€œManage Shared Fileโ€ by default โ€” to reveal the collaboration-management view where they can change settings and add or remove collaborators. If you use CloudKit sharing, the system provides a management view for you; otherwise, you create your own. + +**Consider posting collaboration event notifications in Messages.** Choose the type of event that occurred โ€” such as a change in the content or the collaboration membership, or the mention of a participant โ€” and include a universal link people can use to open the relevant view in your app. For developer guidance, see [`SWHighlightEvent`](https://developer.apple.com/documentation/SharedWithYou/SWHighlightEvent). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/collaboration-and-sharing#Platform-considerations) + + _No additional considerations for iOS, iPadOS, or macOS. Not available in tvOS._ + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/collaboration-and-sharing#visionOS) + +By default, the system supports screen sharing for an app running in the Shared Space by streaming the current window to other collaborators. If one person transitions the app to a Full Space while sharing is in progress, the system pauses the stream for other people until the app returns to the Shared Space. For guidance, see [Immersive experiences](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences). + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/collaboration-and-sharing#watchOS) + +In your SwiftUI app running in watchOS, use [`ShareLink`](https://developer.apple.com/documentation/SwiftUI/ShareLink) to present the system-provided share sheet. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/collaboration-and-sharing#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/collaboration-and-sharing#Related) + +[Activity views](https://developer.apple.com/design/human-interface-guidelines/activity-views) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/collaboration-and-sharing#Developer-documentation) + +[Shared with You](https://developer.apple.com/documentation/SharedWithYou) + +[`ShareLink`](https://developer.apple.com/documentation/SwiftUI/ShareLink) โ€” SwiftUI + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/collaboration-and-sharing#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/124/74342B30-92E9-48F3-B0F2-6E42C8FD9391/6506_wide_250x141_1x.jpg) Design for Collaboration with Messages ](https://developer.apple.com/videos/play/wwdc2022/10015) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/124/9785075B-13E9-4631-AD74-77B814019BF4/6589_wide_250x141_1x.jpg) Enhance collaboration experiences with Messages ](https://developer.apple.com/videos/play/wwdc2022/10095) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/124/39FE3E81-AB11-4FEE-AE05-37951E2ADB12/6587_wide_250x141_1x.jpg) Integrate your custom collaboration app with Messages ](https://developer.apple.com/videos/play/wwdc2022/10093) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/collaboration-and-sharing#Change-log) + +Date| Changes +---|--- +December 5, 2023| Added artwork illustrating button placement and various types of collaboration permissions. +June 21, 2023| Updated to include guidance for visionOS. +September 14, 2022| New page. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/drag-and-drop.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/drag-and-drop.md new file mode 100644 index 00000000..586bb6e7 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/drag-and-drop.md @@ -0,0 +1,134 @@ +--- +title: "Drag and drop | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/drag-and-drop + +# Drag and drop + +Using drag and drop, people can move or duplicate selected photos, text, and other content by dragging the selection from one location to another. + +![A sketch of two overlapping squares containing an arrow pointing to the upper-left, suggesting a transition to a new destination. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/e5f75d051f0c1030012fab8220990cc6/patterns-drag-and-drop-intro%402x.png) + +To perform drag and drop, people select content in one location, called the _source_ , and drop it in another, called the _destination_. These locations can be in the same container โ€” like a text view โ€” or in different containers, like text views on opposite sides of a split view, or even in different apps. + +Depending on various factors, the drag and drop action might _move_ the selected content to the destination or _copy_ it. After a successful drop, moved content exists only in the destination; copied content exists in both locations. As a general rule, dropping selected content within the same container moves it, whereas dropping content in a different container copies it. Dragging and dropping content between apps always results in a copy. + +People use different interactions to perform drag and drop depending on platform. For example: + + * In visionOS, people pinch and hold a virtual object while dragging it to a new location in any direction, including along the z-axis. + + * iOS and iPadOS support drag and drop through gestures on the touchscreen, interactions with a pointing device, and through full keyboard-access mode. + + * Universal Control lets people drag content between their Mac and iPad. + + * On a Mac, people can interact with a pointing device, use full keyboard access mode, or use VoiceOver to perform drag and drop. + + + + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/drag-and-drop#Best-practices) + +**As much as possible, support drag and drop throughout your app.** Most people are familiar with drag and drop and they often try it everywhere. When you use system-provided components โ€” such as text fields and text views โ€” you get built-in support for drag and drop. + +**Offer alternative ways to accomplish drag-and-drop actions.** Sometimes, drag-and-drop operations are inconvenient or impossible for people to perform, so itโ€™s important to provide other ways to do the same things. For example, you can include menu commands that people can use to copy an item and move it to another location. In iOS and iPadOS, you can use accessibility APIs to identify sources and destinations so that people can use assistive technologies to drag and drop in your app (for developer guidance, see [`accessibilityDragSourceDescriptors`](https://developer.apple.com/documentation/ObjectiveC/NSObject-swift.class/accessibilityDragSourceDescriptors) and [`accessibilityDropPointDescriptors`](https://developer.apple.com/documentation/ObjectiveC/NSObject-swift.class/accessibilityDropPointDescriptors)). + +**Determine when dragging and dropping content within your app results in a move or a copy.** In general, a move makes sense when the source and destination containers are the same โ€” such as dragging text from one location to another within a document โ€” and a copy makes sense when theyโ€™re different, like dragging an image from one document to another. Before you change these defaults, consider the behavior that most people expect and prefer the one that is least likely to result in frustration or data loss. + +**Support multi-item drag and drop when it makes sense.** People appreciate the convenience of dragging a group of items to a destination, instead of dragging each item separately. In iOS, iPadOS, macOS, and visionOS, people can select multiple items and drag them as a group; macOS also lets people select multiple items from several apps and drag them as a group. In iPadOS, people can select an item, start dragging it, and add other items to the group without stopping the drag operation. + +**Prefer letting people undo a drag-and-drop operation.** Sometimes, people inadvertently drop content in the wrong destination, so they appreciate being able to undo the action and return to their previous state. You might also be able to help people avoid mistakes by asking for confirmation before completing a drag-and-drop operation that canโ€™t be undone. In macOS, for example, the Finder asks for confirmation when people drag a file into a write-only folder because they wonโ€™t be able to open the folder and remove the dropped item. In some situations, it might make sense to provide a way to reverse the results of drag and drop when people canโ€™t undo it. For example, Photos lets people cancel photo sharing after dropping a photo into a shared photo stream. + +**Consider offering multiple versions of dragged content, ordered from highest to lowest fidelity.** By providing multiple alternatives, the destination can choose the highest quality version it can accept. For example, if people can drag a line drawing they created in your app, you could offer a PDF vector representation, a lossless PNG image with transparency, and a lossy JPEG image without transparency, in that order. Another example is an app that uses rich, complicated objects, like charts. This app might offer the native chart object followed by a simpler version โ€” like an image of the chart โ€” for destinations that donโ€™t support chart objects. + +**Consider supporting spring loading.** Spring loading lets people activate certain controls, like buttons and segmented controls, by dragging selected content over them. For example, Calendar lets people drag a selected event over the day, week, month, or year segments in the toolbar, giving them a convenient way to move the event to a different date. On a Mac equipped with a Magic Trackpad, a button or segmented control can activate when people force-click it while continuing to hold the content; on iPad, these components can activate when people hover over them while holding the content. + +## [Providing feedback](https://developer.apple.com/design/human-interface-guidelines/drag-and-drop#Providing-feedback) + +Drag and drop is a dynamic process that can result in multiple outcomes. To help people feel in control the process, itโ€™s crucial to provide clear and continuous feedback throughout. + +**Display a drag image as soon as people drag a selection about three points.** It works well to create a translucent representation of the content people are dragging. Translucency helps distinguish the representation from the original content and lets people see destinations as they pass over them. Display the drag image until people drop the content. + +**If it adds clarity, modify the drag image to help people predict the result of a drag-and-drop operation.** For example, when dragging a photo into a document, the drag image could expand to show the default size of the photo in the document. You can also use drag _flocking_ to visually group multiple drag items โ€” letting people confirm that they havenโ€™t missed an item they want to drag โ€” and then ungroup the items when people drop them. Although changing the drag image can provide valuable feedback, avoid creating a distracting experience in which the drag image is constantly and radically changing. + +**Show people whether a destination can accept dragged content.** For example, you might display an insertion point or highlight a containing view only when the destination can accept a dragged item, and show no visual feedback โ€” or an explicit โ€œnot allowedโ€ image, like the `circle.slash` from SF Symbols โ€” when it canโ€™t. Display highlighting or other visual cues only while the content is positioned above the destination, removing the visual feedback when people drag the content away. When there are multiple possible destinations, provide visual cues that help people identify one at a time. + +**When people drop an item on an invalid destination, or when dropping fails, provide visual feedback.** For example, the item can move back from its current location to its source (if the source is still visible) or it can scale up and fade out to give the impression of the item evaporating instead of landing successfully. + +## [Accepting drops](https://developer.apple.com/design/human-interface-guidelines/drag-and-drop#Accepting-drops) + +**Scroll the contents of a destination when necessary.** When people drag an item within a scrolling container that has a lot of content, the content can automatically scroll as people move the item over it. This behavior makes it easy for people to find the right place to drop the item, but if they continue the drag operation outside of the container, automatic scrolling is no longer necessary. System-provided text views and text fields behave this way by default. + +**When thereโ€™s a choice, pick the richest version of dropped content your app can accept.** For example, if people drag a chart object from another app, the drag operation might offer both the rich, native chart object and a simple image of it. If your app supports charts, extract and display the native chart object; it it doesnโ€™t, use the image instead. + +**Extract only the relevant portion of dropped content if necessary.** For example, when people drag a contact to a recipient field in an email, Mail displays only the name and email address, not the contactโ€™s address information. + +**When a physical keyboard is attached, check for the Option key at drop time.** When people hold the Option key while dragging, they can force a drag-and-drop operation within the same container to behave like a copy. If people stop holding Option before dropping content in the same container, the drag operation results in a move. + +**Provide feedback when dropped content needs time to transfer.** For example, you might display a progress indicator to help people estimate how long the transfer will take. In collections, lists, and tables, you might also display a placeholder at the drop location so people know where to find the content after it finishes transferring. The system can display an alert when a time-consuming transfer occurs between apps. + +**Provide feedback when dropped content initiates a task or action.** If people drop content onto a control that initiates a task โ€” such as printing โ€” show people that the task has begun and keep them informed of its progress. + +**Apply appropriate styling to dropped text.** When the source and destination both support the same text styles, make sure dropped text maintains its original font, typeface, size, and other attributes. Otherwise, apply the destinationโ€™s style to dropped text. + +**After a drop, maintain the contentโ€™s selection state in the destination, updating it in the source as needed.** People expect the content they drop to remain selected so they can immediately act on it. When the source and destination are the same container, the content disappears from its original location when the drag operation performs a move. When a drag operation within the same container performs a copy, remove the selection state from the content that remains in the original location. When people drag selected content to a different container, deselect the content in the source. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/drag-and-drop#Platform-considerations) + + _Not supported in tvOS or watchOS._ + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/drag-and-drop#iOS-iPadOS) + +**Let people perform multiple simultaneous drag activities.** In iPadOS, people can sequentially add items to an in-progress drag session, gathering as many items as their fingers can handle. For example, people can select an app icon on the Home Screen, start dragging it, and select additional app icons before dropping all of them in a different Home Screen or in a folder. To support this interaction, you need to let people add items during a drag โ€” providing visual feedback through flocking โ€” and accept multiple, simultaneous drops. + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/drag-and-drop#macOS) + +**Consider letting people drag content from your app into the Finder.** When you support this, be sure to present the content in a format your app can open later. For example, Calendar lets people drag an event to the Finder as a `.ics` file. People can share this file with others or drag it back to Calendar to open it. When necessary, you can output dragged content in a _clipping_ , which is a temporary container for storing dragged content. For example, most system apps let people drag text to the Finder, where it appears as a clipping. Later, people can drag the clipping into a text field or other location that accepts text. Note that a drag-and-drop clipping isnโ€™t related to the Clipboard. + +**Let people drag selected content from an inactive window without first making the window active.** Selected content in an inactive window is known as a _background selection_ and has a different appearance from selected content in the active window. In general, people expect to drag a background selection to the active window without bringing the inactive window forward. + +**When possible, let people drag individual items from an inactive window without affecting an existing background selection.** For example, people can drag an unselected file from an inactive Finder window without deselecting any of the windowโ€™s selected files. + +**Consider displaying a badge during multi-item drag operations.** A badge is a small filled oval containing a number you can use to indicate the number of items people are dragging. If a destination can accept only a subset of dragged items, update the badge to show the new number. + +**Consider changing the pointer appearance to indicate what will happen when people drop content.** In addition to using the _copy_ pointer, you might want to use the _drag link_ , _disappearing item_ , and _operation not allowed_ pointers, depending on the situation. For guidance, see [Pointers](https://developer.apple.com/design/human-interface-guidelines/pointing-devices#Pointers). + +**As much as possible, let people select and drag content with a single motion.** Unless people are selecting multiple items, they appreciate it when they donโ€™t have to pause between making a selection and starting the drag operation. + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/drag-and-drop#visionOS) + +**When possible, launch your app to handle content that people drop into empty space.** When you associate a user activity with draggable app content, your app can open a window or scene that handles the content when people drop it. For example, when people drop a URL into empty space, it launches Safari; when people drop Quick Lookโ€“supported content, Quick Look launches to display it. For developer guidance, see [`NSUserActivity`](https://developer.apple.com/documentation/Foundation/NSUserActivity). + +Video with custom controls. + +Content description: A recording that shows a wearer dragging a 3D file named meteor out of a Finder window. The wearer drags the file into empty space, dropping it in an area that's visually near a table in their physical surroundings. The dropped file opens, showing a 3D meteor that appears to float above the table. + +Play + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/drag-and-drop#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/drag-and-drop#Related) + +[Universal Control](https://support.apple.com/en-us/HT212757) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/drag-and-drop#Developer-documentation) + +[Drag and drop](https://developer.apple.com/documentation/UIKit/drag-and-drop) โ€” UIKit + +[Drag and Drop](https://developer.apple.com/documentation/AppKit/drag-and-drop) โ€” AppKit + +[File Provider](https://developer.apple.com/documentation/FileProvider) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/drag-and-drop#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/9CCE8A5D-A751-441C-B88F-FB91E2D1958E/4949_wide_250x141_1x.jpg) What's new in UIKit ](https://developer.apple.com/videos/play/wwdc2021/10059) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/F93E642C-EBBD-479E-AC7F-F801103EF53F/5227_wide_250x141_1x.jpg) SwiftUI on the Mac: The finishing touches ](https://developer.apple.com/videos/play/wwdc2021/10289) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/49/5C8F0205-3AE9-4647-870B-5C10FB7EA6FF/3520_wide_250x141_1x.jpg) Designed for iPad ](https://developer.apple.com/videos/play/wwdc2020/10206) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/drag-and-drop#Change-log) + +Date| Changes +---|--- +October 24, 2023| Added artwork. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/entering-data.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/entering-data.md new file mode 100644 index 00000000..aae13e20 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/entering-data.md @@ -0,0 +1,69 @@ +--- +title: "Entering data | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/entering-data + +# Entering data + +When you need information from people, design ways that make it easy for them to provide it without making mistakes. + +![A sketch of a pencil writing within a field, suggesting data entry. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/34d57a13e8fcf5f561a1d9c1eccf8e30/patterns-entering-data-intro%402x.png) + +Entering information can be a tedious process regardless of the interaction methods people use. Improve the experience by: + + * Pre-gathering as much information as possible to minimize the amount of data that people need to supply + + * Supporting all available input methods so people can choose the method that works for them + + + + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/entering-data#Best-practices) + +**Get information from the system whenever possible.** Donโ€™t ask people to enter information that you can gather automatically โ€” such as from settings โ€” or by getting their permission, such as their location or calendar information. + +**Be clear about the data you need.** For example, you might display a prompt in a text field โ€” like โ€œusername@company.comโ€ โ€” or provide an introductory label that describes the information, like โ€œEmail.โ€ You can also prefill fields with reasonable default values, which can minimize decision making and speed data entry. + +**Use a secure text-entry field when appropriate.** If your app or game needs sensitive data, use a field that obscures peopleโ€™s input as they enter it, typically by displaying a small filled circle symbol for each character. For developer guidance, see [`SecureField`](https://developer.apple.com/documentation/SwiftUI/SecureField). In tvOS, you can also configure a [digit entry view](https://developer.apple.com/design/human-interface-guidelines/digit-entry-views) to obscure the numerals people enter (for developer guidance, see [`isSecureDigitEntry`](https://developer.apple.com/documentation/TVUIKit/TVDigitEntryViewController/isSecureDigitEntry)). When you use the system-provided text field in visionOS, the system shows the entered data to the wearer, but not to anyone else; for example, a secure text field automatically blurs when people use AirPlay to stream their content. + +**Never prepopulate a password field.** Always ask people to enter their password or use biometric or keychain authentication. For guidance, see [Managing accounts](https://developer.apple.com/design/human-interface-guidelines/managing-accounts). + +**When possible, offer choices instead of requiring text entry.** Itโ€™s usually easier and more efficient to choose from lists of options than to type information, even when a keyboard is conveniently available. When it makes sense, consider using a picker, menu, or other selection component to give people an easy way to provide the information you need. + +**As much as possible, let people provide data by dragging and dropping it or by pasting it.** Supporting these interactions can ease data entry and make your experience feel more integrated with the rest of the system. + +**Dynamically validate field values.** People can get frustrated when they have to go back and correct mistakes after filling out a lengthy form. When you verify values as soon as people enter them โ€” and provide feedback as soon as you detect a problem โ€” you give them the opportunity to correct errors right away. For numeric data in particular, consider using a number formatter, which automatically configures a text field to accept only numeric values. You can also configure a formatter to display the value in a specific way, such as with a certain number of decimal places, as a percentage, or as currency. + +**When data entry is necessary, make sure people understand that they must provide the required data before they can proceed.** For example, if you include a Next or Continue button after a set of text fields, make the button available only after people enter the data you require. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/entering-data#Platform-considerations) + + _No additional considerations for iOS, iPadOS, tvOS, visionOS, or watchOS._ + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/entering-data#macOS) + +**Consider using an expansion tooltip to show the full version of clipped or truncated text in a field.** An _expansion tooltip_ behaves like a regular tooltip, appearing when the pointer rests on top of a field. Apps running in macOS โ€” including iOS and iPadOS apps running on a Mac โ€” can use an expansion tooltip to help people view the complete data they entered when a text field is too small to display it. For guidance, see [Offering help > macOS, visionOS](https://developer.apple.com/design/human-interface-guidelines/offering-help#macOS-visionOS). + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/entering-data#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/entering-data#Related) + +[Text fields](https://developer.apple.com/design/human-interface-guidelines/text-fields) + +[Virtual keyboards](https://developer.apple.com/design/human-interface-guidelines/virtual-keyboards) + +[Keyboards](https://developer.apple.com/design/human-interface-guidelines/keyboards) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/entering-data#Developer-documentation) + +[Input events](https://developer.apple.com/documentation/SwiftUI/Input-events) โ€” SwiftUI + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/entering-data#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/9CCE8A5D-A751-441C-B88F-FB91E2D1958E/4949_wide_250x141_1x.jpg) What's new in UIKit ](https://developer.apple.com/videos/play/wwdc2021/10059) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/entering-data#Change-log) + +Date| Changes +---|--- +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/feedback.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/feedback.md new file mode 100644 index 00000000..1d76ac21 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/feedback.md @@ -0,0 +1,67 @@ +--- +title: "Feedback | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/feedback + +# Feedback + +Feedback helps people know whatโ€™s happening, discover what they can do next, understand the results of actions, and avoid mistakes. + +![A sketch of a pointer surrounded by a circular set of short lines, suggesting a response to a mouse click. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/d7e2c91a509e05b5e8ee422c6fea86b3/patterns-feedback-intro%402x.png) + +Providing clear, consistent feedback as people interact with your app or game can make it feel intuitive and encourage deeper exploration. Feedback can communicate several different things, such as: + + * The current status of something + + * The success or failure of an important task or action + + * A warning about an action that can have negative consequences + + * An opportunity to correct a mistake or problematic situation + + + + +The most effective feedback tends to match the significance of the information to the way itโ€™s delivered. For example, it often works well to display status information in a passive way so that people can view it when they need it. In contrast, a warning about possible data loss needs to interrupt people so they have a chance to avoid the problem. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/feedback#Best-practices) + +**Make sure all feedback is accessible.** When you use multiple ways to provide feedback, you reach more people and give them the opportunity to receive the feedback in ways that work for them. For example, when you provide feedback using color, text, sound, and haptics, people can receive it whether they silence their device, look away from the screen, or use VoiceOver. (For guidance on providing haptic feedback, see [Playing haptics](https://developer.apple.com/design/human-interface-guidelines/playing-haptics).) + +**Consider integrating status feedback into your interface.** When status feedback is available near the items it describes, people get important information without having to take action or leave their current context. For example, Mail in iOS and iPadOS describes the most recent update and displays the number of unread messages in the toolbar of the mailbox screen, making the information unobtrusive but easy for people to check when theyโ€™re interested. + +**Use alerts to deliver critical โ€” and ideally actionable โ€” information.** By design, alerts disrupt the current context, so you need to match the importance of the information to the level of interruption. Alerts can lose their impact if you use them too often or to deliver unimportant information. For guidance, see [Alerts](https://developer.apple.com/design/human-interface-guidelines/alerts). + +**Warn people when they initiate a task that can cause data loss thatโ€™s unexpected and irreversible.** In contrast, donโ€™t warn people when data loss is the expected result of their action. For example, the Finder doesnโ€™t warn people every time they throw away a file because deleting the file is the expected result. + +**When it makes sense, confirm that a significant action or task has completed.** For example, people appreciate getting feedback that confirms a successful Apple Pay transaction. Itโ€™s generally best to reserve this type of confirmation for activities that are sufficiently important โ€” because people typically expect their action or task to succeed, they only need to know when it doesnโ€™t. + +**Show people when a command canโ€™t be carried out and help them understand why.** For example, if people request directions without specifying a destination, Maps tells them that it canโ€™t provide directions to and from the same location. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/feedback#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, or visionOS._ + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/feedback#watchOS) + +**Avoid displaying an indeterminate progress indicator โ€” such as a loading indicator โ€” in a watchOS app.** An animated indicator can make people think they need to continue paying attention to the display, which isnโ€™t a good user experience. To provide a better experience, reassure people that theyโ€™ll receive a notification when the process completes. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/feedback#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/feedback#Related) + +[Playing audio](https://developer.apple.com/design/human-interface-guidelines/playing-audio) + +[Playing haptics](https://developer.apple.com/design/human-interface-guidelines/playing-haptics) + +[Motion](https://developer.apple.com/design/human-interface-guidelines/motion) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/feedback#Developer-documentation) + +[Animation and haptics](https://developer.apple.com/documentation/UIKit/animation-and-haptics) โ€” UIKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/feedback#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/42/E55D60D2-C7D7-4F96-9A9D-8AF4C7D6BB49/2247_wide_250x141_1x.jpg) Designing Fluid Interfaces ](https://developer.apple.com/videos/play/wwdc2018/803) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/7/2546ECBD-6443-41EC-921D-6429026F8B67/1700_wide_250x141_1x.jpg) Essential Design Principles ](https://developer.apple.com/videos/play/wwdc2017/802) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/file-management.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/file-management.md new file mode 100644 index 00000000..fb9fda5a --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/file-management.md @@ -0,0 +1,135 @@ +--- +title: "File management | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/file-management + +# File management + +Some apps can support documents and files that people expect to manage throughout the system. + +![A sketch of a document with the upper right corner folded in, suggesting interaction with files. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/c753c4f8870e5c729becf174c1f0c5e5/patterns-file-management-intro%402x.png) + +Document-based apps โ€” such as Pages, Keynote, Photos, and Preview โ€” help people create, edit, and save documents and files, often providing customized ways for people to browse for content they want to open in the app. + +People also expect to browse documents without first opening a document-based app. On a Mac, for example, people use the Finder to access the macOS file system; on iPhone, iPad, and Apple Vision Pro, people use the Files app to manage the documents and files on their device. In watchOS and tvOS, people donโ€™t typically create, edit, or manage documents, so these systems donโ€™t provide a document-browsing interface. + +## [Creating and opening files](https://developer.apple.com/design/human-interface-guidelines/file-management#Creating-and-opening-files) + +**Use app menus and keyboard shortcuts to give people convenient ways to create and open documents.** In iPadOS and macOS, people expect to create new documents or open existing ones using familiar menu commands. When you provide commands like New or Open, iPadOS presents them in the shortcuts interface that displays when people hold the Command key on a connected hardware keyboard, and macOS presents them in the menu bar File menu. Regardless of the availability of keyboard shortcuts, include an Add (+) button to help people create a new document. In a macOS app, you put the add action in the File menu (for guidance, see [File menu](https://developer.apple.com/design/human-interface-guidelines/the-menu-bar#File-menu)). + +**If your app requires a custom file browser, support peopleโ€™s understanding of the platformโ€™s file system.** People who are familiar with the Finder and Files apps already understand the basic layout of their deviceโ€™s file system. Although you might want to show the most relevant part of the file system when your custom file browser opens โ€” for example, a Documents or iCloud folder or the most recently selected location โ€” let people use your browser to view the rest of the file system if they want. + +## [Saving work](https://developer.apple.com/design/human-interface-guidelines/file-management#Saving-work) + +**Help people be confident that their work is always preserved unless they cancel or delete it.** In general, avoid making people take an explicit action to save their work. Instead, automatically perform periodic saves while theyโ€™re editing and when they close a file or switch to another app. + +**Hide file extensions by default, but let people view them if they choose.** Be sure to reflect the current choice in all the save or open interfaces you display. + +## [Quick Look previews](https://developer.apple.com/design/human-interface-guidelines/file-management#Quick-Look-previews) + +Quick Look helps you create previews of the files your app handles so that people can view them within your app and in some cases interact with them. For example, you can use Quick Look to let people listen to a preview of an audio file, add markup to a photoโ€™s preview, or rotate and scale a 3D file preview to examine it in different ways. + +**Use a Quick Look viewer to let people preview a file even when your app canโ€™t open it.** If your app lets people attach or otherwise interact with files that it doesnโ€™t support, implementing a Quick Look viewer lets people preview those files without leaving your app. + +**Consider implementing a Quick Look generator if your app produces custom file types.** A Quick Look generator lets other apps โ€” including the Finder, Files, and Spotlight โ€” display previews of your documents, making it easier for people to find them. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/file-management#Platform-considerations) + + _No additional considerations for tvOS, visionOS, or watchOS._ + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/file-management#iOS-iPadOS) + +#### [Document launcher](https://developer.apple.com/design/human-interface-guidelines/file-management#Document-launcher) + +Starting in iOS 18 and iPadOS 18, document-based apps can use the systemโ€™s document launcher to give people a consistent, highly graphical way to browse, open, and create files. The document launcher presents a full-screen experience that highlights key elements of your appโ€™s theme, while making it easy for people to create new documents. For developer guidance, see [`DocumentGroupLaunchScene`](https://developer.apple.com/documentation/SwiftUI/DocumentGroupLaunchScene). + +The document launcher consists of three main parts: + + * A _title card_ that displays the app title and two app-specific buttons + + * A background image that appears behind the title card and additional images โ€” called _accessories_ โ€” that can appear around it + + * A sheet that contains a file browser and optional app-specific controls + + + + +You can customize all three parts of the document launcher. Although the system automatically displays your app name in the title card, you specify the text and functions of the cardโ€™s primary and secondary buttons. You can also create a custom background image, one or more accessory images to surround the title card, and provide some custom controls that can appear in the file browserโ€™s toolbar. + +![A screenshot of a writing app's document launcher on iPad in landscape orientation. The document launcher displays a custom background and two accessory images. At the bottom, the file browser sheet provides 3 tabs: Recents, Shared, and Browse.](https://docs-assets.developer.apple.com/published/5c3d3fe966c8f4d89b462856bec0ed24/file-management-document-launcher%402x.png) + +**Assign the title cardโ€™s buttons to your appโ€™s most important functions.** The primary button typically creates a new document, and the secondary button can provide additional options. For example, the primary button in Numbers is Start Writing and the secondary button is Choose a Template. + +**Provide a background thatโ€™s clearly distinct from the accessories and title card.** You can use a solid color, a gradient, or a pattern. Avoid including complex images or patterns that might distract from foreground elements. + +**Be mindful of accessory placement.** For example, you can place accessories both in front of and behind the title card to create the appearance of depth, but you need to make sure that your app name and both buttons remain clearly visible. Avoid cluttering the title card with too many accessories, and be sure to test its overall appearance across the range of screen sizes and device orientations that you support. + +**Use animation sparingly.** Too much motion on the display can confuse or disorient people. If you want to animate your accessories, consider creating gentle, repeating animations that subtly highlight and enhance your appโ€™s content. For example, you might create an animation that makes an accessory appear to breathe or sway softly. For guidance, see [Motion](https://developer.apple.com/design/human-interface-guidelines/motion). + +#### [File provider app extension](https://developer.apple.com/design/human-interface-guidelines/file-management#File-provider-app-extension) + +If your app can share its files with other apps, you can create a file provider app extension that displays a custom interface for importing, exporting, opening, and moving your appโ€™s documents. For developer guidance, see [File Provider](https://developer.apple.com/documentation/FileProvider). An _app extension_ is code you provide that people can install and use to extend the functionality of a specific area of the system; to learn more, see [App extensions](https://developer.apple.com/app-extensions/). + +**When someone uses your file provider extension to open or import documents, display only documents that are appropriate in the current context.** For example, if a PDF-editing app loads your extension, only list PDF files for opening or import. You might also want to display additional information, such as modification dates, sizes, and whether documents are local or remote. + +**Let people select a destination when exporting and moving documents.** Unless your app stores documents in a single directory, let people navigate to a specific destination in your directory hierarchy. You could also provide a way to add new subdirectories. + +**Avoid including a custom top toolbar.** Your extension loads within a modal view that already includes a toolbar. Providing a second toolbar is confusing and takes space away from your content. + +Your app can also let people browse and open files from other apps. For developer guidance, see [Adding a document browser to your app](https://developer.apple.com/documentation/UIKit/adding-a-document-browser-to-your-app). + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/file-management#macOS) + +#### [Custom file management](https://developer.apple.com/design/human-interface-guidelines/file-management#Custom-file-management) + +People have strong associations with the familiar file browsing experience of the Finder and most document-based apps. Use the default file browser unless you have an important reason to create a custom one. + +**Make your custom file-opening interface convenient.** For example, people might appreciate an โ€œopen recentโ€ action in addition to the simple โ€œopenโ€ action. You might also want to let people choose criteria on which to filter the file-browsing experience, or select multiple documents to open at once. In a macOS open panel, you can customize the title of the Open button to reflect the task โ€” for example, if your app lets people insert a fileโ€™s contents into the current document, you might change the title to Insert. + +**Provide a save interface to let people change a fileโ€™s name, format, or location.** By default, a new documentโ€™s title is โ€œUntitledโ€ until people choose a custom name. As with a document-opening interface, a save view can also provide a browsing experience that defaults to a logical location to help people place the saved document where they want. If you support saving content in different formats, also give people a way to choose a specific file format. + +**Consider extending the functionality of the Save dialog.** If it makes sense in your app, you can add a custom accessory view containing useful settings or options to the Save dialog. For example, the dialog for saving Mail messages as files contains an option to include attachments. + +#### [Finder Sync extensions](https://developer.apple.com/design/human-interface-guidelines/file-management#Finder-Sync-extensions) + +If your app syncs local and remote files, you can create a Finder Sync app extension to express file synchronization status and control within the Finder. For developer guidance, see [Finder Sync](https://developer.apple.com/documentation/FinderSync). + +For example, you can use a Finder Sync extension to: + + * Display badges in the Finder to indicate the sync status of items + + * Provide custom contextual menu items that perform file and folder management tasks, like favoriting and adding password-protection + + * Provide custom toolbar buttons that perform global actions, like initiating a sync operation + + + + +**Help people avoid losing work if they turn off autosaving.** People can turn off autosaving by selecting the โ€œAsk to keep changes when closing documentsโ€ toggle in Desktop & Dock settings. In this scenario, show that a document has unsaved changes and present a save dialog when people choose to close the document, quit your app, log out, or restart. + +**When autosaving is off, make sure people know when a document has unsaved changes.** To show that there are unsaved changes, display a dot on the document windowโ€™s close button and next to the documentโ€™s name in your appโ€™s Window menu. When autosaving is on, showing a dot in these locations is confusing, because it implies that people need to take action to avoid losing their work. Regardless of autosave status, you can append โ€œEditedโ€ to the documentโ€™s title in the title bar, but be sure to remove this suffix as soon as autosave occurs or when people explicitly save their work. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/file-management#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/file-management#Related) + +[Toolbars](https://developer.apple.com/design/human-interface-guidelines/toolbars) + +[File menu](https://developer.apple.com/design/human-interface-guidelines/the-menu-bar#File-menu) + +[Printing](https://developer.apple.com/design/human-interface-guidelines/printing) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/file-management#Developer-documentation) + +[Documents](https://developer.apple.com/documentation/SwiftUI/Documents) โ€” SwiftUI + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/file-management#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/49/68960E87-E488-40C3-90E3-A820B5119FF0/3727_wide_250x141_1x.jpg) Build document-based apps in SwiftUI ](https://developer.apple.com/videos/play/wwdc2020/10039) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/file-management#Change-log) + +Date| Changes +---|--- +June 10, 2024| Added guidelines for using the document launcher in iOS and iPadOS. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/going-full-screen.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/going-full-screen.md new file mode 100644 index 00000000..8dc8ff8f --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/going-full-screen.md @@ -0,0 +1,79 @@ +--- +title: "Going full screen | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/going-full-screen + +# Going full screen + +iPhone, iPad, and Mac offer full-screen modes that let people expand a window to fill the screen, hiding system controls and providing a distraction-free environment. + +![A sketch of two outward-pointing arrows arranged in a vertical line extending from the upper-left to the bottom-right, suggesting expansion. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/b07f350730ba32976cba9f14829b2ce1/patterns-going-full-screen-intro%402x.png) + +Apple TV and Apple Watch donโ€™t offer full-screen modes because apps and games already fill the screen by default. Apple Vision Pro doesnโ€™t offer a full-screen mode because people can expand a window to fill more of their view or use the Digital Crown to hide passthrough and transition to a more immersive experience (for guidance, see [Immersive experiences](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences)). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/going-full-screen#Best-practices) + +**Support full-screen mode when it makes sense for your experience.** People appreciate full-screen mode when they want to concentrate on a task or be immersed in content. Consider offering a full-screen mode if your experience lets people play a game; view media like videos or photo slideshows; or perform an in-depth task that benefits from a distraction-free environment. + +**If necessary, adjust your layout in full-screen mode, but donโ€™t programmatically resize your window.** When a window is larger in full-screen mode than in non-full-screen mode, you want to keep essential content prominent while making good use of the extra space. For example, it might make sense to adjust the proportions of your interface without changing which items appear. If you make such adjustments, be sure theyโ€™re subtle enough to maintain a consistent interface and avoid causing visually jarring transitions between modes. + +**Continue to provide access to essential features and controls so people can complete their task without exiting full-screen mode.** For example, a full-screen media experience needs to make playback controls persistently available or easy to reveal when people need them. + +**Except in games, let people reveal the Dock while your iPadOS or macOS app is in full-screen mode.** In iPadOS and macOS, itโ€™s important to preserve access to the Dock so people can quickly open other apps and Dock items. To help prevent people from accidentally revealing the Dock while theyโ€™re playing your full-screen game, you can ask iPadOS to ignore an initial swipe up from the screenโ€™s bottom edge or hide the Dock entirely in macOS. For developer guidance, see [`preferredScreenEdgesDeferringSystemGestures`](https://developer.apple.com/documentation/SwiftUI/UIHostingController/preferredScreenEdgesDeferringSystemGestures) (SwiftUI), [`preferredScreenEdgesDeferringSystemGestures`](https://developer.apple.com/documentation/UIKit/UIViewController/preferredScreenEdgesDeferringSystemGestures) (UIKit) and [`hideDock`](https://developer.apple.com/documentation/AppKit/NSApplication/PresentationOptions-swift.struct/hideDock) (AppKit). + +**After people switch away from your full-screen experience, help them resume where they left off when they return.** For example, a game or a slideshow needs to pause automatically when people leave the experience so they donโ€™t miss anything. + +**Let people choose when to exit full-screen mode.** People generally donโ€™t expect full-screen mode to end automatically when they switch to a different experience or finish an absorbing activity, like playing a game or viewing a movie. + +**Prioritize content by temporarily hiding toolbars and navigation controls.** You can offer a distraction-free environment by hiding elements when content is the primary focus, such as when viewing full-screen photos or reading a document. If you implement such behavior, let people restore the hidden elements with a familiar gesture or action like tapping, swiping down, or moving the cursor to the top of the screen. Be sure to keep controls visible when theyโ€™re essential for navigation or performing tasks. Although a visionOS window can hide its toolbars or navigation controls, people generally expect different types of immersive experiences while wearing Apple Vision Pro; for guidance, see [Immersive experiences](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/going-full-screen#Platform-considerations) + + _Not supported in tvOS, visionOS, or watchOS._ + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/going-full-screen#iOS-iPadOS) + +**Consider deferring system gestures to prevent accidental exits in a full-screen app or game.** By default, the Home Screen indicator automatically hides shortly after someone switches to your app or game. It reappears when someone interacts with the bottom portion of the screen, allowing them to swipe once to exit. Whenever possible, retain this behavior because itโ€™s familiar and what people expect. If supporting this results in unexpected exits, you can enable two swipes rather than one to exit. For developer guidance, see [`preferredScreenEdgesDeferringSystemGestures`](https://developer.apple.com/documentation/SwiftUI/UIHostingController/preferredScreenEdgesDeferringSystemGestures). + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/going-full-screen#macOS) + +**Use the system-provided full-screen experience.** Using the systemโ€™s full-screen support ensures that your full-screen window works well in all contexts. For example, some Mac models include a camera housing that occupies an area at the top-center of the screen. Using the systemโ€™s full-screen support automatically accommodates this area. For developer guidance, see [`toggleFullScreen(_:)`](https://developer.apple.com/documentation/AppKit/NSWindow/toggleFullScreen\(_:\)). + +**In a game, donโ€™t change the display mode when players go full screen.** People expect to be in control of their display mode, and changing it automatically doesnโ€™t improve performance. + +For additional developer guidance, see [Managing your game window for Metal in macOS](https://developer.apple.com/documentation/Metal/managing-your-game-window-for-metal-in-macos). + +**Always let people choose when to enter full-screen mode.** Prefer letting people use your windowโ€™s Enter Full Screen button, View menu item, or the Control-Command-F keyboard shortcut. Avoid offering a custom menu of window modes. In a game, you might also provide a custom [toggle](https://developer.apple.com/design/human-interface-guidelines/toggles) that turns full-screen mode on and off. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/going-full-screen#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/going-full-screen#Related) + +[Layout](https://developer.apple.com/design/human-interface-guidelines/layout) + +[Multitasking](https://developer.apple.com/design/human-interface-guidelines/multitasking) + +[Windows](https://developer.apple.com/design/human-interface-guidelines/windows) + +[The menu bar](https://developer.apple.com/design/human-interface-guidelines/the-menu-bar) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/going-full-screen#Developer-documentation) + +[`fullScreenCover(item:onDismiss:content:)`](https://developer.apple.com/documentation/SwiftUI/View/fullScreenCover\(item:onDismiss:content:\)) โ€” SwiftUI + +[`NSScreen`](https://developer.apple.com/documentation/AppKit/NSScreen) โ€” AppKit + +[`NSWindow.CollectionBehavior`](https://developer.apple.com/documentation/AppKit/NSWindow/CollectionBehavior-swift.struct) โ€” AppKit + +[Managing your game window for Metal in macOS](https://developer.apple.com/documentation/Metal/managing-your-game-window-for-metal-in-macos) โ€” Swift, Objective-C + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/going-full-screen#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/873F40BE-101A-4C0D-99F0-F5C7CE7B47A3/10046_wide_250x141_1x.jpg) Elevate the design of your iPad app ](https://developer.apple.com/videos/play/wwdc2025/208) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/going-full-screen#Change-log) + +Date| Changes +---|--- +June 9, 2025| Updated guidance for hiding toolbars and navigation controls, and deferring Home Screen indicator gestures in full-screen iOS and iPadOS apps and games. +June 10, 2024| Enhanced guidance for playing a game in full-screen mode. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/launching.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/launching.md new file mode 100644 index 00000000..5282163f --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/launching.md @@ -0,0 +1,81 @@ +--- +title: "Launching | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/launching + +# Launching + +A streamlined launch experience helps people start using your app or game immediately. + +![A sketch of a square containing an arrow pointing to the upper-right corner, suggesting a transition to a new state. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/5ef419551a96fe1df7df2bd5d610b5dc/patterns-launching-intro%402x.png) + +Launching begins when someone opens your app or game, includes an initial download, and ends when the first screen is ready. After launching completes, you might offer an [onboarding](https://developer.apple.com/design/human-interface-guidelines/onboarding) experience, which can give people a high-level view of your app or game. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/launching#Best-practices) + +**Launch instantly.** People want to start interacting with your app or game right away, and sometimes they donโ€™t want to wait more than a couple of seconds. + +**If the platform requires it, provide a launch screen.** In iOS, iPadOS, and tvOS, the system displays your launch screen the moment your app or game starts and quickly replaces it with your first screen, giving people the impression that your experience is fast and responsive. For guidance, see [Launch screens](https://developer.apple.com/design/human-interface-guidelines/launching#Launch-screens). macOS, visionOS, and watchOS donโ€™t require launch screens. + +**If you need a splash screen, consider displaying it at the beginning of your onboarding flow.** A splash screen is a beautiful graphic that succinctly communicates branding and other information you need to provide. If you donโ€™t provide an onboarding experience, you might display your splash screen as soon as launching completes. + +**Restore the previous state when your app restarts so people can continue where they left off.** Avoid making people retrace steps to reach their previous location in your app or game. Restore granular details of the previous state as much as possible. For example, scroll the view to peopleโ€™s most recent position, and display windows in the same state and location in which people left them. + +## [Launch screens](https://developer.apple.com/design/human-interface-guidelines/launching#Launch-screens) + + _Not applicable for macOS, visionOS, or watchOS._ + +**Downplay the launch experience.** A launch screen isnโ€™t part of an onboarding experience or a splash screen, and it isnโ€™t an opportunity for artistic expression. A launch screenโ€™s sole function is to enhance the perception of your experience as quick to launch and immediately ready to use. + +**Design a launch screen thatโ€™s nearly identical to the first screen of your app or game.** If you include elements that look different when launching completes, people may experience an unpleasant flash between the launch screen and your first screen. If your app or game displays a solid color before transitioning to the first screen, create a launch screen that displays only that solid color. Also make sure that your launch screen matches the deviceโ€™s current orientation and appearance mode. + +**Avoid including text on your launch screen, even if your first screen displays text.** Because the content in a launch screen doesnโ€™t change, any text you display wonโ€™t be localized. + +**Donโ€™t advertise.** The launch screen isnโ€™t a branding opportunity. Avoid creating a screen that looks like a splash screen or an โ€œAboutโ€ window, and donโ€™t include logos or other branding elements unless theyโ€™re a fixed part of your appโ€™s first screen. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/launching#Platform-considerations) + + _No additional considerations for macOS or watchOS._ + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/launching#iOS-iPadOS) + +**Launch in the appropriate orientation.** If your app or game supports both portrait and landscape modes, launch using the deviceโ€™s current orientation. If your interface only runs in one orientation, launch in that orientation and let people rotate the device if necessary. Ensure a landscape-only interface responds correctly, regardless of whether people enter landscape orientation by rotating the device left or right. For guidance, see [Layout](https://developer.apple.com/design/human-interface-guidelines/layout). + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/launching#tvOS) + +Note + +Unlike the [layered images](https://developer.apple.com/design/human-interface-guidelines/images#Layered-images) throughout much of a tvOS app, the launch screen is static. + +**In a live-viewing app, consider automatically starting playback soon after people start the app.** People come to your app to watch TV, so you might want to start playing new or recently viewed live content after a few seconds of inactivity. For guidance, see [Live-viewing apps](https://developer.apple.com/design/human-interface-guidelines/live-viewing-apps). + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/launching#visionOS) + +**Consider launching in the Shared Space even if your app is fully immersive.** Opening a window in the Shared Space lets you provide more context about your app or game while giving it time to load, and it also lets you present a control that people can use to open your fully immersive experience. In general, people appreciate being able to choose when to transition to a Full Space, especially if theyโ€™re currently running other apps in the Shared Space. For guidance, see [Immersive experiences](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences). + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/launching#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/launching#Related) + +[Onboarding](https://developer.apple.com/design/human-interface-guidelines/onboarding) + +[Loading](https://developer.apple.com/design/human-interface-guidelines/loading) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/launching#Developer-documentation) + +[Specifying your appโ€™s launch screen](https://developer.apple.com/documentation/Xcode/specifying-your-apps-launch-screen) โ€” Xcode + +[Responding to the launch of your app](https://developer.apple.com/documentation/UIKit/responding-to-the-launch-of-your-app) โ€” UIKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/launching#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/48/38776A03-1056-4A47-9AB0-E4A8652AD5C4/2662_wide_250x141_1x.jpg) Optimizing App Launch ](https://developer.apple.com/videos/play/wwdc2019/423) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/7/2C48F507-180B-4858-BB26-488C234B067F/1920_wide_250x141_1x.jpg) Love at First Launch ](https://developer.apple.com/videos/play/wwdc2017/816) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/launching#Change-log) + +Date| Changes +---|--- +June 10, 2024| Added guidance on displaying a splash screen. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/live-viewing-apps.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/live-viewing-apps.md new file mode 100644 index 00000000..8249e796 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/live-viewing-apps.md @@ -0,0 +1,79 @@ +--- +title: "Live-viewing apps | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/live-viewing-apps + +# Live-viewing apps + +As you design a live-viewing app, prioritize the content and create fun, fluid interactions that encourage immersion in the live-viewing experience. + +![A sketch of a television containing a play button, suggesting playback of media. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/cba28a7b98ccba8fdc5f498d69753ce8/patterns-live-viewing-intro%402x.png) + +Live-viewing apps need to elevate and prioritize live content. In every screen, draw peopleโ€™s attention to live content and make sure they can distinguish it from video-on-demand (VOD) content at a glance. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/live-viewing-apps#Best-practices) + +**Feature live content prominently and make it easy to access.** People come to your app to watch content, so you want to minimize the interval between starting your app and playing content. When live content is in the first tab, people donโ€™t have to tap more than once to start viewing it. + +**Let people tap once โ€” or not at all โ€” to start playback.** For example, you might display a Watch Now button on top of featured or recently viewed live content. When people tap this button, it immediately disappears and playback begins, replacing your appโ€™s UI with a full-screen, immersive viewing experience. + +**Make sure live content looks live.** People need to be able to distinguish live content from VOD content. Although simply playing live content is the best way to make it feel live, you can also help people recognize live content by marking it in some way. For example, you might display other channels in a collection row titled โ€œLiveโ€ and give each item a visual indicator โ€” such as a badge, symbol, or sash โ€” that identifies it as live. + +**Consider indicating the progress of currently playing live content.** People appreciate knowing where theyโ€™ll land when they jump into in-progress live content. You can use a progress bar or other indicator to show people how much content remains. + +**Give people additional actions and viewing alternatives.** In addition to playback, which always needs to be the primary action, make it easy for people to record, restart, download, and perform other actions that you support. Display these actions in the same order throughout your app โ€” for example, Watch, Start Over, Record, and Favorite. Also, if the currently playing content is playing again at other times, show this information so that people can schedule their viewing. + +**Consider using a content footer for browsing channels during playback.** A content footer lets people browse without taking them out of the live playback experience. If you decide to use a content footer, be sure to: + + * Give it a subtle treatment, such as a darkening, to keep text legible and help all items remain visually distinct from the content playing behind it. + + * Make it easy for people to identify the thumbnail that represents the currently playing content by, for example, badging the thumbnail or tinting its progress bar. + + * Match the categories in the content footer to those in your electronic program guide (for related guidance, see [EPG experience](https://developer.apple.com/design/human-interface-guidelines/live-viewing-apps#EPG-experience)). + + * Design a simple, predictable way for people to invoke and dismiss the content footer โ€” for example, if swiping up invokes the footer, people would expect swiping down to dismiss it. + + + + +**Provide instant visual feedback when people change channels.** This is essential for two reasons: people need confirmation that theyโ€™ve arrived at the channel they want, and providing feedback can give the streaming content some time to load. + +**Match audio to the current context.** When people start playing live content, they expect the audio to match even if they switch to browsing while the content plays in the background. However, when people navigate away from the live tab in your app, they leave the live-viewing context, so audio needs to stop. + +## [EPG experience](https://developer.apple.com/design/human-interface-guidelines/live-viewing-apps#EPG-experience) + +Live-viewing apps typically provide an electronic program guide (EPG) that contains information about scheduled programming. Follow these guidelines to give people a streamlined EPG experience that feels designed specifically for your live-viewing app. + +**Prominently display current information and make it easy to return to playback.** When people first open the EPG, the current program, channel, and time needs to be easy to spot so they can instantly return to the current channel. + +**Make browsing the EPG effortless.** A typical EPG contains a lot of information, so itโ€™s important to help people page, scroll, or jump through it easily. Also consider providing a My Channels group or a Favorites group that gives people quick access to the content they view most often. + +**Group content into familiar categories to help people find it more easily.** For example, you might use categories like Movies, TV Shows, Kids, Sports, and Popular. If your app includes a content footer, organize content thumbnails using the same categories as in the EPG. + +**Let people browse the EPG without leaving their current content.** For example, you can continue playing content in a picture-in-picture (PiP) mode or in the background while people browse the EPG. + +## [Cloud DVR](https://developer.apple.com/design/human-interface-guidelines/live-viewing-apps#Cloud-DVR) + +If you support digital video recording (DVR) in the cloud, follow these guidelines to provide a great recording experience in your live-viewing app. + +**Let people start and stop recording from the info panel.** While live-streaming, people want to reveal the info panel to start recording immediately. + +**Let people record a future program in a view that provides details about the content.** Also, give people the option to record only that program or all future episodes. + +**Help people adapt the recording experience to their needs.** Let people specify precisely what they want to record, such as only the current episode, only new episodes, or only games that involve specific teams. + +**Allow playback and other content-specific actions within your cloud DVR area.** When people open a view that displays content details in your cloud DVR section, let them play or delete content and, if applicable, adjust recording settings. + +**Consider offering a control that lets people manage cloud DVR settings.** For example, you might let people delete recordings theyโ€™ve already watched or content thatโ€™s older than a certain number of days. Ideally, help people avoid running out of space by letting them set up automatic storage management, which overwrites the oldest or already viewed content. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/live-viewing-apps#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/live-viewing-apps#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/live-viewing-apps#Related) + +[Remotes](https://developer.apple.com/design/human-interface-guidelines/remotes) + +[Playing video](https://developer.apple.com/design/human-interface-guidelines/playing-video) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/loading.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/loading.md new file mode 100644 index 00000000..1abe27a9 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/loading.md @@ -0,0 +1,59 @@ +--- +title: "Loading | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/loading + +# Loading + +The best content-loading experience finishes before people become aware of it. + +![A sketch of a spinning indeterminate activity indicator, suggesting data loading. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/cfdf824156ed794426ac55a2cb38ec15/patterns-loading-intro%402x.png) + +If your app or game loads assets, levels, or other content, design the behavior so it doesnโ€™t disrupt or negatively impact the user experience. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/loading#Best-practices) + +**Show something as soon as possible.** If you make people wait for loading to complete before displaying anything, they can interpret the lack of content as a problem with your app or game. Instead, consider showing placeholder text, graphics, or animations as content loads, replacing these elements as content becomes available. + +**Let people do other things in your app or game while they wait for content to load.** Loading content in the background helps give people access to other actions. For example, a game could load content in the background while players learn about the next level or view an in-game menu. For developer guidance, see [Improving the player experience for games with large downloads](https://developer.apple.com/documentation/GameKit/improving-the-player-experience-for-games-with-large-downloads). + +**If loading takes an unavoidably long time, give people something interesting to view while they wait.** For example, you might provide gameplay hints, display tips, or introduce people to new features. Gauge the remaining loading time as accurately as possible to help you avoid giving people too little time to enjoy your placeholder content or having so much time that you need to repeat it. + +**Improve installation and launch time by downloading large assets in the background.** Consider using the [Background Assets](https://developer.apple.com/documentation/BackgroundAssets) framework to schedule asset downloads โ€” like game level packs, 3D character models, and textures โ€” to occur immediately after installation, during updates, or at other nondisruptive times. + +## [Showing progress](https://developer.apple.com/design/human-interface-guidelines/loading#Showing-progress) + +**Clearly communicate that content is loading and how long it might take to complete.** Ideally, content displays instantly, but for situations where loading takes more than a moment or two, you can use system-provided components โ€” called _progress indicators_ โ€” to show that loading is ongoing. In general, you use a _determinate_ progress indicator when you know how long loading will take, and you use an _indeterminate_ progress indicator when you donโ€™t. For guidance, see [Progress indicators](https://developer.apple.com/design/human-interface-guidelines/progress-indicators). + +**For games, consider creating a custom loading view.** Standard progress indicators work well in most apps, but can sometimes feel out of place in a game. Consider designing a more engaging experience by using custom animations and elements that match the style of your game. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/loading#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, or visionOS._ + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/loading#watchOS) + +**As much as possible, avoid showing a loading indicator in your watchOS experience.** People expect quick interactions with their Apple Watch, so aim to display content immediately. In situations where content needs a second or two to load, itโ€™s better to display a loading indicator than a blank screen. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/loading#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/loading#Related) + +[Launching](https://developer.apple.com/design/human-interface-guidelines/launching) + +[Progress indicators](https://developer.apple.com/design/human-interface-guidelines/progress-indicators) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/loading#Developer-documentation) + +[Background Assets](https://developer.apple.com/documentation/BackgroundAssets) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/loading#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/A51258F3-769A-4301-BE75-0DDE23322569/9860_wide_250x141_1x.jpg) Discover Apple-Hosted Background Assets ](https://developer.apple.com/videos/play/wwdc2025/325) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/loading#Change-log) + +Date| Changes +---|--- +June 9, 2025| Revised guidance for storing downloads to reflect downloading large assets in the background. +June 10, 2024| Added guidelines for showing progress and storing downloads, and enhanced guidance for games. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/managing-accounts.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/managing-accounts.md new file mode 100644 index 00000000..f5f20b9f --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/managing-accounts.md @@ -0,0 +1,107 @@ +--- +title: "Managing accounts | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/managing-accounts + +# Managing accounts + +When it doesnโ€™t create an unnecessary barrier to your experience, an account can be a convenient way for people to access their content and track personal details. + +![A sketch of a person, suggesting personal information. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/bc6ed656b5ef4483ce1c17d7e0042ce2/patterns-managing-accounts-intro%402x.png) + +Ask people to create an account only if your core functionality requires it; otherwise, let people enjoy your app or game without one. If you require an account, consider using [Sign in with Apple](https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple) to give people a consistent sign-in experience they can trust and the convenience of not having to remember multiple accounts and authentication methods. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/managing-accounts#Best-practices) + +**Explain the benefits of creating an account and how to sign up.** If your app or game requires an account, write a brief, friendly description of the reasons for the requirement and its benefits. Display this message in your sign-in view. + +**Delay sign-in for as long as possible.** People often abandon apps when theyโ€™re forced to sign in before they can do anything useful. To help avoid this situation, give people a chance to get a sense of what your app or game does before asking them to make a commitment to it. For example, a shopping app might let people browse as much as they want, requiring sign-in only when theyโ€™re ready to make a purchase. + +**If you donโ€™t use Sign in with Apple in your iOS, iPadOS, macOS, or visionOS app, prefer using a passkey.** Passkeys simplify account creation and authentication, eliminating the need for people to create or enter passwords. When an app supports passkeys, people simply provide their user name when creating a new account or signing in to an existing one. For developer guidance, see [Supporting passkeys](https://developer.apple.com/documentation/AuthenticationServices/supporting-passkeys). If you need to continue using passwords for authentication, augment security by requiring two-factor authentication (for developer guidance, see [Securing Logins with iCloud Keychain Verification Codes](https://developer.apple.com/documentation/AuthenticationServices/securing-logins-with-icloud-keychain-verification-codes)). + +**Always identify the authentication method you offer.** For example, if you display a button for signing in to your app with Face ID, title it using a phrase like โ€œSign In with Face IDโ€ instead of a generic phrase like โ€œSign In.โ€ + +**Refer only to authentication methods that are available in the current context.** For example, donโ€™t reference Face ID on a device that doesnโ€™t offer it. Check the deviceโ€™s capabilities and use the appropriate terminology. For developer guidance, see [`LABiometryType`](https://developer.apple.com/documentation/LocalAuthentication/LABiometryType). + +**In general, avoid offering an app-specific setting for opting in to biometric authentication.** People turn on biometric authentication at the system level, so presenting an in-app setting is redundant and could be confusing. + +**Avoid using the term _passcode_ to refer to account authentication.** People create a passcode to unlock their device or authenticate for Apple services. If you use the term in your interface, people might think youโ€™re asking them to reuse their passcode in your app or game. + +## [Deleting accounts](https://developer.apple.com/design/human-interface-guidelines/managing-accounts#Deleting-accounts) + +If you help people create an account within your app or game, you must also help them delete it, not just deactivate it. In addition to following the guidelines below, be sure to understand and comply with your regionโ€™s legal requirements related to account deletion and the right to be forgotten. + +Important + +If legal requirements compel your app to maintain accounts or information โ€” such as digital health records โ€” or to follow a specific account-deletion process, clearly describe the situation so people can understand the information or accounts you must maintain and the process you must follow. + +**Provide a clear way to initiate account deletion within your app or game.** If people canโ€™t perform account deletion within your app, you must provide a direct link to the webpage on which people can do so. Make the link easy to discover โ€” for example, donโ€™t bury it in your Privacy Policy or Terms of Service pages. + +Developer note + +If people used [Sign in with Apple](https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple) to create an account within your app, you revoke the associated tokens when they delete their account. See [`Token revocation`](https://developer.apple.com/documentation/SigninwithAppleRESTAPI/Revoke-tokens). + +**Provide a consistent account-deletion experience whether people perform it within your app or game or on the website.** For example, avoid making one version of the deletion flow longer or more complicated than the other. + +**Consider letting people schedule account deletion to occur in the future.** People can appreciate the opportunity to use their remaining services or wait until their subscription auto-renews before deleting their account. If you offer a way to schedule account deletion, offer an option for immediate deletion as well. + +**Tell people when account deletion will complete, and notify them when itโ€™s finished.** Because it can sometimes take a while to fully delete an account, itโ€™s essential to keep people informed about the status of the deletion process so they know what to expect. + +**If you support in-app purchases, help people understand how billing and cancellation work when they delete their account.** For example, you might need to help people understand the following scenarios: + + * Billing for an auto-renewable subscription continues through Apple until people cancel the subscription, regardless of whether they delete their account. + + * After they delete their account, people need to cancel their subscription or request a refund. + + + + +In addition to helping people understand these scenarios, provide information that describes how to cancel subscriptions and manage purchases. For guidance, see [Helping people manage their subscriptions](https://developer.apple.com/design/human-interface-guidelines/in-app-purchase#Helping-people-manage-their-subscriptions) and [Providing help with in-app purchases](https://developer.apple.com/design/human-interface-guidelines/in-app-purchase#Providing-help-with-in-app-purchases). + +Note + +Even if people didnโ€™t use your app to purchase the subscription, you still need to support account deletion. + +## [TV provider accounts](https://developer.apple.com/design/human-interface-guidelines/managing-accounts#TV-provider-accounts) + +Many popular TV providers let people sign in to their accounts at the system level, eliminating the need to authenticate on an app-by-app basis. If your TV provider app requires people to sign in, use TV Provider Authentication to provide the most efficient onboarding experience. + +**Avoid displaying a sign-out option when people are signed in at the system level.** If your app must include a sign-out option, invoking it needs to prompt people to navigate to Settings > TV Provider to sign out of their account. + +**Never instruct people to sign out by adjusting privacy controls.** The TV provider controls in Settings > Privacy arenโ€™t a sign-out mechanism. These settings help people manage the apps that can access their TV provider account. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/managing-accounts#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, or visionOS._ + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/managing-accounts#tvOS) + +Most people interact with Apple TV using a remote, not a keyboard, so ask for the minimum amount of information necessary. + +**Prefer letting people use another device to sign up or authenticate.** When you configure your appโ€™s associated domains, Apple TV can work with other devices to safely suggest sign-in credentials, including [Sign in with Apple](https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple). For developer guidance, see [Configuring an associated domain](https://developer.apple.com/documentation/Xcode/configuring-an-associated-domain). + +**When people are signed in to a shared account, avoid asking them to choose their profile every time they become the current user.** In tvOS 16 and later, your app can share its credentials with all users while storing each individualโ€™s profile and user data separately. When you support this type of sharing, your app can automatically use the current userโ€™s profile without asking each person to sign in separately to a shared account. For developer guidance, see [`kSecUseUserIndependentKeychain`](https://developer.apple.com/documentation/Security/kSecUseUserIndependentKeychain) and [`User Management Entitlement`](https://developer.apple.com/documentation/BundleResources/Entitlements/com.apple.developer.user-management). + +**Minimize data entry.** If you need to gather more than a small amount of information, ask people to visit a website from another device. If you need an email address, show the email keyboard screen, which includes a list of recently entered addresses. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/managing-accounts#watchOS) + +Use iCloud synchronization to provide access to the Keychain, letting people autofill user names and passwords and preserve app settings. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/managing-accounts#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/managing-accounts#Related) + +[Onboarding](https://developer.apple.com/design/human-interface-guidelines/onboarding) + +[Sign in with Apple](https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/managing-accounts#Developer-documentation) + +[Supporting passkeys](https://developer.apple.com/documentation/AuthenticationServices/supporting-passkeys) โ€” Authentication Services + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/managing-accounts#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/473C8E4A-1764-482D-BE24-B3A7BBDBD526/9996_wide_250x141_1x.jpg) Whatโ€™s new in passkeys ](https://developer.apple.com/videos/play/wwdc2025/279) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/9D0EBBBA-766E-4306-A14C-39339DD76D58/9271_wide_250x141_1x.jpg) Whatโ€™s new in device management ](https://developer.apple.com/videos/play/wwdc2024/10143) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/managing-notifications.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/managing-notifications.md new file mode 100644 index 00000000..040250e1 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/managing-notifications.md @@ -0,0 +1,99 @@ +--- +title: "Managing notifications | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/managing-notifications + +# Managing notifications + +Notifications can give people timely and important information, whether the device is locked or in use. + +![A sketch of bell with a small overlapping circle, suggesting a notification sound. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/ba0d1e169421e490842a4009ee7d09e5/patterns-managing-notifications-intro%402x.png) + +You need to get permission before sending any notification. The system lets people change this decision in settings, where they can also silence all notifications (except for government alerts in some locales). + +## [Integrating with Focus](https://developer.apple.com/design/human-interface-guidelines/managing-notifications#Integrating-with-Focus) + +People appreciate receiving a notification for something they care about, but they donโ€™t always appreciate being interrupted. To help people manage the experience, the system lets them specify delivery times and set up a Focus. + + * A Focus helps people filter notifications during a time period they reserve for an activity like sleeping, working, reading, or driving. + + * Delivery scheduling lets people choose whether to receive notification alerts immediately or in a summary thatโ€™s delivered at times they choose. + + + + +People identify the contacts and apps that can break through a Focus to deliver notification alerts. In a Work Focus, for example, people might want to receive alerts from work colleagues, family members, and work-related apps as soon as notifications arrive. People might also want to receive all Time Sensitive notification alerts during a Focus. A _Time Sensitive_ notification contains essential information people appreciate getting right away. + +Important + +Even though a Focus might delay the delivery of a notification alert, the notification itself is available as soon as it arrives. + +To support these behavior customizations, you first identify the types of notifications your app or game can send. If you support direct communications โ€” like phone calls and messages โ€” you use _communication_ notifications; for all other types of tasks, you use _noncommunication_ notifications. To support communication notifications, you adopt SiriKit intents, which means people can use Siri to customize notification behaviors; for developer guidance, see [`INSendMessageIntent`](https://developer.apple.com/documentation/Intents/INSendMessageIntent) and [`UNNotificationContentProviding`](https://developer.apple.com/documentation/UserNotifications/UNNotificationContentProviding). + +You need to specify a system-defined interruption level for each noncommunication notification you send. The system uses the interruption level to help determine when to deliver the alert; when a communication notification arrives, the system uses the sender to determine when to deliver the alert. + +The system defines four interruption levels for noncommunication notifications: + + * _Passive_. Information people can view at their leisure, like a restaurant recommendation. + + * _Active_ (the default). Information people might appreciate knowing about when it arrives, like a score update on their favorite sports team. + + * _Time Sensitive_. Information that directly impacts the person and requires their immediate attention, like an account security issue or a package delivery. + + * _Critical_. Urgent information about health and safety that directly impacts the person and demands their immediate attention. Critical notifications are extremely rare and typically come from governmental and public agencies or apps that help people manage their health or home. + + + + +Notification alerts in each system-defined interruption level can behave in the following ways: + +Interruption level| Overrides scheduled delivery| Breaks through Focus| Overrides Ring/Silent switch on iPhone and iPad +---|---|---|--- +Passive| No| No| No +Active| No| No| No +Time Sensitive| Yes| Yes| No +Critical| Yes| Yes| Yes + +Note + +Because a Critical notification can override the Ring/Silent switch and break through scheduled delivery and Focus, you must get an entitlement to send one. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/managing-notifications#Best-practices) + +**Build trust by accurately representing the urgency of each notification.** People have several ways to adjust how they receive your notifications โ€” including turning off all notifications โ€” so itโ€™s essential to be as realistic as possible when assigning an interruption level. You donโ€™t want people to feel that a notification uses a high level of urgency to interrupt them with low-priority information. + +**Use the Time Sensitive interruption level only for notifications that are relevant in the moment.** To help people understand the benefits of letting Time Sensitive notifications break through a Focus or scheduled delivery, make sure the notification is about an event thatโ€™s happening now or will happen within an hour. The first time a Time Sensitive notification arrives from your app, the system describes how such a notification works and gives people a way to turn it off if they donโ€™t agree that the information requires their immediate attention. Going forward, the system periodically gives people additional opportunities to evaluate how your Time Sensitive notification is working for them. For developer guidance, see [`UNNotificationInterruptionLevel`](https://developer.apple.com/documentation/UserNotifications/UNNotificationInterruptionLevel). + +## [Sending marketing notifications](https://developer.apple.com/design/human-interface-guidelines/managing-notifications#Sending-marketing-notifications) + +Donโ€™t use notifications to send marketing or promotional content unless people explicitly agree to receive such information. When people want to learn about new features, content, or events related to your app or game, they can grant their permission to receive marketing notifications. For example, people who use a subscription app might appreciate getting an offer to become a subscriber, and game players might want to receive a special offer related to a live game event. + +**Never use the Time Sensitive interruption level to send a marketing notification.** People may have agreed to receive marketing notifications from your app, but such a notification must never break through a Focus or scheduled delivery setting. + +**Get peopleโ€™s permission if you want to send them promotional or marketing notifications.** Before you send these notifications to people, you must receive their explicit permission to do so. Create an alert, modal view, or other interface that describes the types of information you want to send and gives people a clear way to opt in or out. + +**Make sure people can manage their notification settings within your app.** In addition to requesting permission to send informational or marketing notifications, you must also provide an in-app settings screen that lets people change their choice. For guidance, see [Settings](https://developer.apple.com/design/human-interface-guidelines/settings). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/managing-notifications#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, or visionOS._ + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/managing-notifications#watchOS) + +By default, the notification settings people use for apps on their iPhone apply to the same apps on their Apple Watch. People can manage these settings in the Apple Watch app on iPhone, or they can access per-notification options โ€” such as Mute 1 Hour or Turn off Time Sensitive โ€” by swiping left when a notification arrives on their Apple Watch. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/managing-notifications#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/managing-notifications#Related) + +[Privacy](https://developer.apple.com/design/human-interface-guidelines/privacy) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/managing-notifications#Developer-documentation) + +[User Notifications](https://developer.apple.com/documentation/UserNotifications) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/managing-notifications#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/B63A08EA-8856-4C77-9E1B-EA1CAD990619/4986_wide_250x141_1x.jpg) Send communication and Time Sensitive notifications ](https://developer.apple.com/videos/play/wwdc2021/10091) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/49/3D8237BC-06E3-4711-8552-7008A5D5BAAD/3764_wide_250x141_1x.jpg) The Push Notifications primer ](https://developer.apple.com/videos/play/wwdc2020/10095) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/modality.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/modality.md new file mode 100644 index 00000000..1f4ba019 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/modality.md @@ -0,0 +1,82 @@ +--- +title: "Modality | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/modality + +# Modality + +Modality is a design technique that presents content in a separate, dedicated mode that prevents interaction with the parent view and requires an explicit action to dismiss. + +![A sketch of an active window above an inactive window, suggesting focus on the frontmost window. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/9efb35fd7fafa01ce3447dc6f13ae2d8/patterns-modality-intro%402x.png) + +Presenting content modally can: + + * Ensure that people receive critical information and, if necessary, act on it + + * Provide options that let people confirm or modify their most recent action + + * Help people perform a distinct, narrowly scoped task without losing track of their previous context + + * Give people an immersive experience or help them concentrate on a complex task + + + + +Depending on the platform, you might use different components to present these types of modal experiences. For example, all platforms can present an _alert_ , which is a modal view that delivers important information related to your app or game. In addition, each platform may define various types of modal views for presenting context-specific options, such as _activity views,_ _sheets_ , and _confirmation dialogs_ or _action sheets_. To help people perform a distinct task, iOS, iPadOS, and macOS apps tend to use sheets or popovers, but iPadOS, macOS, and visionOS apps might also just use a separate window. + +To provide a temporary experience, like viewing media, or to help people perform a distinct, multistep task, like editing content, apps can offer a full-screen modal experience. In contrast, apps may also offer nonmodal types of full-screen experiences; for guidance, see [Going full screen](https://developer.apple.com/design/human-interface-guidelines/going-full-screen). visionOS apps can offer a range of immersive experiences; for guidance, see [Immersive experiences](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/modality#Best-practices) + +**Present content modally only when thereโ€™s a clear benefit.** A modal experience takes people out of their current context and requires an action to dismiss, so itโ€™s important to use modality only when it helps people focus or make choices that affect their content or device. + +**Aim to keep modal tasks simple, short, and streamlined.** If a modal task is too complicated, people can lose track of the task they suspended when they entered the modal view, especially if the modal view obscures their previous context. + +**Take care to avoid creating a modal experience that feels like an app within your app.** In particular, presenting a hierarchy of views within a modal task can make people forget how to retrace their steps. If a modal task must contain subviews, provide a single path through the hierarchy and avoid including buttons that people might mistake for the button that dismisses the modal view. + +**Consider using a full-screen modal style for in-depth content or a complex task.** A modal experience that fills a window or the device display minimizes distractions, so it can work well for presenting videos, photos, or camera views, or to support a multistep task like marking up a document or editing a photo. When a visionOS app runs alongside other apps in the Shared Space, a full-screen modal presentation fills a window; if people transition the app to a Full Space, the full-screen modal presentation can become a more immersive experience. + +**Always give people an obvious way to dismiss a modal view.** In general, it works well to follow the platform conventions people already know. For example, in iOS, iPadOS, and watchOS apps, people typically expect to find a button in the top toolbar or swipe down; in macOS and tvOS apps, people expect to find a button in the main content view. + +**When necessary, help people avoid data loss by getting confirmation before closing a modal view.** Regardless of whether people use a dismiss gesture or a button, if closing the view could result in the loss of user-generated content, be sure to explain the situation and give people ways to resolve it. For example, in iOS, you might present an action sheet that includes a save option. + +**Make it easy to identify a modal viewโ€™s task.** When people enter a modal view, they switch away from their previous context and might not return to it right away. When you provide a title that names the modal viewโ€™s task โ€” or additional text that describes the task or provides guidance โ€” you can help people keep their place in your app. + +**Let people dismiss a modal view before presenting another one.** Allowing multiple modal views to be visible at the same time tends to create visual clutter and can make your app seem scattered and disorganized. People need to remember the context they were in before a modal view appears, so presenting multiple views adds to peopleโ€™s cognitive load, especially when a modal view hides another one by appearing on top of it. Although an alert can appear on top of all other content โ€” including other modal views โ€” you never want to display more than one alert at the same time. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/modality#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/modality#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/modality#Related) + +[Sheets](https://developer.apple.com/design/human-interface-guidelines/sheets) + +[Alerts](https://developer.apple.com/design/human-interface-guidelines/alerts) + +[Popovers](https://developer.apple.com/design/human-interface-guidelines/popovers) + +[Action sheets](https://developer.apple.com/design/human-interface-guidelines/action-sheets) + +[Activity views](https://developer.apple.com/design/human-interface-guidelines/activity-views) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/modality#Developer-documentation) + +[Presentation modifiers](https://developer.apple.com/documentation/SwiftUI/View-Presentation) โ€” SwiftUI + +[`UIModalPresentationStyle`](https://developer.apple.com/documentation/UIKit/UIModalPresentationStyle) โ€” UIKit + +[Modal Windows and Panels](https://developer.apple.com/documentation/AppKit/modal-windows-and-panels) โ€” AppKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/modality#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/1AAA030E-2ECA-47D8-AE09-6D7B72A840F6/10044_wide_250x141_1x.jpg) Get to know the new design system ](https://developer.apple.com/videos/play/wwdc2025/356) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/modality#Change-log) + +Date| Changes +---|--- +December 5, 2023| Enhanced guidance for in-depth modal experiences and clarified guidance on multiple modal views. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/multitasking.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/multitasking.md new file mode 100644 index 00000000..5788b1ad --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/multitasking.md @@ -0,0 +1,131 @@ +--- +title: "Multitasking | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/multitasking + +# Multitasking + +Multitasking lets people switch quickly from one app to another, performing tasks in each. + +![A sketch of two side-by-side windows in a split view arrangement, suggesting multitasking. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/94f1391bf700ee7af09ad0f966dd7b36/patterns-multitasking-intro%402x.png) + +People expect to use multitasking on their devices, and they may think something is wrong if your app doesnโ€™t allow it. With rare exceptions โ€” such as some games, and Apple Vision Pro apps running in a Full Space โ€” every app needs to work well with multitasking. + +In addition to app switching, multitasking can present different experiences on different devices; see [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/multitasking#Platform-considerations). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/multitasking#Best-practices) + +A great multitasking experience helps people accomplish tasks in multiple apps by managing content in a variety of simultaneous contexts. Because you donโ€™t know when people will initiate multitasking, your app or game always needs to be prepared to save and restore their context. + +**Pause activities that require peopleโ€™s attention or active participation when they switch away.** If your app is a game or a media-viewing app, for example, make sure people donโ€™t miss anything when they switch to another app. When they switch back, let them continue as if they never left. + +**Respond smoothly to audio interruptions.** Occasionally, audio from another app or the system itself may interrupt your appโ€™s audio. For example, an incoming phone call or a music playlist initiated by Siri might interrupt your appโ€™s audio. When situations like these occur, people expect your app to respond in the following ways: + + * Pause audio indefinitely for primary audio interruptions, such as playing music, podcasts, or audiobooks. + + * Temporarily lower the volume or pause the audio for shorter interruptions, such as GPS directional notifications, and resume the original volume or playback when the interruption ends. + + + + +For guidance, see [Playing audio](https://developer.apple.com/design/human-interface-guidelines/playing-audio). + +**Finish user-initiated tasks in the background.** When someone starts a task like downloading assets or processing a video file, they expect it to finish even if they switch away from your app. If your app is in the middle of performing a task that doesnโ€™t need additional input, complete it in the background before suspending. + +**Use notifications sparingly.** Your app can send notifications when itโ€™s suspended or running in the background. If people start an important or time-sensitive task in your app, and then switch away from it, they might appreciate receiving a notification when the task completes so they can switch back to your app and take the next step. In contrast, people donโ€™t generally need to know the moment a routine or secondary task completes. In this scenario, avoid sending an unnecessary notification; instead, let people check on the task when they return to your app. For guidance, see [Managing notifications](https://developer.apple.com/design/human-interface-guidelines/managing-notifications). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/multitasking#Platform-considerations) + + _Not supported in watchOS._ + +### [iOS](https://developer.apple.com/design/human-interface-guidelines/multitasking#iOS) + +On iPhone, multitasking lets people use FaceTime or watch a video in Picture in Picture while they also use a different app. + +![A screenshot of the app switcher on iPhone, showing four open apps.](https://docs-assets.developer.apple.com/published/519ce5b2d1298e573aab62d4ea3427c9/multitasking-app-switcher-iphone%402x.png) + +The app switcher displays all currently open apps. + +![A screenshot of Mail on iPhone, showing an individual email. On top of the email body content, a small image in the bottom-left corner shows the person currently in a FaceTime call.](https://docs-assets.developer.apple.com/published/f68005bf620706a5d6c6c03d09af37f4/multitasking-pip-iphone%402x.png) + +A current FaceTime call can continue while people use another app. + +### [iPadOS](https://developer.apple.com/design/human-interface-guidelines/multitasking#iPadOS) + +On iPad, people can view and interact with the [windows](https://developer.apple.com/design/human-interface-guidelines/windows) of several different apps at the same time. An individual app can also support multiple open windows, which lets people view and interact with more than one window in the same app at one time. + +People can use iPad with either full-screen or windowed apps. When full screen, apps occupy the full screen, and people can switch between individual app windows using the app switcher. + +![A screenshot of the iPad app switcher in landscape orientation, showing five open apps. Thumbnail representations of the apps are arranged in a grid.](https://docs-assets.developer.apple.com/published/b1c2946808ad75c7036af18706a55b79/multitasking-ipad-app-switcher%402x.png) + +When using windowed apps, app windows are resizable, and people can arrange them to suit their needs with behavior similar to macOS. The system provides window controls for common tiling configurations, entering full screen, minimizing, and closing windows. The system identifies the frontmost window by coloring its window controls and casting a drop shadow on windows behind it. For guidance, see [Windows > iPadOS](https://developer.apple.com/design/human-interface-guidelines/windows#iPadOS). + +![A screenshot of two windowed apps on iPad in landscape orientation. The frontmost app window overlaps and casts a shadow on the one behind it, and has colored window controls to indicate that the window is active. Both windows sit atop the Home Screen background, and the Dock appears at the bottom.](https://docs-assets.developer.apple.com/published/433d49d66e117152f7cca9605ebe9628/multitasking-ipad-windows-maps-landmarks%402x.png) + +Additionally, videos and FaceTime calls can also play in a Picture in Picture overlay above other content regardless of whether apps are full screen or windowed. + +Note + +Apps donโ€™t control multitasking configurations or receive any indication of the ones that people choose. + +To help your app respond correctly when people open it while windowed, make sure it adapts gracefully to different screen sizes. For guidance, see [Layout](https://developer.apple.com/design/human-interface-guidelines/layout) and [Windows](https://developer.apple.com/design/human-interface-guidelines/windows); for developer guidance, see [Multitasking on iPad, Mac, and Apple Vision Pro](https://developer.apple.com/documentation/UIKit/multitasking-on-ipad-mac-and-apple-vision-pro). To learn more about how people use iPad multitasking features, see [Use multitasking on your iPad](https://support.apple.com/en-us/HT207582). + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/multitasking#macOS) + +On Mac, multitasking is the default experience because people typically run more than one app at a time, switching between windows and tasks as they work. When multiple app windows are open, macOS applies drop shadows that make the windows appear layered on the desktop, and applies other visual effects to help people distinguish different window states; for guidance, see [macOS window states](https://developer.apple.com/design/human-interface-guidelines/windows#macOS-window-states). + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/multitasking#tvOS) + +On Apple TV, people can play or browse content while also playing movies or TV shows in Picture in Picture (where supported). + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/multitasking#visionOS) + +On Apple Vision Pro, people can run multiple apps at the same time in the Shared Space, viewing and switching between windows and volumes throughout the space. + +Only one window is active at a time in the Shared Space. When people look from one window to another, the window theyโ€™re currently looking at becomes active while the previous window becomes more translucent and appears to recede along the z-axis. Closing an app window in the Shared Space transitions the app to the background without quitting it. + +Note + +When an app is the Now Playing app, closing its window automatically pauses audio playback; if people want to resume playback, they can do so in Control Center without opening the window. + +**Avoid interfering with the system-provided multitasking behavior.** When people look from one window to another, visionOS applies a feathered mask to the window they look away from to clarify its changed state. To avoid interfering with this visual feedback, donโ€™t change the appearance of a windowโ€™s edges. + +Video with custom controls. + +Content description: A recording showing the Notes app and the Settings app in the Shared Space in visionOS. The viewer first repositions the Notes window to slightly overlap the Settings window before activating Settings and then switching back to Notes. Each time an app becomes active, the system applies feathering to the inactive app's window. + +Play + +**Donโ€™t pause a windowโ€™s video playback when people look away from it.** In visionOS, as in macOS, people expect the playback they start in one window to continue while they view or perform a task in another window. + +**Be prepared for situations where your audio can duck.** Unless an app is currently the Now Playing app, its audio can duck when people look away from it to another app. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/multitasking#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/multitasking#Related) + +[Layout](https://developer.apple.com/design/human-interface-guidelines/layout) + +[Windows](https://developer.apple.com/design/human-interface-guidelines/windows) + +[Playing video](https://developer.apple.com/design/human-interface-guidelines/playing-video) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/multitasking#Developer-documentation) + +[Responding to the launch of your app](https://developer.apple.com/documentation/UIKit/responding-to-the-launch-of-your-app) โ€” UIKit + +[Multitasking on iPad, Mac, and Apple Vision Pro](https://developer.apple.com/documentation/UIKit/multitasking-on-ipad-mac-and-apple-vision-pro) โ€” UIKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/multitasking#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/873F40BE-101A-4C0D-99F0-F5C7CE7B47A3/10046_wide_250x141_1x.jpg) Elevate the design of your iPad app ](https://developer.apple.com/videos/play/wwdc2025/208) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/A8CAF870-197F-4982-83D8-56513E5D7D0B/10000_wide_250x141_1x.jpg) Make your UIKit app more flexible ](https://developer.apple.com/videos/play/wwdc2025/282) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/multitasking#Change-log) + +Date| Changes +---|--- +June 9, 2025| Reorganized guidance in platform considerations, and added guidance for multitasking with multiple windows in iPadOS. +December 5, 2023| Added artwork for primary and auxiliary windows in iPadOS. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/offering-help.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/offering-help.md new file mode 100644 index 00000000..38eabce6 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/offering-help.md @@ -0,0 +1,117 @@ +--- +title: "Offering help | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/offering-help + +# Offering help + +Although the most effective experiences are approachable and intuitive, you can provide contextual help when necessary. + +![A sketch of a question mark, suggesting help is available. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/c09494b87143553d5b544aade5282148/patterns-offering-help-intro%402x.png) + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/offering-help#Best-practices) + +**Let your appโ€™s tasks inform the types of help people might need.** For example, you might help people perform simple, one- or two-step tasks by displaying an inline view that succinctly describes the task. In contrast, if your app or game supports complex or multistep tasks you might want to provide a tutorial that teaches people how to accomplish larger goals. In general, directly relate the help you provide to the precise action or task people are doing right now and make it easy for people to dismiss or avoid the help if they donโ€™t need it. + +**Use relevant and consistent language and images in your help content.** Always make sure guidance is appropriate for the current context. For example, if someoneโ€™s using the Siri Remote with your tvOS experience, donโ€™t show tips or images that feature a game controller. Also be sure the terms and descriptions you use are consistent with the platform. For example, donโ€™t write copy that tells people to click a button on an iPhone or tap a menu item on a Mac. + +**Make sure all help content is inclusive.** For guidance, see [Inclusion](https://developer.apple.com/design/human-interface-guidelines/inclusion). + +**Avoid bloating your help content by explaining how standard components or patterns work.** Instead, describe the specific action or task that a standard element performs in your app or game. If your experience introduces a unique control or expects people to use an input device in a nonstandard way โ€” such as holding the Siri Remote rotated 90 degrees โ€” orient people quickly, preferring animation or graphics to educate instead of a lengthy description. + +## [Creating tips](https://developer.apple.com/design/human-interface-guidelines/offering-help#Creating-tips) + +A tip is a small, transient view that briefly describes how to use a feature in your app. Tips are a great way to teach people about new or less obvious features in your app, or help them discover faster ways to accomplish a task. For developer guidance, see [TipKit](https://developer.apple.com/documentation/TipKit). + +**Use the most appropriate tip type for your appโ€™s user interface.** Display a popover tip when you want to preserve the content flow, or an inline tip when you want to ensure that surrounding information is visible. You can use an annotation-style inline tip when pointing to a specific UI element, or a hint-style tip when itโ€™s not related to a specific piece of UI. + +![An illustration of a popover-style tip on iPhone. The tip appears atop nearby content, and points to a feature depicted by a blue star icon. The content beneath the tip is obscured.](https://docs-assets.developer.apple.com/published/2a1deca274cc855ac01321f3a583b858/offering-help-tip-popover%402x.png) + +Popover + +![An illustration of an annotation-style tip on iPhone. The tip is embedded among the surrounding content, and points to a feature depicted by a blue star icon. Displaced text appears above and below the tip.](https://docs-assets.developer.apple.com/published/e2b5c6a0a9d94a6c7a16cb1b2c9be517/offering-help-tip-annotation%402x.png) + +Annotation + +![An illustration of an hint-style tip on iPhone. The tip is embedded among the surrounding content. Displaced text appears above and below the tip.](https://docs-assets.developer.apple.com/published/706238176beb6869a8cf4cd06e22912c/offering-help-tip-hint%402x.png) + +Hint + +**Use tips for simple features.** Tips work best on features that are easy to describe and that people can complete with a few simple steps. If a feature requires more than three actions, itโ€™s probably too complicated for a tip. + +**Make tips short, actionable, and engaging.** A tipโ€™s goal is to encourage people to try new features. Use direct, action-oriented language to describe what the feature does and explain how to use it. Keep your tips to one or two sentences and avoid including content thatโ€™s promotional or related to a different feature or user flow. Promotional content is anything that advertises, sells, or isnโ€™t aligned with the current context of what the person is doing. + +**Define rules to help ensure your tips reach the intended audience.** Not everyone benefits from every tip. For example, people whoโ€™ve already used a feature wonโ€™t appreciate viewing a tip that describes it. Use parameter-based or event-based eligibility rules to control when a tip appears, and only display a tip if someone might benefit from its use. When your app has more than one tip, set the display frequency so tips display at a reasonable cadence โ€” for example, once every 24 hours. + +**If thereโ€™s an image or symbol that people associate with the feature, consider including it in the tip, and prefer the filled variant.** For example, a tip with a star can help people understand that the tip is related to favorites. + +![An illustration of a hint-style tip with an unfilled blue star symbol on the leading side.](https://docs-assets.developer.apple.com/published/e4a119cd09255a5e22c5132263c39cd9/offering-help-tip-symbol-usage-unfilled-incorrect%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration of a hint-style tip with a filled blue star symbol on the leading side.](https://docs-assets.developer.apple.com/published/be5eb0f23474419bcb3b182b24c24d77/offering-help-tip-symbol-usage-filled-correct%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +If the feature is represented by an image that the tip connects to directly, avoid repeating the same image in both the tip and the UI. + +![An illustration of an annotation-style tip pointing to a feature depicted by a blue star icon. The tip includes a similar blue star symbol on its leading side.](https://docs-assets.developer.apple.com/published/b35499b146567d76a7ca996cf3efb9e9/offering-help-tip-symbol-usage-incorrect%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png) + +![An illustration of an annotation-style tip pointing to a feature depicted by a blue star icon. The tip is text-only and omits an accompanying symbol.](https://docs-assets.developer.apple.com/published/afa3b233a9ec05e15e54fd1ec909c015/offering-help-tip-symbol-usage-correct%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png) + +**Use buttons to direct people to information or options.** If your feature has settings people can customize, or you want to redirect people to an area where they can learn more about a feature, consider adding a button. Buttons can take people directly to the settings where they make adjustments. Or if thereโ€™s more information people might find useful, add a button to take them to additional resources, such as a setup flow. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/offering-help#Platform-considerations) + + _No additional considerations for iOS, iPadOS, tvOS, or watchOS._ + +### [macOS, visionOS](https://developer.apple.com/design/human-interface-guidelines/offering-help#macOS-visionOS) + +A _tooltip_ (called a _help tag_ in user documentation) displays a small, transient view that briefly describes how to use a component in the interface. In apps that run on a Mac โ€” including iPhone and iPad apps โ€” tooltips can appear when a person holds the pointer over an element; in visionOS apps, a tooltip can appear when a person looks at an element or holds the pointer over it. For developer guidance, see [`help(_:)`](https://developer.apple.com/documentation/SwiftUI/View/help\(_:\)-6oiyb). + +![An illustration of a toolbar in macOS Finder with the pointer over the Back button. A tooltip with the title See folders you viewed previously appears beneath the pointer.](https://docs-assets.developer.apple.com/published/a5dc5c63ac62773df2b4aea95ad85f39/offering-help-macos-tooltip-help-tag%402x.png) + +**Describe only the control that people indicate interest in.** When people want to know how to use a specific control, they donโ€™t want to learn how to use nearby controls or how to perform a larger task. + +**Explain the action or task the control initiates.** It often works well to begin the description with a verb โ€” for example, โ€œRestore default settingsโ€ or โ€œAdd or remove a language from the list.โ€ + +**In general, avoid repeating a controlโ€™s name in its tooltip.** Repeating the name takes up space in the tooltip and rarely adds value to the description. + +**Be brief.** As much as possible, limit tooltip content to a maximum of 60 to 75 characters (note that localization often changes the length of text). To make a description brief and direct, consider using a sentence fragment and omitting articles. If you need a lot of text to describe a control, consider simplifying your interface design. + +**Use sentence case.** Sentence case tends to appear more casual and approachable. If you write complete sentences, omit ending punctuation unless itโ€™s required to be consistent with your appโ€™s style. + +**Consider offering context-sensitive tooltips.** For example, you could provide different text for a controlโ€™s different states. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/offering-help#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/offering-help#Related) + +[Onboarding](https://developer.apple.com/design/human-interface-guidelines/onboarding) + +[Feedback](https://developer.apple.com/design/human-interface-guidelines/feedback) + +[Writing](https://developer.apple.com/design/human-interface-guidelines/writing) + +[Help menu](https://developer.apple.com/design/human-interface-guidelines/the-menu-bar#Help-menu) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/offering-help#Developer-documentation) + +[TipKit](https://developer.apple.com/documentation/TipKit) + +[`NSHelpManager`](https://developer.apple.com/documentation/AppKit/NSHelpManager) โ€” AppKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/offering-help#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/7BCB7D16-5D51-419D-B332-E008219FB631/8293_wide_250x141_1x.jpg) Make features discoverable with TipKit ](https://developer.apple.com/videos/play/wwdc2023/10229) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/offering-help#Change-log) + +Date| Changes +---|--- +December 5, 2023| Included visionOS in guidance for creating tooltips. +September 12, 2023| Added guidance for creating tips. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/onboarding.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/onboarding.md new file mode 100644 index 00000000..c074cfcc --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/onboarding.md @@ -0,0 +1,69 @@ +--- +title: "Onboarding | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/onboarding + +# Onboarding + +Onboarding can help people get a quick start using your app or game. + +![A sketch of a waving hand, suggesting a gesture of welcoming. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/044d56043e30ee77f306a8613041311b/patterns-onboarding-intro%402x.png) + +Ideally, people can understand your app or game simply by experiencing it, but if onboarding is necessary, design a flow thatโ€™s fast, fun, and optional. When available, onboarding occurs after [launching](https://developer.apple.com/design/human-interface-guidelines/launching) is complete โ€” it isnโ€™t part of the launch experience. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/onboarding#Best-practices) + +**Teach through interactivity.** People tend to grasp and retain information better when they can actually perform the task theyโ€™re learning about instead of just viewing instructional material. As much as possible, provide an interactive onboarding experience where people can safely test an action, discover a feature, or try out a game mechanic. + +**Consider providing a collection of context-specific tips instead of a single onboarding flow.** Integrating contextually relevant tips into your experience can help people learn about their current task while they make progress in your app or game. A context-specific tip can also help people learn better because it lets them concentrate on a single action or task before encountering new information. When you have instructional content that refers to a specific area of the interface, display these instructions near that area. For developer guidance, see [TipKit](https://developer.apple.com/documentation/TipKit). + +**If you need to present a prerequisite onboarding flow, design a brief, enjoyable experience that doesnโ€™t require people to memorize a lot of information.** When onboarding is quick and entertaining, people are more likely to complete it. In contrast, if you try to teach too much, people can feel overwhelmed and may be less likely to remember what they learned. + +**If it makes sense to offer a separate tutorial, consider making it optional.** If you let people skip the tutorial when they first launch your app or game, donโ€™t present it again on subsequent launches, but make sure itโ€™s easy for people to find if they want to view it later. For example, you could make the tutorial available in a help, account, or settings area within your app or game. + +**Keep onboarding content focused on the experience you provide.** People enter your onboarding flow to learn about your app or game; they donโ€™t need to learn how to use the system or the device. + +## [Additional content](https://developer.apple.com/design/human-interface-guidelines/onboarding#Additional-content) + +**Briefly display a splash screen if necessary.** If you need to include a splash screen, design a beautiful graphic that communicates succinctly. Aim to display your splash screen just long enough for people to absorb the information at a glance without feeling that itโ€™s delaying their experience. + +**Donโ€™t let large downloads hinder onboarding.** People want to start using your app or game immediately after first launching it, whether they participate in an onboarding flow or skip it. Consider including enough media and other content in your software package to prevent people from having to wait for downloads to complete before they can start interacting with your app or game. For guidance, see [Launching](https://developer.apple.com/design/human-interface-guidelines/launching). + +**Avoid displaying licensing details within your onboarding flow.** Let the App Store display agreements and disclaimers so people can read them before downloading your app or game. If you must include these items within the onboarding flow, integrate them in a balanced way that doesnโ€™t disrupt the experience. + +## [Additional requests](https://developer.apple.com/design/human-interface-guidelines/onboarding#Additional-requests) + +**Postpone nonessential setup flows or customization steps.** Provide reasonable default settings so most people can immediately start interacting with your app or game without performing additional configuration. + +**If your app or game needs access to private data or resources before it can function, consider integrating the permission request into your onboarding flow.** In this scenario, making the request during your onboarding flow gives you the opportunity to show people why your app or game needs their permission and the benefits of granting it. Otherwise, present a permission request when people first access the specific function that relies on private data or resources. For guidance, see [Requesting permission](https://developer.apple.com/design/human-interface-guidelines/privacy#Requesting-permission). + +**Prefer letting people experience your app or game before prompting them for ratings or purchases.** People can be more likely to respond positively to such requests when theyโ€™ve had a chance to become engaged with your app or game. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/onboarding#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/onboarding#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/onboarding#Related) + +[Launching](https://developer.apple.com/design/human-interface-guidelines/launching) + +[Feedback](https://developer.apple.com/design/human-interface-guidelines/feedback) + +[Offering help](https://developer.apple.com/design/human-interface-guidelines/offering-help) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/onboarding#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/BE8FF113-0FE1-40FC-86BF-FE95BE2FF7A5/5027_wide_250x141_1x.jpg) Discoverable design ](https://developer.apple.com/videos/play/wwdc2021/10126) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/48/B27C091C-CF5C-4E42-B1E2-9172615462AB/2724_wide_250x141_1x.jpg) Designing Award Winning Apps and Games ](https://developer.apple.com/videos/play/wwdc2019/802) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/7/2C48F507-180B-4858-BB26-488C234B067F/1920_wide_250x141_1x.jpg) Love at First Launch ](https://developer.apple.com/videos/play/wwdc2017/816) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/onboarding#Change-log) + +Date| Changes +---|--- +June 10, 2024| Clarified different approaches to onboarding and added a guideline on displaying a splash screen. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/playing-audio.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/playing-audio.md new file mode 100644 index 00000000..02767258 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/playing-audio.md @@ -0,0 +1,124 @@ +--- +title: "Playing audio | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/playing-audio + +# Playing audio + +People expect rich audio experiences that automatically adjust when the context changes on the device. + +![A sketch of a speaker emitting sound waves, suggesting the playback of audio. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/1cf1272e2d6283fc614294242efe2145/patterns-playing-audio-intro%402x.png) + +Devices can play audio in a variety of ways, such as through internal or external speakers, headphones, and wirelessly through devices that use Bluetooth or AirPlay. To manipulate sound on their devices people use several types of controls, including volume buttons, the Ring/Silent switch on iPhone, headphone controls, the Control Center volume slider, and sound controls in third-party accessories. Whether sound is a primary part of your experience or an embellishment, you need to make sure it behaves as people expect as they make changes to volume and output. + +**Silence.** People switch a device to silent when they want to avoid being interrupted by unexpected sounds like ringtones and incoming message tones. In this scenario, they also want to silence nonessential sounds, such as keyboard clicks, sound effects, game soundtracks, and other audible feedback. When a device is in silent mode, it plays only the audio that people explicitly initiate, like media playback, alarms, and audio/video messaging. + +**Volume.** People expect their volume settings to affect all sound in the system โ€” including music and in-app sound effects โ€” regardless of the method they use to adjust the volume. An exception is the ringer volume on iPhone, which people can adjust separately in Settings. + +**Headphones.** People use headphones to keep their listening private and in some cases to free their hands. When connecting headphones, people expect sound to reroute automatically without interruption; when disconnecting headphones, they expect playback to pause immediately. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/playing-audio#Best-practices) + +**Adjust levels automatically when necessary โ€” donโ€™t adjust the overall volume.** Your app can adjust relative, independent volume levels to achieve a great mix of audio, but the system volume always governs the final output. + +**Permit rerouting of audio when possible.** People often want to select a different audio output device. For example, they may want to listen to music through their living room stereo, car radio, or Apple TV. Support this capability unless thereโ€™s a compelling reason not to. + +**Use the system-provided volume view to let people make audio adjustments.** The volume view includes a volume-level slider and a control for rerouting audio output. You can customize the appearance of the slider. For developer guidance, see [`MPVolumeView`](https://developer.apple.com/documentation/MediaPlayer/MPVolumeView). + +**Choose an audio category that fits the way your app or game uses sound.** Depending on the audio category you choose, your appโ€™s sounds can mix with other audio, play while your app is in the background, or stop when people set the Ring/Silent switch to silent. As much as possible, pick a category that helps your app meet peopleโ€™s expectations. For example, donโ€™t make people stop listening to music from another app if you donโ€™t need to. For developer guidance, see [`AVAudioSession.Category`](https://developer.apple.com/documentation/AVFAudio/AVAudioSession/Category-swift.struct). + +Category| Meaning| Behavior +---|---|--- +Solo ambient| Sound isnโ€™t essential, but it silences other audio. For example, a game with a soundtrack.| Responds to the silence switch. Doesnโ€™t mix with other sounds. Doesnโ€™t play in the background. +Ambient| Sound isnโ€™t essential, and it doesnโ€™t silence other audio. For example, a game that lets people play music from another app during gameplay in place of the gameโ€™s soundtrack.| Responds to the silence switch. Mixes with other sounds. Doesnโ€™t play in the background. +Playback| Sound is essential and might mix with other audio. For example, an audiobook or educational app that teaches a foreign language, which people might want to listen to after leaving the app.| Doesnโ€™t respond to the silence switch. May or may not mix with other sounds. Can play in the background. +Record| Sound is recorded. For example, a note-taking app that offers an audio recording mode. An app of this nature might switch its category to playback if it lets people play the recorded notes.| Doesnโ€™t respond to the silence switch. Doesnโ€™t mix with other sounds. Can record in the background. +Play and record| Sound is recorded and played, potentially simultaneously. For example, an audio messaging or video calling app.| Doesnโ€™t respond to the silence switch. May or may not mix with other sounds. Can record and play in the background. + +**Respond to audio controls only when it makes sense.** People can control audio playback from outside your appโ€™s interface โ€” such as in Control Center or with controls on their headphones โ€” regardless of whether your app is in the foreground or background. If your app is actively playing audio, in a clear audio-related context, or connected to a device that uses Bluetooth or AirPlay, itโ€™s fine to respond to audio controls. Otherwise, when people activate a control, avoid halting audio currently playing from another app. + +**Avoid repurposing audio controls.** People expect audio controls to behave consistently in all apps, so itโ€™s essential to avoid redefining the meaning of an audio control in your app. If your app doesnโ€™t support certain controls, donโ€™t respond to them. + +**Consider creating custom audio player controls only if you need to offer commands that the system doesnโ€™t support.** For example, you might want to define custom increments for skipping forward or backward, or present content thatโ€™s related to the playing audio, such as a sports score. + +**Let other apps know when your app finishes playing temporary audio.** If your app can temporarily interrupt the audio of other apps, be sure to flag your audio session in a way that lets other apps know when they can resume. For developer guidance, see [`notifyOthersOnDeactivation`](https://developer.apple.com/documentation/AVFAudio/AVAudioSession/SetActiveOptions/notifyOthersOnDeactivation). + +## [Handling interruptions](https://developer.apple.com/design/human-interface-guidelines/playing-audio#Handling-interruptions) + +Although most apps and games rely on the systemโ€™s default interruption behavior, you can customize this behavior to better accommodate your needs. + +**Determine how to respond to audio-session interruptions.** For example, if your app supports recording or other audio-related tasks that people donโ€™t want interrupted, you can tell the system to avoid interrupting the currently playing audio for an incoming call unless people choose to accept it. Another example is a VoIP app, which must end a call when people close the Smart Folio of their iPad while theyโ€™re using the built-in microphone. Closing the Smart Folio automatically mutes the iPad microphone and by default interrupts the audio session associated with it. If a VoIP app restarts the audio session when people reopen their Smart Folio, it risks invading peopleโ€™s privacy by unmuting the microphone without their knowledge. You can inspect an audio-session interruption to help determine the right way to respond; for developer guidance, see [Handling audio interruptions](https://developer.apple.com/documentation/AVFAudio/handling-audio-interruptions). + +**When an interruption ends, determine whether to resume audio playback automatically.** Sometimes, audio from a different app can interrupt the audio your app is playing. An interruption can be _resumable_ , like an incoming phone call, or _nonresumable_ , like when people start a new music playlist. Use the interruption type and your appโ€™s type to decide whether to resume playback automatically. For example, a media playback app thatโ€™s actively playing audio when an interruption occurs can check to be sure the type is resumable before continuing playback when the interruption ends. On the other hand, a game doesnโ€™t need to check the interruption type before automatically resuming playback, because a game plays audio without an explicit user choice. For developer guidance, see [`shouldResume`](https://developer.apple.com/documentation/AVFAudio/AVAudioSession/InterruptionOptions/shouldResume). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/playing-audio#Platform-considerations) + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/playing-audio#iOS-iPadOS) + +**Use the systemโ€™s sound services to play short sounds and vibrations.** For developer guidance, see [Audio Services](https://developer.apple.com/documentation/AudioToolbox/audio-services). + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/playing-audio#macOS) + +In macOS, notification sounds mix with other audio by default. + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/playing-audio#tvOS) + +In tvOS, the system plays audio only when people initiate it, through interactions within apps and games or when performing device calibrations. For example, tvOS doesnโ€™t play sounds to accompany components like alerts or notifications. + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/playing-audio#visionOS) + +Subtle, expressive sounds are everywhere in visionOS, enhancing experiences and providing essential feedback when people look at a virtual object and use gestures to interact with it. The system combines audio algorithms with information about a personโ€™s physical surroundings to produce _Spatial Audio_ , which is sound that people can perceive as coming from specific locations in space, not just from speakers. + +Important + +In visionOS, as in every platform, avoid communicating important information using only sound. Always provide additional ways to help people understand your app. For guidance, see [Accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility). + +In visionOS, audio playback from the Now Playing app pauses automatically when people close the appโ€™s window, and audio from an app that isnโ€™t the Now Playing app can duck when people look away from it to different app. + +**Prefer playing sound.** People generally choose to keep sounds audible while theyโ€™re wearing the device, so an app that doesnโ€™t play sound โ€” especially in an immersive moment โ€” can feel lifeless and may even seem broken. Throughout the design process, look for opportunities to create meaningful sounds that aid navigation and help people understand the spatial qualities of your app. + +**Design custom sounds for custom UI elements.** In general, a system-provided element plays sound to help people locate it and receive feedback when they interact with it. To help people interact with your custom elements, design sounds that provide feedback and enhance the spatial experience of your app. + +**Use Spatial Audio to create an intuitive, engaging experience.** Because people can perceive Spatial Audio as coming from anywhere around them, it works especially well in a fully immersive context as a way to help an experience feel lifelike. _Ambient audio_ provides pervasive sounds that can help anchor people in a virtual world and an _audio source_ can sound like it comes from a specific object. As you build the soundscape for your app, consider using both types of audio. + +**Consider defining a range of places from which your app sounds can originate.** Spatial Audio helps people locate the object thatโ€™s making sound, whether itโ€™s stationary or moving in space. For example, when people move an app window thatโ€™s playing audio, the sound continues to come directly from the window, wherever people move it. + +**Consider varying sounds that people could perceive as repetitive over time.** For example, the system subtly varies the pitch and volume of the virtual keyboardโ€™s sounds, suggesting the different sounds a physical keyboard can make as people naturally vary the speed and forcefulness of their typing. An efficient way to achieve a pleasing variation in sound is to randomize a sound fileโ€™s pitch and volume during playback, instead of creating different files. + +**Decide whether you need to play sound thatโ€™s fixed to the wearer or tracked by the wearer.** People perceive _fixed_ sound as if itโ€™s pointed at them, regardless of the direction they look or the virtual objects they move. In contrast, people tend to perceive _tracked_ sound as coming from a particular object, so moving the object closer or farther away changes what they hear. In general, you want to use tracked sound to enhance the realism of your experience, but there could be cases where fixed sound is a good choice. For example, Mindfulness uses fixed sound to envelop the wearer in an engaging, peaceful setting. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/playing-audio#watchOS) + +In watchOS, the system manages audio playback. An app can play short audio clips while itโ€™s active and running in the foreground, or it can play longer audio that continues even when people lower their wrist or switch to another app. For developer guidance, see [Playing Background Audio](https://developer.apple.com/documentation/WatchKit/playing-background-audio). + +**Use the recommended encoding values for media assets.** Specifically, use the 64 kbps HE-AAC (High-Efficiency Advanced Audio Coding) format to produce good-quality audio with lower data requirements. + +**Consider** **presenting a Now Playing view so people can control current or recently played audio without leaving your app.** The system-provided Now Playing view also displays information about the current audio source โ€” which might be another app on a personโ€™s Apple Watch or iPhone โ€” and automatically selects the current or most recently used source. For developer guidance, see [Adding a Now Playing View](https://developer.apple.com/documentation/WatchKit/adding-a-now-playing-view). + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/playing-audio#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/playing-audio#Related) + +[Playing video](https://developer.apple.com/design/human-interface-guidelines/playing-video) + +[Feedback](https://developer.apple.com/design/human-interface-guidelines/feedback) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/playing-audio#Developer-documentation) + +[Configuring your app for media playback](https://developer.apple.com/documentation/AVFoundation/configuring-your-app-for-media-playback) โ€” AVFoundation + +[`AVAudioSession`](https://developer.apple.com/documentation/AVFAudio/AVAudioSession) โ€” AVFAudio + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/playing-audio#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/FAD92809-C8B7-4968-802C-C662B1AF6C94/8340_wide_250x141_1x.jpg) Explore immersive sound design ](https://developer.apple.com/videos/play/wwdc2023/10271) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/15489B11-8744-483D-AD38-EF78D8962FF4/8126_wide_250x141_1x.jpg) Principles of spatial design ](https://developer.apple.com/videos/play/wwdc2023/10072) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/28DB1BEB-3F4F-4418-ACD3-779DBFB889CB/5196_wide_250x141_1x.jpg) Immerse your app in Spatial Audio ](https://developer.apple.com/videos/play/wwdc2021/10265) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/playing-audio#Change-log) + +Date| Changes +---|--- +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/playing-haptics.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/playing-haptics.md new file mode 100644 index 00000000..f8fd3f6d --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/playing-haptics.md @@ -0,0 +1,280 @@ +--- +title: "Playing haptics | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/playing-haptics + +# Playing haptics + +Playing haptics can engage peopleโ€™s sense of touch and bring their familiarity with the physical world into your app or game. + +![A sketch of a horizontal line of three slightly overlapping circles, suggesting vibration. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/367115f42d7b6235a3087fd140366955/patterns-playing-haptics-intro%402x.png) + +Depending on the platform and the device people are using, the system can play haptics in addition to visual and auditory feedback. For example, components like switches, sliders, and pickers automatically play haptic feedback on supported iPhone models; on Apple Watch, the Taptic Engine generates haptics for a number of built-in feedback patterns, which watchOS combines with an audible tone. On a Mac thatโ€™s equipped with a Force Touch trackpad, an app can play haptics while people drag content or when they force click to change the speed of media controls. + +In addition to built-in haptic capabilities, some external input devices can also play haptics. For example: + + * In an iPadOS, macOS, tvOS, or visionOS app or game, [game controllers](https://developer.apple.com/design/human-interface-guidelines/game-controls) can provide haptic feedback (for developer guidance, see [Playing Haptics on Game Controllers](https://developer.apple.com/documentation/CoreHaptics/playing-haptics-on-game-controllers)). + + * [Apple Pencil Pro](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble) and some trackpads can provide haptic feedback when connected to certain iPad models. (For details on Apple Pencil features and compatibility, see [Apple Pencil](https://www.apple.com/apple-pencil/).) + + + + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Best-practices) + +**Use system-provided haptic patterns according to their documented meanings.** People recognize standard haptics because the system plays them consistently on interactions with standard controls. If the documented use case for a pattern doesnโ€™t make sense in your app or game, avoid using the pattern to mean something else. Instead, use a generic pattern or create your own, where supported. For guidance, see [Custom haptics](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Custom-haptics). + +**Use haptics consistently throughout your app or game.** Itโ€™s important to build a clear, causal relationship between each haptic and the action that causes it so people learn to associate certain haptic patterns with certain experiences. If a haptic doesnโ€™t reinforce a cause-and-effect relationship, it can be confusing and seem gratuitous. For example, if your game plays a specific haptic pattern when a character fails to finish a mission, people associate that pattern with a negative outcome. If you use the same haptic pattern for a positive outcome like a level completion, people will be confused. + +**Prefer using haptics to complement other feedback in your app or game.** When visual, auditory, and tactile feedback are in harmony โ€” as they generally are in the physical world โ€” the user experience is more coherent and can seem more natural. For example, you generally want to match the intensity and sharpness of a haptic with the intensity and sharpness of the animation it accompanies. You can also synchronize sound with haptics; for developer guidance, see [Delivering Rich App Experiences with Haptics](https://developer.apple.com/documentation/CoreHaptics/delivering-rich-app-experiences-with-haptics). + +**Avoid overusing haptics.** Sometimes a haptic can feel just right when it happens occasionally, but become tiresome when it plays frequently. Doing user testing can help you discover a balance that most people appreciate. Often, the best haptic experience is one that people may not be conscious of, but miss when itโ€™s turned off. + +**In most apps, prefer playing short haptics that complement discrete events.** Although long-running haptics that accompany a gameplay flow can enhance the experience, long-running haptics in an app can dilute the meaning of the feedback and distract people from their task. On Apple Pencil Pro, for example, continuous or long-lasting haptics donโ€™t tend to clarify the writing or drawing experience and can even make holding the pencil less pleasant. + +**Make haptics optional.** Let people turn off or mute haptics, and make sure people can still enjoy your app or game without them. + +**Be aware that playing haptics might impact other user experiences.** By design, haptics produce enough physical force for people to feel the vibration. Ensure that haptic vibrations donโ€™t disrupt experiences involving device features like the camera, gyroscope, or microphone. + +## [Custom haptics](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Custom-haptics) + +Games often use custom haptics to enhance gameplay. Although itโ€™s less common, nongame apps might also use custom haptics to provide a richer, more delightful experience. + +You can design custom haptic patterns that vary dynamically, based on user input or context. For example, the impact players feel when a game character jumps from a tree can be stronger than when the character jumps in place, and substantial experiences โ€” like a collision or a hit โ€” can feel very different from subtle experiences like the approach of footsteps or a looming danger. + +There are two basic building blocks you can use to generate custom haptic patterns. + + * _Transient_ events are brief and compact, often feeling like taps or impulses. The experience of tapping the Flashlight button on the Home Screen is an example of a transient event. + + * _Continuous_ events feel like sustained vibrations, such as the experience of the lasers effect in a message. + + + + +Regardless of the type of haptic event you use to generate a custom haptic, you can also control its _sharpness_ and _intensity_. You can think of sharpness as a way to abstract a haptic experience into the waveform that produces the corresponding physical sensations. Specifying sharpness lets you relay to the system your intent for the experience. For example, you might use sharpness values to convey an experience thatโ€™s soft, rounded, or organic, or one thatโ€™s crisp, precise, or mechanical. As the term implies, intensity means the strength of the haptic. + +By combining transient and continuous events, varying sharpness and intensity, and including optional audio content, you can create a wide range of different haptic experiences. For developer guidance, see [Core Haptics](https://developer.apple.com/documentation/CoreHaptics). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Platform-considerations) + +### [iOS](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#iOS) + +On supported iPhone models, you can add haptics to your experience in the following ways: + + * Use standard UI components โ€” like [toggles](https://developer.apple.com/design/human-interface-guidelines/toggles), [sliders](https://developer.apple.com/design/human-interface-guidelines/sliders), and [pickers](https://developer.apple.com/design/human-interface-guidelines/pickers) โ€” that play Apple-designed system haptics by default. + + * When it makes sense, use a feedback generator to play one of several predefined haptic patterns in the categories of [notification](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Notification), [impact](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Impact), and [selection](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Selection) (for developer guidance, see [`UIFeedbackGenerator`](https://developer.apple.com/documentation/UIKit/UIFeedbackGenerator)). + + + + +#### [Notification](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Notification) + +Notification haptics provide feedback about the outcome of a task or action, such as depositing a check or unlocking a vehicle. + +Video with custom controls. + +Content description: An animation that represents a series of two haptic pulses of various durations and strengths by showing bars of different sizes and playing audio tones of different pitches. This particular pattern represents a success. + +Play + +**Success.** Indicates that a task or action has completed. + +Video with custom controls. + +Content description: An animation that represents a series of two haptic pulses of various durations and strengths by showing bars of different sizes and playing audio tones of different pitches. This particular pattern represents a warning. + +Play + +**Warning.** Indicates that a task or action has produced a warning of some kind. + +Video with custom controls. + +Content description: An animation that represents a series of four haptic pulses of various durations and strengths by showing bars of different sizes and playing audio tones of different pitches. This particular pattern represents an error. + +Play + +**Error.** Indicates that an error has occurred. + +#### [Impact](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Impact) + +Impact haptics provide a physical metaphor you can use to complement a visual experience. For example, people might feel a tap when a view snaps into place or a thud when two heavy objects collide. + +Video with custom controls. + +Content description: An animation that represents a single haptic pulse of a specific duration and strength by showing a bar of a specific size and playing an audio tone of a specific pitch. This particular pattern represents a light impact. + +Play + +**Light.** Indicates a collision between small or lightweight UI objects. + +Video with custom controls. + +Content description: An animation that represents a single haptic pulse of a specific duration and strength by showing a bar of a specific size and playing an audio tone of a specific pitch. This particular pattern represents a medium impact. + +Play + +**Medium.** Indicates a collision between medium-sized or medium-weight UI objects. + +Video with custom controls. + +Content description: An animation that represents a single haptic pulse of a specific duration and strength by showing a bar of a specific size and playing an audio tone of a specific pitch. This particular pattern represents a heavy impact. + +Play + +**Heavy.** Indicates a collision between large or heavyweight UI objects. + +Video with custom controls. + +Content description: An animation that represents a single haptic pulse of a specific duration and strength by showing a bar of a specific size and playing an audio tone of a specific pitch. This particular pattern represents a rigid impact. + +Play + +**Rigid.** Indicates a collision between hard or inflexible UI objects. + +Video with custom controls. + +Content description: An animation that represents a single haptic pulse of a specific duration and strength by showing a bar of a specific size and playing an audio tone of a specific pitch. This particular pattern represents a soft impact. + +Play + +**Soft.** Indicates a collision between soft or flexible UI objects. + +#### [Selection](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Selection) + +Selection haptics provide feedback while the values of a UI element are changing. + +Video with custom controls. + +Content description: An animation that represents a single haptic pulse of a specific duration and strength by showing a bar of a specific size and playing an audio tone of a specific pitch. This particular pattern represents a selection. + +Play + +**Selection.** Indicates that a UI elementโ€™s values are changing. + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#macOS) + +When a Magic Trackpad is available, your app can provide one of the three following haptic patterns in response to a drag operation or force click. + +Haptic feedback pattern| Description +---|--- +Alignment| Indicates the alignment of a dragged item. For example, this pattern could be used in a drawing app when the people drag a shape into alignment with another shape. Other scenarios where this type of feedback could be used might include scaling an object to fit within specific dimensions, positioning an object at a preferred location, or reaching the beginning/end or minimum/maximum of something like a scrubber in a video app. +Level change| Indicates movement between discrete levels of pressure. For example, as people press a fast-forward button on a video player, playback could increase or decrease and haptic feedback could be provided as different levels of pressure are reached. +Generic| Intended for providing general feedback when the other patterns donโ€™t apply. + +For developer guidance, see [`NSHapticFeedbackPerformer`](https://developer.apple.com/documentation/AppKit/NSHapticFeedbackPerformer). + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#watchOS) + +Apple Watch Series 4 and later provides haptic feedback for the Digital Crown, which gives people a more tactile experience as they scroll through content. By default, the system provides linear haptic detents that people can feel as they rotate the Digital Crown. Some system controls, like table views, provide detents as new items scroll onto the screen. For developer guidance, see [`WKHapticType`](https://developer.apple.com/documentation/WatchKit/WKHapticType). + +watchOS defines the following set of haptics, each of which conveys a specific meaning to people. + + * Notification + * Up + * Down + * Success + * Failure + * Retry + * Start + * Stop + * Click + + + +Video with custom controls. + +Content description: An animation that represents an arrangement of haptic pulses of various durations and strengths by showing a set of thin vertical lines that symbolize sound waves. + +Play + +**Notification.** Tells the person that something significant or out of the ordinary has happened and requires their attention. The system plays this same haptic when a local or remote notification arrives. + +Video with custom controls. + +Content description: An animation that represents an arrangement of haptic pulses of various durations and strengths by showing a set of thin vertical lines that symbolize sound waves. + +Play + +**Up.** Tells the person that an important value increased above a significant threshold. + +Video with custom controls. + +Content description: An animation that represents an arrangement of haptic pulses of various durations and strengths by showing a set of thin vertical lines that symbolize sound waves. + +Play + +**Down.** Tells the person that an important value decreased below a significant threshold. + +Video with custom controls. + +Content description: An animation that represents an arrangement of haptic pulses of various durations and strengths by showing a set of thin vertical lines that symbolize sound waves. + +Play + +**Success.** Tells the person that an action completed successfully. + +Video with custom controls. + +Content description: An animation that represents an arrangement of haptic pulses of various durations and strengths by showing a set of thin vertical lines that symbolize sound waves. + +Play + +**Failure.** Tells the person that an action failed. + +Video with custom controls. + +Content description: An animation that represents an arrangement of haptic pulses of various durations and strengths by showing a set of thin vertical lines that symbolize sound waves. + +Play + +**Retry.** Tells the person that an action failed but they can retry it. + +Video with custom controls. + +Content description: An animation that represents an arrangement of haptic pulses of various durations and strengths by showing a set of thin vertical lines that symbolize sound waves. + +Play + +**Start.** Tells the person that an activity started. Use this haptic when starting a timer or any other activity that a person can explicitly start and stop. The stop haptic usually follows this haptic. + +Video with custom controls. + +Content description: An animation that represents an arrangement of haptic pulses of various durations and strengths by showing a set of thin vertical lines that symbolize sound waves. + +Play + +**Stop.** Tells the person that an activity stopped. Use this haptic when stopping a timer or other activity that the person previously started. + +Video with custom controls. + +Content description: An animation that represents an arrangement of haptic pulses of various durations and strengths by showing a set of thin vertical lines that symbolize sound waves. + +Play + +**Click.** Provides the sensation of a dial clicking, helping you communicate progress at predefined increments or intervals. Overusing the click haptic tends to diminish its utility and can even be confusing when clicks overlap each other. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Related) + +[Feedback](https://developer.apple.com/design/human-interface-guidelines/feedback) + +[Gestures](https://developer.apple.com/design/human-interface-guidelines/gestures) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Developer-documentation) + +[Core Haptics](https://developer.apple.com/documentation/CoreHaptics) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/100FA6DD-1A48-485A-AFC2-47FDB92376D3/5210_wide_250x141_1x.jpg) Practice audio haptic design ](https://developer.apple.com/videos/play/wwdc2021/10278) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/48/9EEAE751-B5EE-4934-8F3A-38361FBA05DE/3277_wide_250x141_1x.jpg) Introducing Core Haptics ](https://developer.apple.com/videos/play/wwdc2019/520) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Change-log) + +Date| Changes +---|--- +May 7, 2024| Added guidance for playing haptics on Apple Pencil Pro. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/playing-video.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/playing-video.md new file mode 100644 index 00000000..5d06e204 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/playing-video.md @@ -0,0 +1,180 @@ +--- +title: "Playing video | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/playing-video + +# Playing video + +People expect to enjoy rich video experiences on their devices, regardless of the app or game theyโ€™re using. + +![A sketch of a play button, suggesting video playback. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/26e9c68a8a956a2f5ed6b1d9988c0dce/patterns-playing-video-intro%402x.png) + +The system provides video players designed for you to use to embed playback experiences within your app or game in iOS, iPadOS, macOS, tvOS, and visionOS. You can also offer your content through the TV app in these platforms, which gives people a convenient and consistent viewing experience. + +The system-provided video players support different aspect-ratio playback modes and in most platforms, Picture in Picture (PiP) viewing mode. Although people can switch modes during playback, by default, the system selects one of the following playback modes based on a videoโ€™s aspect ratio: + + * In full-screen โ€” or _aspect-fill_ โ€” mode, the video scales to fill the display, and some edge cropping may occur. This mode is the default for wide video (2:1 through 2.40:1). For developer guidance, see [`resizeAspectFill`](https://developer.apple.com/documentation/AVFoundation/AVLayerVideoGravity/resizeAspectFill). + + * In fit-to-screen โ€” or _aspect_ โ€” mode, the entire video is visible onscreen, and letterboxing or pillarboxing occurs as needed. This mode is the default for standard video (4:3, 16:9, and anything up to 2:1) and ultrawide video (anything above 2.40:1). For developer guidance, see [`resizeAspect`](https://developer.apple.com/documentation/AVFoundation/AVLayerVideoGravity/resizeAspect). + + + + +In visionOS and tvOS, the built-in video player also provides _transport controls,_ which let people perform playback tasks, like turning on subtitles or changing the audio language, and actions, like adding a show to a library or favoriting a clip. Below the transport controls, the video player displays _content tabs_ , like Info, Episodes, or Chapters, that can provide supporting information and help streamline navigation. In visionOS, the transport controls appear as an [ornament](https://developer.apple.com/design/human-interface-guidelines/ornaments). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/playing-video#Best-practices) + +**Use the system video player to give people a familiar and convenient experience.** The built-in video player provides an exceptional video playback experience that offers consistent interactions and behaviors that let people concentrate on enjoying immersive content. If your app truly requires a custom video player, reference the behavior and interface of the system video player to help you provide an experience that people can instantly understand. A custom experience that diverges slightly from the system-provided experience can cause frustration because people donโ€™t know which of their habitual interactions they can continue to use. + +**Always display video content at its original aspect ratio.** When video content uses embedded letterbox or pillarbox padding to conform to a specific aspect ratio, the system may be unable to correctly scale the video based on the current playback mode. Padding embedded within the video frame can cause videos to appear smaller in both full-screen and fit-to-screen modes. It also prevents videos from displaying correctly in edge-to-edge, non-full-screen contexts, like Picture in Picture mode on iPad. + +Here are some examples that show how padding can affect video display on iPhone Xs. + + * Result of padding a 4:3 video + * Result of padding a 21:9 video + + + +![An illustration of iPhone in landscape orientation. A blue rectangle shows the AVKit safe area within the screen. Overlaying the safe area and extending beyond it on all sides is a purple rectangle that represents the 4:3 video area, which doesn't include any embedded padding.](https://docs-assets.developer.apple.com/published/d0ed256c73ed2b5b8cba4baf20d437e1/video-fill-4-3-right%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png)4:3 video in full-screen viewing mode + +![An illustration of iPhone in landscape orientation. A blue rectangle shows the AVKit safe area within the screen. Overlaying the safe area and extending beyond its top and bottom edges is a purple rectangle that represents the 4:3 video area. Attached to the left and right edges of the purple rectangle are two vertical pink rectangles that extend to the left and right device edges, representing embedded pillarboxes.](https://docs-assets.developer.apple.com/published/f6c10e1815aabff99ace1a053ba75757/video-fill-4-3-wrong%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png)4:3 video with embedded padding, in full-screen viewing mode + +![A legend for the illustrations, identifying blue as the AVKit safe area, purple as the video area, and pink as the embedded padding.](https://docs-assets.developer.apple.com/published/fc703118ebdf69f3c70d70ffce64727e/legend-letter-pillar%402x.png) + +![An illustration of iPhone in landscape orientation. A blue rectangle shows the AVKit safe area within the screen. Overlaying the safe area and extending to the top and bottom edges of the device is a purple rectangle representing the 21:9 video area, which doesn't include any embedded padding.](https://docs-assets.developer.apple.com/published/bc4f68b42fb171bb35e19393dbe741f1/video-fit-21-9-right%402x.png) + +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png)21:9 video in fit-to-screen viewing mode + +![An illustration of iPhone in landscape orientation. A blue rectangle shows the AVKit safe area within the screen. Overlaying the safe area is a purple rectangle representing the 21:9 video area plus two horizontal pink rectangles attached to its top and bottom edges, representing embedded letterboxes. The area that includes the video and the letterboxes extends to the top and bottom edges of the device, but doesn't extend to the left and right edges of the safe area.](https://docs-assets.developer.apple.com/published/ce74b05ac601afa8404fd40db9cc8380/video-fit-21-9-wrong%402x.png) + +![An X in a circle to indicate incorrect usage.](https://docs-assets.developer.apple.com/published/209f6f0fc8ad99d9bf59e12d82d06584/crossout%402x.png)21:9 video with embedded padding, in fit-to-screen viewing mode + +![A legend for the illustrations, identifying blue as the AVKit safe area, purple as the video area, and pink as the embedded padding.](https://docs-assets.developer.apple.com/published/fc703118ebdf69f3c70d70ffce64727e/legend-letter-pillar%402x.png) + +**Provide additional information when it adds value.** In iOS, iPadOS, tvOS, and visionOS, you can customize a videoโ€™s additional information by providing an image, title, description, and other useful information. In general, restrict this content so that it doesnโ€™t obscure media playback. For developer guidance, see [`externalMetadata`](https://developer.apple.com/documentation/AVFoundation/AVPlayerItem/externalMetadata). + +**Support the interactions people expect, regardless of the input device theyโ€™re using to control playback.** For example, people expect to press Space on a connected keyboard to play or pause media playback on Apple Vision Pro, Mac, iPhone, iPad, and Apple TV. Similarly, people expect to move through their media on Apple TV by making familiar, intuitive gestures with the Siri Remote. For guidance, see Keyboards and Remotes. + +**If people need to access playback options or content-specific information in your tvOS app, consider adding a transport control or a custom content tab.** People typically open a transport control or content tab while theyโ€™re watching a video, so itโ€™s essential to provide only the most useful actions and information. Help people return quickly to the viewing experience by making sure your actions donโ€™t take more than a step or two and your content is succinct. Use a transport control to support a playback-related action like favoriting a video; use custom content tabs to display supplementary information or recommendations. + +**Avoid allowing audio from different sources to mix as viewers switch between modes.** Mixed audio is an unpleasant and frustrating user experience. In general, audio mixes when at least one of the audio sources fails to handle secondary audio correctly. Here is a typical scenario: While watching a full-screen video, the viewer moves it into the PiP window, where the system automatically mutes the video. In the full-screen window, the viewer starts a game that plays background music, then switches to the PiP window and unmutes the video. If the game doesnโ€™t handle secondary audio appropriately, its audio mixes with the audio from the unmuted video. For developer guidance, see [`silenceSecondaryAudioHintNotification`](https://developer.apple.com/documentation/AVFAudio/AVAudioSession/silenceSecondaryAudioHintNotification). + +## [Integrating with the TV app](https://developer.apple.com/design/human-interface-guidelines/playing-video#Integrating-with-the-TV-app) + +The TV app provides global access to favorite, recently played, and recommended video content from across the system. When people initiate content playback within your app, the TV app automatically opens your app and transitions to it. Follow these guidelines to help the TV app experience feel like an integrated part of your app. + +**Ensure a smooth transition to your app.** The TV app fades to black when transitioning to your app and doesnโ€™t show your appโ€™s launch screen. Maintain visual continuity with this transition by immediately presenting your own black screen before starting to play or resume content. + +**Show the expected content immediately.** People expect the content they choose to begin playing as soon as the transition to your app completes, especially when resuming playback. Jump right from your appโ€™s black screen into content, and avoid displaying splash screens, detail screens, intro animations, or any other barriers that make it take longer to reach content. In rare situations where you must display an interstitial element before the selected media plays, people can choose Select to step through the element, or choose Play if they want to skip the interstitial content and start playback. + +**Avoid asking people if they want to resume playback.** If playback can be resumed, do so automatically without prompting for confirmation. + +**Play or pause playback when people press Space on a connected Bluetooth keyboard.** Pressing Space to control media playback is an interaction people expect, regardless of the keyboard theyโ€™re using. + +**Make sure content plays for the correct viewer.** If your app supports multiple user profiles, the TV app can specify a profile when issuing a playback request. Make your app automatically switch to this profile before starting playback. If a playback request doesnโ€™t specify a profile, ask the viewer to choose one before playback begins so this information is available in the future. + +**Use the previous end time when resuming playback of a long video clip.** Resuming playback at the previous stopping point lets people quickly continue where they left off. + +### [Loading content](https://developer.apple.com/design/human-interface-guidelines/playing-video#Loading-content) + +**Avoid displaying loading screens when possible.** A loading screen is unnecessary if your content loads quickly, but if loading takes more than two seconds, consider showing a black loading screen with a centered activity spinner and no surrounding content. + +**Start playback immediately.** If you must display a loading screen, display it only until enough content loads for playback to begin. Continue loading remaining content in the background. + +**Minimize loading screen content.** If you include branding or images on your loading screen, do so minimally while maintaining the black background that helps provide a seamless transition to playback. + +### [Exiting playback](https://developer.apple.com/design/human-interface-guidelines/playing-video#Exiting-playback) + +After exiting playback, people remain in your app rather than returning to the TV app, so itโ€™s a good idea to help them avoid becoming disoriented. + +**Show a contextually relevant screen.** When exiting playback, display a detail view for the content the viewer was just watching and include an option to resume playback. If a detail view isnโ€™t available, show either a menu that lists this content or your appโ€™s main menu. + +**Be prepared for an immediate exit.** Prepare an exit view as soon as possible after receiving a playback notification so youโ€™re ready to display the view if people exit immediately after playback begins. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/playing-video#Platform-considerations) + + _No additional considerations for iOS, iPadOS, or macOS._ + +### [tvOS](https://developer.apple.com/design/human-interface-guidelines/playing-video#tvOS) + +**Defer to content when displaying logos or noninteractive overlays above video.** A small, unobtrusive logo or countdown timer may be appropriate for your video, but avoid large, distracting overlays that donโ€™t enhance the viewing experience. Also, be aware that some devices are prone to image retention, so itโ€™s generally better to keep overlays short and to prefer translucent graphics in Standard Dynamic Range (SDR) to bright, opaque content. + +**Show interactive overlays gracefully.** Some videos display interactive overlays, such as quizzes, surveys, and progress check-ins. For the best user experience, implement a minimum delay of 0.5 seconds to pause playing media, and display an interactive overlay. Give people a clear way to dismiss the overlay and resume media playback after they finish interacting. + +### [visionOS](https://developer.apple.com/design/human-interface-guidelines/playing-video#visionOS) + +**Help people stay comfortable when playing video in your app.** Often, an app doesnโ€™t control the content in the videos it plays, but you can help people stay comfortable by: + + * Letting them choose when to start playing a video + + * Using a small window for playback, letting people resize it if they want + + * Making sure people can see their surroundings during playback + + + + +**In a fully immersive experience, avoid letting virtual content obscure playback or transport controls.** In a fully immersive context, the system automatically places the video player at a predictable location that provides an optimal viewing experience. Use this location to help make sure that no virtual content occludes the default playback or transport controls in the ornament near the bottom of the player. + +**Avoid automatically starting a fully immersive video playback experience.** People need control over their experience and theyโ€™re unlikely to appreciate being launched into a fully immersive video without warning. + +**Create a thumbnail track if you want to support scrubbing.** The system displays thumbnails as people scrub to different times in the video, helping them choose the section they want. To improve performance, supply a set of thumbnails that each measure 160 px in width. For developer guidance, see [HTTP Live Streaming (HLS) Authoring Specification for Apple Devices > Trick Play](https://developer.apple.com/documentation/http-live-streaming/hls-authoring-specification-for-apple-devices#Trick-Play). + +**Avoid expanding an inline video player to fill a window.** When you display the system-provided player view in a window, playback controls appear in the same plane as the player view and not in an ornament that floats above the window. Inline video needs to be 2D and you want to make sure that window content remains visible around the player so people donโ€™t expect a more immersive playback experience. For developer guidance, see [`AVPlayerViewController`](https://developer.apple.com/documentation/AVKit/AVPlayerViewController). + +**Use a RealityKit video player if you need to play video in a view like a splash screen or a transitional view.** In situations like these, people generally expect the video to lead into the next experience, so they donโ€™t need playback controls or system-provided integration, like dimming and view anchoring. The RealityKit video player automatically uses the correct aspect ratio for both 2D and 3D video and supports closed captions. RealityKit can also help you play video as a special effect on the surface of a custom view or object. For developer guidance, see [RealityKit](https://developer.apple.com/documentation/RealityKit). + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/playing-video#watchOS) + +In watchOS, the system manages video playback. Apps can play short video clips while the app is active and running in the foreground. You can use a movie element to embed clips in your interface and play video inline, or you can play a clip in a separate interface. For developer guidance, see [`VideoPlayer`](https://developer.apple.com/documentation/AVKit/VideoPlayer). + +**Keep video clips short.** Prefer shorter clips of no longer than 30 seconds. Long clips consume more disk space and require people to keep their wrists raised for longer periods of time, which can cause fatigue. + +**Use the recommended sizes and encoding values for media assets.** In particular, avoid scaling video clips, which affects performance and results in a suboptimal appearance. The following table lists the recommended encoding and resolution values for video assets. The audio encoding values apply to both movies and audio-only assets. + +Attribute| Value +---|--- +Video codec| H.264 High Profile +Video bit rate| 160 kbps at up to 30 fps +Resolution (full screen)| 208x260 px (portrait orientation) +Resolution (16:9)| 320x180 px (landscape orientation) +Audio| 64 kbps HE-AAC + +**Avoid creating a poster image that looks like a system control.** You want people to understand that they can tap a movie element for playback; you donโ€™t want to confuse people by making movie elements look like something else. + +**Consider creating a poster image that represents a video clipโ€™s contents.** When people tap a poster image, the system replaces the image with the video and begins inline playback. A relevant poster image can help people make an informed decision about whether to view the video. In general, avoid creating a poster image that has nothing to do with the content or that people might mistake for a control. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/playing-video#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/playing-video#Related) + +[Playing audio](https://developer.apple.com/design/human-interface-guidelines/playing-audio) + +[Feedback](https://developer.apple.com/design/human-interface-guidelines/feedback) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/playing-video#Developer-documentation) + +[Configuring your app for media playback](https://developer.apple.com/documentation/AVFoundation/configuring-your-app-for-media-playback) โ€” AVFoundation + +[AVKit](https://developer.apple.com/documentation/AVKit) + +[HTTP Live Streaming](https://developer.apple.com/streaming/) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/playing-video#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/124/344FB830-5184-4C63-8D0C-D8861D31A70D/6645_wide_250x141_1x.jpg) Create a great video playback experience ](https://developer.apple.com/videos/play/wwdc2022/10147) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/3F1BA6AB-BC74-4A1A-A440-90BA4AE5A23D/10042_wide_250x141_1x.jpg) Explore video experiences for visionOS ](https://developer.apple.com/videos/play/wwdc2025/304) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/53F0161E-DB14-4A7D-8A94-B76244201AB8/5102_wide_250x141_1x.jpg) Deliver a great playback experience on tvOS ](https://developer.apple.com/videos/play/wwdc2021/10191) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/playing-video#Change-log) + +Date| Changes +---|--- +September 12, 2023| Corrected the recommended width for a thumbnail in visionOS. +June 21, 2023| Updated to include guidance for visionOS. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/printing.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/printing.md new file mode 100644 index 00000000..32d743fb --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/printing.md @@ -0,0 +1,50 @@ +--- +title: "Printing | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/printing + +# Printing + +An iOS, iPadOS, macOS, or visionOS app can integrate system-provided print functionality when it makes sense, presenting custom printer- and document-specific options if necessary. + +![A sketch of a printer, suggesting printing. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/1194b45798f16b503c0a91c6d8ed3ecc/patterns-printing-intro%402x.png) + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/printing#Best-practices) + +**Make printing discoverable.** Help people find your print action by placing it in standard system locations. For example, include a Print item in your macOS appโ€™s File menu; in your iOS or iPadOS app, add a toolbar button that opens an [action sheet](https://developer.apple.com/design/human-interface-guidelines/action-sheets). If your macOS app has a toolbar, you might want to put a Print button there, too, but consider making it an optional button that people can add when they customize the toolbar. + +**Present a printing option only when itโ€™s possible.** If thereโ€™s nothing onscreen to print, or no printers are available, dim the Print item in a macOS appโ€™s File menu and remove the Print action from the Action sheet in an iOS or iPadOS app. If you implement a custom print button, dim or hide it when printing isnโ€™t possible. + +**Present relevant printing options.** If it makes sense to offer options like selecting a page range, requesting multiple copies, or printing on both sides โ€” and the printer supports the options โ€” use the system-provided view to present them. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/printing#Platform-considerations) + + _No additional considerations for iOS, iPadOS, or visionOS. Not supported in tvOS or watchOS._ + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/printing#macOS) + +**If your macOS app offers app-specific print options that the system doesnโ€™t offer, consider creating a custom category for the print panel.** By default, the print panel offers several categories of settings, such as Layout, Paper Handling, and Media & Quality. Give your custom category a unique name, such as your app name, and include options that help people have a great print experience in your app. For example, Keynote offers presentation-specific options, like the ability to print presenter notes, slide backgrounds, and skipped slides. + +**If your app supports document-specific page settings, consider presenting a page setup dialog.** A _page setup dialog_ includes rarely changed settings for page size, orientation, and scaling that apply to printing a particular document. If this makes sense in your app, avoid implementing features the system already provides. For example, you donโ€™t need to include options like changing the page orientation or printing in reverse order because the system implements these options. + +**Make sure interdependencies between options are clear.** For example, if double-sided printing is available, an option to print on transparencies becomes unavailable. + +**Separate advanced features from frequently used features.** Consider using a disclosure control to hide advanced options until theyโ€™re needed. Label advanced options as _Advanced Options_. + +**Consider letting people preview the effect of a setting.** For example, you could update a thumbnail image to show the effect of changing a tone control. + +**Consider storing modified settings with the document.** At minimum, it makes sense to store print settings until the document is closed in case people want to print it again. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/printing#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/printing#Related) + +[File management](https://developer.apple.com/design/human-interface-guidelines/file-management) + +[File menu](https://developer.apple.com/design/human-interface-guidelines/the-menu-bar#File-menu) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/printing#Developer-documentation) + +[`UIPrintInteractionController`](https://developer.apple.com/documentation/UIKit/UIPrintInteractionController) โ€” UIKit + +[`NSDocument`](https://developer.apple.com/documentation/AppKit/NSDocument) โ€” AppKit + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/ratings-and-reviews.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/ratings-and-reviews.md new file mode 100644 index 00000000..9878415c --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/ratings-and-reviews.md @@ -0,0 +1,48 @@ +--- +title: "Ratings and reviews | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/ratings-and-reviews + +# Ratings and reviews + +People often view the ratings and reviews for an app or game before they download it. + +![A sketch of a half-filled star, suggesting a favorability rating. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/1d5f982affa4088005ecd7c8d6edddff/patterns-ratings-and-reviews-intro%402x.png) + +Delivering a great overall experience is the best way to encourage positive ratings and reviews, but itโ€™s also crucial to choose the right time to ask people for feedback. Although every app is different, some possible ways to do this involve looking at how many times or how frequently people launch your app, the number of features someone explores, or the number of tasks they complete. + +People can always rate your app within the App Store. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/ratings-and-reviews#Best-practices) + +**Ask for a rating only after people have demonstrated engagement with your app or game.** For example, you might prompt people when they complete a game level or a significant task. Avoid asking for a rating on first launch or during onboarding, because people havenโ€™t had enough time to gain a clear understanding of your appโ€™s value or form an opinion. People may even be more likely to leave negative feedback if they feel an app is asking for a rating before they get a chance to use it. + +![An illustration of the UI that appears when a person is prompted to rate an app or game in macOS.](https://docs-assets.developer.apple.com/published/b0aee2c246e2cfd035ba21ed1a2004c3/ratings-and-reviews-ios-alert%402x.png) + +**Avoid interrupting people while theyโ€™re performing a task or playing a game.** Asking for feedback can disrupt the user experience and feel like a burden. Look for natural breaks or stopping points in your app or game where a rating request is less likely to be bothersome. + +**Avoid pestering people.** Repeated rating requests can be irritating, and may even negatively influence peopleโ€™s opinion of your app. Consider allowing at least a week or two between requests, prompting again after people demonstrate additional engagement with your experience. + +**Prefer the system-provided prompt.** iOS, iPadOS, and macOS offer a consistent, nonintrusive way for apps and games to request ratings and reviews. When you identify places in your experience where it makes sense to ask for feedback, the system checks for previous feedback and โ€” if there isnโ€™t any โ€” displays an in-app prompt that asks for a rating and an optional written review. People can supply feedback or dismiss the prompt with a single tap or click; they can also opt out of receiving these prompts for all apps they have installed. The system automatically limits the display of the prompt to three occurrences per app within a 365-day period. For developer guidance, see [`RequestReviewAction`](https://developer.apple.com/documentation/StoreKit/RequestReviewAction). + +**Weigh the benefits of resetting your summary rating against the potential disadvantage of showing fewer ratings.** When you release a new version of your app or game, you can reset the summary of individual ratings you received since the last reset. Although resetting means that the ratings reflect the current version, it also tends to result in having fewer ratings overall, which can discourage some people from downloading your app. For developer guidance, see [Reset app summary rating](https://help.apple.com/app-store-connect/#/devfb7e87af8). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/ratings-and-reviews#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/ratings-and-reviews#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/ratings-and-reviews#Related) + +[Ratings, reviews, and responses](https://developer.apple.com/app-store/ratings-and-reviews/) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/ratings-and-reviews#Developer-documentation) + +[`RequestReviewAction`](https://developer.apple.com/documentation/StoreKit/RequestReviewAction) โ€” StoreKit + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/ratings-and-reviews#Change-log) + +Date| Changes +---|--- +September 12, 2023| Added artwork. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/searching.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/searching.md new file mode 100644 index 00000000..7e9cfadd --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/searching.md @@ -0,0 +1,70 @@ +--- +title: "Searching | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/searching + +# Searching + +People use various search techniques to find content on their device, within an app, and within a document or file. + +![A sketch of a magnifying glass, suggesting the search for information. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/d80bc4d95013730b824dc7956f912b4d/patterns-searching-intro%402x.png) + +To search for content within an app, people generally expect to use a [search field](https://developer.apple.com/design/human-interface-guidelines/search-fields). When it makes sense, you can personalize the search experience by using what you know about how people interact with your app. For example, you might display recent searches, search suggestions, completions, or corrections based on terms people searched earlier in your app. + +In some cases, people appreciate the ability to scope a search or filter the results. For example, people might want to search for items by specifying attributes like creation date, file size, or file type. For guidance, see [Scope controls and tokens](https://developer.apple.com/design/human-interface-guidelines/search-fields#Scope-controls-and-tokens). You can also help people find content within an open document or file by implementing ways to find content in a window or page in your iOS, iPadOS, or macOS app. + +In iOS, iPadOS, and macOS, Spotlight helps people find content across all apps in the system and on the web. When you index and provide information about your appโ€™s content, people can use Spotlight to find content your app contains without opening it first. For guidance, see [Systemwide search](https://developer.apple.com/design/human-interface-guidelines/searching#Systemwide-search). + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/searching#Best-practices) + +**If search is important, consider making it a primary action.** For example, in the Apple TV, Photos, and Phone apps in iOS, search occupies a distinct tab in the [tab bar](https://developer.apple.com/design/human-interface-guidelines/tab-bars). In the Notes app, a search field is in the [toolbar](https://developer.apple.com/design/human-interface-guidelines/toolbars), making search clearly visible and easily accessible. + +**Aim to make your appโ€™s content searchable through a single location.** People appreciate having one clearly identified location they can use to find anything in your app that they are looking for. For apps with clearly distinct sections, it may still be useful to offer a local search. For example, search acts as a filter on the current view when searching your Recents and Contacts in the iOS Phone app. + +**Use placeholder text to indicate what content is searchable.** For example, the Apple TV app includes the placeholder text _Shows, Movies, and More_. + +**Clearly display the current scope of a search.** Use a descriptive placeholder text, a [scope control](https://developer.apple.com/design/human-interface-guidelines/search-fields#Scope-controls-and-tokens), or a title to help reinforce what someone is currently searching. For example, in the Mail app there is always a clear reference to the mailbox someone is searching. + +**Provide suggestions to make searching easier.** When you display a personสผs recent searches or offer search suggestions both before and while theyโ€™re typing, you can help people search faster and type less. For developer guidance, see [`searchSuggestions(_:)`](https://developer.apple.com/documentation/SwiftUI/View/searchSuggestions\(_:\)). + +**Take privacy into consideration before displaying search history.** People might not appreciate having their search history appear where others might see it. Depending on the context, consider providing other ways to narrow the search instead. If you do show search history, provide a way for people to clear it if they want. + +## [Systemwide search](https://developer.apple.com/design/human-interface-guidelines/searching#Systemwide-search) + +**Make your appโ€™s content searchable in Spotlight.** You can share content with Spotlight by making it indexable and specifying descriptive attributes known as _metadata_. Spotlight extracts, stores, and organizes this information to allow for fast, comprehensive searches. + +**Define metadata for custom file types you handle.** Supply a Spotlight File Importer plug-in that describes the types of metadata your file format contains. For developer guidance, see [`CSImportExtension`](https://developer.apple.com/documentation/CoreSpotlight/CSImportExtension). + +**Use Spotlight to offer advanced file-search capabilities within the context of your app.** For example, you might include a button that instantly initiates a Spotlight search based on the current selection. You might then display a custom view that presents the search results or a filtered subset of them. + +**Prefer using the system-provided open and save views.** The system-provided open and save views generally include a built-in search field that people can use to search and filter the entire system. For related guidance, see [File management](https://developer.apple.com/design/human-interface-guidelines/file-management). + +**Implement a Quick Look generator if your app produces custom file types.** A Quick Look generator helps Spotlight and other apps show previews of your documents. For developer guidance, see [Quick Look](https://developer.apple.com/documentation/QuickLook). + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/searching#Platform-considerations) + + _No additional considerations for iOS, iPadOS, macOS, tvOS, visionOS, or watchOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/searching#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/searching#Related) + +[Search fields](https://developer.apple.com/design/human-interface-guidelines/search-fields) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/searching#Developer-documentation) + +[Adding your appโ€™s content to Spotlight indexes](https://developer.apple.com/documentation/CoreSpotlight/adding-your-app-s-content-to-spotlight-indexes) โ€” Core Spotlight + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/searching#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/50ADDE23-F013-4993-8B1D-09368B4BD5F4/9259_wide_250x141_1x.jpg) Support semantic search with Core Spotlight ](https://developer.apple.com/videos/play/wwdc2024/10131) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/124/6E076CE0-7DDF-4471-B6F0-005ADF9C7960/6500_wide_250x141_1x.jpg) Whatโ€™s new in iPad app design ](https://developer.apple.com/videos/play/wwdc2022/10009) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/D45C244B-2038-4692-99A0-6131ED5FD984/5084_wide_250x141_1x.jpg) Craft search experiences in SwiftUI ](https://developer.apple.com/videos/play/wwdc2021/10176) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/searching#Change-log) + +Date| Changes +---|--- +June 9, 2025| Updated best practices with general guidance from Search fields, and reorganized guidance for systemwide search. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/settings.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/settings.md new file mode 100644 index 00000000..b77c06eb --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/settings.md @@ -0,0 +1,84 @@ +--- +title: "Settings | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/settings + +# Settings + +People expect apps and games to just work, but they also appreciate having ways to customize the experience to fit their needs. + +![A sketch of a gear, suggesting configuration. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/38706162753c93aa16bfa6295987c33a/patterns-settings-intro%402x.png) + +On all Apple platforms, the system-provided Settings app lets people adjust things like the overall appearance of the system, network connections, account details, accessibility requirements, and language and region settings. On some platforms, the system-provided Settings app can also include settings for specific apps and games, often letting people adjust whether the app or game can access location information, use device features like microphone or camera, and integrate with system features like notifications, Siri, or Search. + +When necessary, you can provide a custom settings area within your app or game to offer general settings that affect your overall experience, like interface style or game-saving behavior. If you need to offer settings that affect only a specific task, you can provide these options within the task itself, so people donโ€™t have to leave the experience to customize it. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/settings#Best-practices) + +**Aim to provide default settings that give the best experience to the largest number of people.** For example, you can automatically maximize performance for the device your game is running on instead of asking players to make this choice after your game launches (for developer guidance, see [Improving your gameโ€™s graphics performance and settings](https://developer.apple.com/documentation/Metal/improving-your-games-graphics-performance-and-settings)). When you choose appropriate default settings, people may not have to make any adjustments before they can start enjoying your app or game. + +**Minimize the number of settings you offer.** Although people appreciate having control over an app or game, too many settings can make the experience feel less approachable, while also making it hard to find a particular setting. + +**Make settings available in ways people expect.** For example, when a physical keyboard is connected, people often use the standard Command-Comma (,) keyboard shortcut to open an appโ€™s settings, whereas in a game, players often use the Esc (Escape) key. + +**Avoid using settings to ask for setup information you can get in other ways.** For example, a game can automatically detect a connected controller or accessory instead of asking the player to identify it; an app can detect whether people are currently using Dark Mode. + +**Respect peopleโ€™s systemwide settings and avoid including redundant versions of them in your custom settings area.** People expect to use the system-provided Settings app to manage global options like accessibility accommodations, scrolling behavior, and authentication methods, and they expect all apps and games to adhere to their choices. Including custom versions of global options in your settings area is likely to confuse people because it implies that systemwide settings may not apply to your app or game and that changing your custom version of a global setting may affect other apps and games, too. + +## [General settings](https://developer.apple.com/design/human-interface-guidelines/settings#General-settings) + +**Put general, infrequently changed settings in your custom settings area.** People must suspend what theyโ€™re doing to open an appโ€™s or gameโ€™s settings area, so you want to include options that people donโ€™t need to change all the time. For example, an app might list options for adjusting window configuration; a game might let players specify game-saving behavior or keyboard mappings; both apps and games might offer options related to peopleโ€™s accounts. + +## [Task-specific options](https://developer.apple.com/design/human-interface-guidelines/settings#Task-specific-options) + +**When possible, prefer letting people modify task-specific options without going to your settings area.** For example, if people can adjust things like showing or hiding parts of the current view, reordering a collection of items, or filtering a list, make these options available in the screens they affect, where theyโ€™re discoverable and convenient. Putting this type of option in a separate settings area disconnects it from its context, requiring people to suspend their task to make adjustments, and often hiding the results until people resume the task. + +Note + +In games, players tend to adjust their approach to a specific task as part of the gameplay, not as a settings option to change. + +## [System settings](https://developer.apple.com/design/human-interface-guidelines/settings#System-settings) + +**Add only the most rarely changed options to the system-provided Settings app.** If it makes sense to add your appโ€™s or gameโ€™s settings to the system-provided Settings app, consider providing a button that opens it directly from your interface. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/settings#Platform-considerations) + + _No additional considerations for iOS, iPadOS, tvOS, or visionOS._ + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/settings#macOS) + +When people choose the Settings item in your appโ€™s or gameโ€™s App menu, your custom settings window opens. Typically, a custom settings window contains a toolbar that includes buttons for switching between views โ€” called _panes_ โ€” that each contain a group of related settings. + +**Include a settings item in the[App menu](https://developer.apple.com/design/human-interface-guidelines/the-menu-bar#App-menu).** Avoid adding settings buttons to a windowโ€™s toolbar, because doing so decreases the space available for essential commands that people use frequently. If you provide document-level options, add this item to your appโ€™s [File menu](https://developer.apple.com/design/human-interface-guidelines/the-menu-bar#File-menu). + +**Dim a settings windowโ€™s minimize and maximize buttons.** Itโ€™s quick to open a custom settings window using the standard Commandโ€“Comma (,) keyboard command, so thereโ€™s no need to keep the window in the Dock, and because a settings window accommodates the size of the current pane, people donโ€™t need to expand the window to see more. + +**In your settings window, use a noncustomizable toolbar that remains visible and always indicates the active toolbar button.** A settings windowโ€™s toolbar identifies the areas people can customize and helps people navigate among those areas. People rely on a stable settings interface to help them find what they need. + +**Update the windowโ€™s title to reflect the currently visible pane.** If your settings window doesnโ€™t have multiple panes, use the title _App Name_ Settings. + +**Restore the most recently viewed pane.** People often adjust related settings more than once, so it can be convenient when a settings window opens to the last pane people used. + +### [watchOS](https://developer.apple.com/design/human-interface-guidelines/settings#watchOS) + +In watchOS, apps and games donโ€™t add custom settings to the system-provided Settings app. As an alternative, consider making a small number of essential options available at the bottom of the main view or letting people use a More menu to reconfigure objects. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/settings#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/settings#Related) + +[Onboarding](https://developer.apple.com/design/human-interface-guidelines/onboarding) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/settings#Developer-documentation) + +[`Settings`](https://developer.apple.com/documentation/SwiftUI/Settings) โ€” SwiftUI + +[`UserDefaults`](https://developer.apple.com/documentation/Foundation/UserDefaults) โ€” Foundation + +[Preference Panes](https://developer.apple.com/documentation/PreferencePanes) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/settings#Change-log) + +Date| Changes +---|--- +June 10, 2024| Reorganized some guidance into new topics and added game-specific examples. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/undo-and-redo.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/undo-and-redo.md new file mode 100644 index 00000000..16671563 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/undo-and-redo.md @@ -0,0 +1,58 @@ +--- +title: "Undo and redo | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/undo-and-redo + +# Undo and redo + +Undo and redo gives people easy ways to reverse many types of actions, which can also help people explore and experiment safely as they learn a new interface or task. + +![A sketch of an arrow that starts right, curves upward, and points to the left, suggesting a return to the start. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/768e64b5954af63fd6f4e9e4a3c5275a/patterns-undo-redo-intro%402x.png) + +People expect undo and redo to let them reverse their recent actions, so theyโ€™re likely to try undoing โ€” often multiple times โ€” until something changes. In a situation like this, people might not remember which of their previous actions an undo is targeting, which can lead to unintended changes and frustration. To help people remain in control, itโ€™s essential to help people predict the outcome of undoing and redoing and to highlight the results. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/undo-and-redo#Best-practices) + +**Help people predict the results of undo and redo as much as possible.** On iPhone, for example, you can describe the result in the alert that displays when people shake the device, giving them the option of performing the undo or canceling it. If you provide undo and redo menu items, you can modify the menu item labels to identify the result. For example, a document-based app might use menu item labels like Undo Typing or Redo Bold. + +**Show the results of an undo or redo.** Sometimes, the most recent action that people want to undo affects content or an area thatโ€™s no longer visible. In cases like this, itโ€™s crucial to highlight the result of each undo and redo to keep people from thinking that the action had no effect, which can lead them to perform it repeatedly. For example, if people undo after deleting a paragraph in a document area thatโ€™s no longer onscreen, you might scroll the document to show the restored paragraph. + +**Let people undo multiple times.** Avoid placing unnecessary limits on the number of times people can undo or redo. People generally expect to undo every action theyโ€™ve performed since taking a logical step like opening a document or saving their work. + +**Consider giving people the option to revert multiple changes at once.** In some scenarios, people might appreciate the ability to undo a batch of discrete but related actions โ€” like incremental adjustments to a single property or attribute โ€” so they donโ€™t have to undo each individual adjustment. In other cases, it can make sense to give people a convenient way to undo all the changes they made since opening a document or saving their work. + +**Provide undo and redo buttons only when necessary.** People generally expect to initiate undo and redo in system-supported ways, such as choosing the items in a macOS appโ€™s Edit menu, using keyboard shortcuts on a Mac or iPad, or shaking their iPhone. If itโ€™s important to provide dedicated undo and redo buttons in your app, use the standard system-provided symbols and put the buttons in a toolbar. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/undo-and-redo#Platform-considerations) + + _No additional considerations for visionOS. Not supported in tvOS or watchOS._ + +### [iOS, iPadOS](https://developer.apple.com/design/human-interface-guidelines/undo-and-redo#iOS-iPadOS) + +**Avoid redefining standard gestures for undo and redo.** For example, people can use a three-finger swipe to initiate an undo or redo, or shake their iPhone. As with all standard gestures, redefining them in your interface runs the risk of confusing people and making your experience unpredictable. + +**Briefly and precisely describe the operation to be undone or redone.** The undo and redo alert title automatically includes a prefix of โ€œUndo โ€ or โ€œRedo โ€ (including the trailing space). You need to provide an additional word or two that describes whatโ€™s being undone or redone, to appear after this prefix. For example, you might create alert titles such as โ€œUndo Nameโ€ or โ€œRedo Address Change.โ€ + +### [macOS](https://developer.apple.com/design/human-interface-guidelines/undo-and-redo#macOS) + +**Place undo and redo commands in the Edit menu and support the standard keyboard shortcuts.** Mac users expect to find undo and redo at the top of the Edit menu; they also expect to use Commandโ€“Z and Shiftโ€“Commandโ€“Z to perform undo and redo, respectively. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/undo-and-redo#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/undo-and-redo#Related) + +[Feedback](https://developer.apple.com/design/human-interface-guidelines/feedback) + +[Pointing devices](https://developer.apple.com/design/human-interface-guidelines/pointing-devices) + +[Standard keyboard shortcuts](https://developer.apple.com/design/human-interface-guidelines/keyboards#Standard-keyboard-shortcuts) + +[Edit menu](https://developer.apple.com/design/human-interface-guidelines/the-menu-bar#Edit-menu) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/undo-and-redo#Developer-documentation) + +[`UndoManager`](https://developer.apple.com/documentation/Foundation/UndoManager) โ€” Foundation + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/undo-and-redo#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/7/2546ECBD-6443-41EC-921D-6429026F8B67/1700_wide_250x141_1x.jpg) Essential Design Principles ](https://developer.apple.com/videos/play/wwdc2017/802) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/workouts.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/workouts.md new file mode 100644 index 00000000..2dfd1d3b --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-patterns/references/workouts.md @@ -0,0 +1,76 @@ +--- +title: "Workouts | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/workouts + +# Workouts + +A great workout or fitness experience encourages people to engage with their current activity and helps them track their progress on their devices. + +![A sketch of a person running, suggesting exercise. The image is overlaid with rectangular and circular grid lines and is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/94f9af416ac4b86e302f1f487b8a1372/patterns-workouts-intro%402x.png) + +People can wear their Apple Watch during many types of workouts, and they might carry their iPhone or iPad during fitness activities like walking, wheelchair pushing, and running. In contrast, people tend to use their larger or more stationary devices like iPad Pro, Mac, and Apple TV to participate in live or recorded workout sessions by themselves or with others. + +You can create a workout experience for Apple Watch, iPhone, or iPad that helps people reach their goals by leveraging activity data from the device and using familiar components to display fitness metrics. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/workouts#Best-practices) + +**In a watchOS fitness app, use workout sessions to provide useful data and relevant controls.** During a fitness appโ€™s active workout sessions, watchOS continues to display the app as time passes between wrist raises, so itโ€™s important to provide the workout data people are most likely to care about. For example, you might show elapsed or remaining time, calories burned, or distance traveled, and offer relevant controls like lap or interval markers. + +**Avoid distracting people from a workout with information thatโ€™s not relevant.** For example, people donโ€™t need to review the list of workouts you offer or access other parts of your app while theyโ€™re working out. Here is an arrangement that many watchOS workout apps use, including Workout: + +![A screenshot of the leftmost Workout screen for an Outdoor Walk workout. Clockwise from the top-left corner are the End, Resume, New, and Segment buttons.](https://docs-assets.developer.apple.com/published/f893039e593af4d8e40eb6d374dfc6a3/workouts-large-buttons%402x.png) + +Large buttons that control the in-progress session โ€” such as End, Resume, and New โ€” appear on the leftmost screen. + +![A screenshot of the middle Workout screen for an Outdoor Walk workout. Five lines of data are visible. From the top, the screen shows the elapsed time, the active calories, the current heart rate, the average pace, and the elevation.](https://docs-assets.developer.apple.com/published/8de838390249b16301bfc5495947da37/workouts-metrics%402x.png) + +Metrics and other data appear on a dedicated screen that people can read at a glance. + +![A screenshot of the rightmost Workout screen, which shows information about the music currently playing.](https://docs-assets.developer.apple.com/published/acc22020a6613bd6558ce8ea836aa156/workouts-media-playback%402x.png) + +If supported, media playback controls appear on the rightmost screen. + +**Use a distinct visual appearance to indicate an active workout.** During a workout, people appreciate being able to recognize an active session at a glance. The metrics page can be a good way to show that a session is active because the values update in real time. In addition to displaying updating values, you can further distinguish the metrics screen by using a unique layout. + +**Provide workout controls that are easy to find and tap.** In addition to making it easy for people to pause, resume, and stop a workout, be sure to provide clear feedback that indicates when a session starts or stops. + +**Help people understand the health information your app records if sensor data is unavailable during a workout.** For example, water may prevent a heart-rate measurement, but your app can still record data like the distance people swam and the number of calories they burned. If your app supports the _Swimming_ or _Other_ workout types, explain the situation using language thatโ€™s similar to the language used in the system-provided Workout app, as shown below: + +| Example text from the Workout app +---|--- +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png)| GPS is not used during a Pool Swim, and water may prevent a heart-rate measurement, but Apple Watch will still track your calories, laps, and distance using the built-in accelerometer. +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png)| In this type of workout, you earn the calorie equivalent of a brisk walk anytime sensor readings are unavailable. +![A checkmark in a circle to indicate correct usage.](https://docs-assets.developer.apple.com/published/88662da92338267bb64cd2275c84e484/checkmark%402x.png)| GPS will only provide distance when you do a freestyle stroke. Water might prevent a heart-rate measurement, but calories will still be tracked using the built-in accelerometer. + +**Provide a summary at the end of a session.** A summary screen confirms that a workout is finished and displays the recorded information. Consider enhancing the summary by including Activity rings, so that people can easily check their current progress. + +**Discard extremely brief workout sessions.** If a session ends a few seconds after it starts, either discard the data automatically or ask people if they want to record the data as a workout. + +**Make sure text is legible for when people are in motion.** When a session requires movement, use large font sizes, high-contrast colors, and arrange text so that the most important information is easy to read. + +**Use Activity rings correctly.** The Activity rings view is an Apple-designed element featuring one or more rings whose colors and meanings match those in the Activity app. Use them only for their documented purpose. + +## [Platform considerations](https://developer.apple.com/design/human-interface-guidelines/workouts#Platform-considerations) + + _No additional considerations for iOS, iPadOS, or watchOS. Not supported in macOS, tvOS, or visionOS._ + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/workouts#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/workouts#Related) + +[Activity rings](https://developer.apple.com/design/human-interface-guidelines/activity-rings) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/workouts#Developer-documentation) + +[WorkoutKit](https://developer.apple.com/documentation/WorkoutKit) + +[Workouts and activity rings](https://developer.apple.com/documentation/HealthKit/workouts-and-activity-rings) โ€” HealthKit + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/workouts#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/12499BF9-8217-4A56-81CA-5E7CB66904DD/9856_wide_250x141_1x.jpg) Track workouts with HealthKit on iOS and iPadOS ](https://developer.apple.com/videos/play/wwdc2025/322) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/50551741-78CD-4E8A-9550-7D0EC29D7882/8035_wide_250x141_1x.jpg) Build custom workouts with WorkoutKit ](https://developer.apple.com/videos/play/wwdc2023/10016) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/119/30D3C2CB-B24D-467A-9B20-A369641E966F/4850_wide_250x141_1x.jpg) Build a workout app for Apple Watch ](https://developer.apple.com/videos/play/wwdc2021/10009) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/SKILL.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/SKILL.md new file mode 100644 index 00000000..f2b72218 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/SKILL.md @@ -0,0 +1,81 @@ +--- +name: hig-platforms +description: Apple Human Interface Guidelines for platform-specific design. +risk: unknown +source: community +date_added: '2026-02-27' +--- + +# Apple HIG: Platform Design + +Check for `.claude/apple-design-context.md` before asking questions. Use existing context and only ask for information not already covered. + +## Key Principles + +1. **Each platform has a distinct identity.** Do not port designs between platforms. Respect each platform's conventions, interaction models, and user expectations. + +2. **iOS: touch-first.** Direct manipulation on a handheld screen. Optimize for one-handed use. Navigation uses tab bars and push/pop stacks. + +3. **iPadOS: expanded canvas.** Support Split View, Slide Over, and Stage Manager. Use sidebars and multi-column layouts. Support pointer and keyboard alongside touch. + +4. **macOS: pointer and keyboard.** Dense information display is acceptable. Use menu bars, toolbars, and keyboard shortcuts extensively. Windows are resizable with precise control. + +5. **tvOS: remote and focus.** Viewed from a distance. Design for the Siri Remote with focus-based navigation. Large text, simple layouts, linear navigation. + +6. **visionOS: spatial interaction.** 3D environment using windows, volumes, and spaces. Eye tracking for targeting, indirect gestures for interaction. Respect ergonomic comfort zones. + +7. **watchOS: glanceable and brief.** Information consumable at a glance. Brief interactions. Digital Crown, haptics, and complications for timely content. + +8. **Games: own paradigm.** Free to define in-game interaction models, but still respect platform conventions for system interactions (notifications, accessibility, controllers). + +## Reference Index + +| Reference | Topic | Key content | +|---|---|---| +| [designing-for-ios.md](references/designing-for-ios.md) | iOS | Touch, tab bars, navigation stacks, gestures, screen sizes, safe areas | +| [designing-for-ipados.md](references/designing-for-ipados.md) | iPadOS | Multitasking, sidebars, pointer, keyboard, Apple Pencil, Stage Manager | +| [designing-for-macos.md](references/designing-for-macos.md) | macOS | Menu bars, toolbars, window management, keyboard shortcuts, dense layouts, Dock | +| [designing-for-tvos.md](references/designing-for-tvos.md) | tvOS | Focus engine, Siri Remote, lean-back experience, content-forward, parallax | +| [designing-for-visionos.md](references/designing-for-visionos.md) | visionOS | Spatial computing, windows/volumes/spaces, eye tracking, hand gestures, depth | +| [designing-for-watchos.md](references/designing-for-watchos.md) | watchOS | Glanceable UI, Digital Crown, complications, notifications, haptics | +| [designing-for-games.md](references/designing-for-games.md) | Games | Controllers, immersive experiences, platform-specific conventions, accessibility | + +## Decision Framework + +1. **Identify the primary use context.** On the go (iOS/watchOS), at a desk (macOS), on the couch (tvOS), spatial environment (visionOS)? + +2. **Match input to interaction.** Touch for direct manipulation, pointer for precision, gaze+gesture for spatial, Digital Crown for quick scrolling, remote for focus navigation. + +3. **Adapt, don't replicate.** A macOS sidebar becomes a tab bar on iPhone. A visionOS volume has no equivalent on watchOS. Translate intent, not implementation. + +4. **Leverage platform strengths.** Live Activities on iOS, Desktop Widgets on macOS, complications on watchOS, immersive spaces on visionOS. + +5. **Maintain brand consistency** while respecting each platform's visual language and interaction patterns. + +## Output Format + +1. **Platform-specific recommendations** citing relevant HIG sections. +2. **Platform differences table** comparing navigation, input, layout, and conventions. +3. **Implementation notes** per platform including recommended APIs and adaptation strategies. + +## Questions to Ask + +1. Which platforms are you targeting? +2. New app or adapting an existing one? If existing, which platform is the base? +3. SwiftUI or UIKit/AppKit? +4. Need to support older OS versions? +5. Primary use context? (On the go, desk, couch, spatial, glanceable?) + +## Related Skills + +- **hig-foundations** -- Shared principles (color, typography, accessibility, layout) across platforms +- **hig-patterns** -- Interaction patterns that manifest differently per platform +- **hig-components-layout** -- Navigation structures (tab bars, sidebars, split views) that vary by platform +- **hig-components-content** -- Content display that adapts across platforms + +--- + +*Built by [Raintree Technology](https://raintree.technology) ยท [More developer tools](https://raintree.technology)* + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-games.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-games.md new file mode 100644 index 00000000..a84942ed --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-games.md @@ -0,0 +1,159 @@ +--- +title: "Designing for games | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/designing-for-games + +# Designing for games + +When people play your game on an Apple device, they dive into the world you designed while relying on the platform features they love. + +![A stylized representation of a game controller shown on top of a grid. The image is overlaid with rectangular and circular grid lines and is tinted green to subtly reflect the green in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/87a9000504347b999d742d13b3b73635/platforms-games-intro%402x.png) + +As you create or adapt a game for Apple platforms, learn how to integrate the fundamental platform characteristics and patterns that help your game feel at home on all Apple devices. To learn what makes each platform unique, see [Designing for iOS](https://developer.apple.com/design/human-interface-guidelines/designing-for-ios), [Designing for iPadOS](https://developer.apple.com/design/human-interface-guidelines/designing-for-ipados), [Designing for macOS](https://developer.apple.com/design/human-interface-guidelines/designing-for-macos), [Designing for tvOS](https://developer.apple.com/design/human-interface-guidelines/designing-for-tvos), [Designing for visionOS](https://developer.apple.com/design/human-interface-guidelines/designing-for-visionos), and [Designing for watchOS](https://developer.apple.com/design/human-interface-guidelines/designing-for-watchos). For developer guidance, see [Games Pathway](https://developer.apple.com/games/pathway/). + +## [Jump into gameplay](https://developer.apple.com/design/human-interface-guidelines/designing-for-games#Jump-into-gameplay) + +**Let people play as soon as installation completes.** You donโ€™t want a playerโ€™s first experience with your game to be waiting for a lengthy download. Include as much playable content as you can in your gameโ€™s initial installation while keeping the download time to 30 minutes or less. Download additional content in the background. For guidance, see [Loading](https://developer.apple.com/design/human-interface-guidelines/loading). + +**Provide great default settings.** People appreciate being able to start playing without first having to change a lot of settings. Use information about a playerโ€™s device to choose the best defaults for your game, such as the device resolution that makes your graphics look great, automatic recognition of paired accessories and game controllers, and the playerโ€™s accessibility settings. Also, make sure your game supports the platformโ€™s most common interaction methods. For guidance, see [Settings](https://developer.apple.com/design/human-interface-guidelines/settings). + +**Teach through play.** Players often learn better when they discover new information and mechanics in the context of your gameโ€™s world, so it can work well to integrate configuration and onboarding flows into a playable tutorial that engages people quickly and helps them feel successful right away. If you also have a written tutorial, consider offering it as a resource players can refer to when they have questions instead of making it a prerequisite for gameplay. For guidance, see [Onboarding](https://developer.apple.com/design/human-interface-guidelines/onboarding). + +**Defer requests until the right time.** You donโ€™t want to bombard people with too many requests before they start playing, but if your game uses certain sensors on an Apple device or personalizes gameplay by accessing data like hand-tracking, you must first get the playerโ€™s permission (for guidance, see [Privacy](https://developer.apple.com/design/human-interface-guidelines/privacy)). To help people understand why youโ€™re making such a request, integrate it into the scenario that requires the data. For example, you could ask permission to track a playerโ€™s hands between an initial cutscene and the first time they can use their hands to control the action. Also, make sure people spend quality time with your game before you ask them for a rating or review (for guidance, see [Ratings and reviews](https://developer.apple.com/design/human-interface-guidelines/ratings-and-reviews)). + +[![A sketch of a square containing an arrow pointing to the upper-right corner, suggesting a transition to a new state. The image is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/95c588c36ab492f99a5a71addbabef12/patterns-launching-thumbnail%402x.png) Launching ](https://developer.apple.com/design/human-interface-guidelines/launching) + +[![A sketch of a waving hand, suggesting a gesture of welcoming. The image is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/d95e8d3a568083918565701d3fe5360e/patterns-onboarding-thumbnail%402x.png) Onboarding ](https://developer.apple.com/design/human-interface-guidelines/onboarding) + +[![A sketch of a spinning indeterminate activity indicator, suggesting data loading. The image is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/d3c4485e4c81890440c23e3f64d5511e/patterns-loading-thumbnail%402x.png) Loading ](https://developer.apple.com/design/human-interface-guidelines/loading) + +## [Look stunning on every display](https://developer.apple.com/design/human-interface-guidelines/designing-for-games#Look-stunning-on-every-display) + +**Make sure text is always legible.** When game text is hard to read, people can struggle to follow the narrative, understand important instructions and information, and stay engaged in the experience. To keep text comfortably legible on each device, ensure that it contrasts well with the background and uses at least the recommended minimum text size in each platform. For guidance, see [Typography](https://developer.apple.com/design/human-interface-guidelines/typography); for developer guidance, see [Adapting your game interface for smaller screens](https://developer.apple.com/documentation/Metal/adapting-your-game-interface-for-smaller-screens). + +Platform| Default text size| Minimum text size +---|---|--- +iOS, iPadOS| 17 pt| 11 pt +macOS| 13 pt| 10 pt +tvOS| 29 pt| 23 pt +visionOS| 17 pt| 12 pt +watchOS| 16 pt| 12 pt + +**Make sure buttons are always easy to use.** Buttons that are too small or too close together can frustrate players and make gameplay less fun. Each platform defines a recommended minimum button size based on its default interaction method. For example, buttons in iOS must be at least 44x44 pt to accommodate touch interaction. For guidance, see [Buttons](https://developer.apple.com/design/human-interface-guidelines/buttons). + +Platform| Default button size| Minimum button size +---|---|--- +iOS, iPadOS| 44x44 pt| 28x28 pt +macOS| 28x28 pt| 20x20 pt +tvOS| 66x66 pt| 56x56 pt +visionOS| 60x60 pt| 28x28 pt +watchOS| 44x44 pt| 28x28 pt + +**Prefer resolution-independent textures and graphics.** If creating resolution-independent assets isnโ€™t possible, match the resolution of your game to the resolution of the device. In visionOS, prefer vector-based art that can continue to look good when the system dynamically scales it as people view it from different distances and angles. For guidance, see [Images](https://developer.apple.com/design/human-interface-guidelines/images). + +**Integrate device features into your layout.** For example, a device may have rounded corners or a camera housing that can affect parts of your interface. To help your game look at home on each device, accommodate such features during layout, relying on platform-provided safe areas when possible (for developer guidance, see [Positioning content relative to the safe area](https://developer.apple.com/documentation/UIKit/positioning-content-relative-to-the-safe-area)). For guidance, see [Layout](https://developer.apple.com/design/human-interface-guidelines/layout); for templates that include safe-area guides, see [Apple Design Resources](https://developer.apple.com/design/resources/). + +**Make sure in-game menus adapt to different aspect ratios.** Games need to look good and behave well at various aspect ratios, such as 16:10, 19.5:9, and 4:3. In particular, in-game menus need to remain legible and easy to use on every device โ€” and, if you support them, in both orientations on iPhone and iPad โ€” without obscuring other content. To help ensure your in-game menus render correctly, consider using dynamic layouts that rely on relative constraints to adjust to different contexts. Avoid fixed layouts as much as possible, and aim to create a custom, device-specific layout only when necessary. For guidance, see [In-game menus](https://developer.apple.com/design/human-interface-guidelines/menus#In-game-menus). + +**Design for the full-screen experience.** People often enjoy playing a game in a distraction-free, full-screen context. In macOS, iOS, and iPadOS, full-screen mode lets people hide other apps and parts of the system UI; in visionOS, a game running in a Full Space can completely surround people, transporting them somewhere else. For guidance, see [Going full screen](https://developer.apple.com/design/human-interface-guidelines/going-full-screen). + +[![A sketch of a small rectangle in the upper-left quadrant of a larger rectangle, suggesting the position of a user interface element within a window. The image is overlaid with rectangular and circular grid lines and is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/0db27821e97ac9613ac0cc103892a10d/foundations-layout-thumbnail%402x.png) Layout ](https://developer.apple.com/design/human-interface-guidelines/layout) + +[![A sketch of a small letter A to the left of a large letter A, suggesting the use of typography to convey hierarchical information. The image is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/30a9c2adbbfa1602133dcfb2f5bfeaab/foundations-typography-thumbnail%402x.png) Typography ](https://developer.apple.com/design/human-interface-guidelines/typography) + +[![A sketch of two outward-pointing arrows arranged in a vertical line extending from the upper-left to the bottom-right, suggesting expansion. The image is tinted orange to subtly reflect the orange in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/335e1e0476ff4830a1a4add07fb016a0/patterns-going-full-screen-thumbnail%402x.png) Going full screen ](https://developer.apple.com/design/human-interface-guidelines/going-full-screen) + +## [Enable intuitive interactions](https://developer.apple.com/design/human-interface-guidelines/designing-for-games#Enable-intuitive-interactions) + +**Support each platformโ€™s default interaction method.** For example, people generally use touch to play games on iPhone; on a Mac, players tend to expect keyboard and mouse or trackpad support; and in a visionOS game, people expect to use their eyes and hands while making indirect and direct gestures. As you work to ensure that your game supports each platformโ€™s default interaction method, pay special attention to control sizing and menu behavior, especially when bringing your game from a pointer-based context to a touch-based one. + +Platform| Default interaction methods| Additional interaction methods +---|---|--- +iOS| Touch| Game controller +iPadOS| Touch| Game controller, keyboard, mouse, trackpad, Apple Pencil +macOS| Keyboard, mouse, trackpad| Game controller +tvOS| Remote| Game controller, keyboard, mouse, trackpad +visionOS| Touch| Game controller, keyboard, mouse, trackpad, spatial game controller +watchOS| Touch| โ€“ + +**Support physical game controllers, while also giving people alternatives.** Every platform except watchOS supports physical game controllers. Although the presence of a game controller makes it straightforward to port controls from an existing game and handle complex control mappings, recognize that not every player can use a physical game controller. To make your game available to as many players as possible, also offer alternative ways to interact with your game. For guidance, see [Physical controllers](https://developer.apple.com/design/human-interface-guidelines/game-controls#Physical-controllers). + +**Offer touch-based game controls that embrace the touchscreen experience on iPhone and iPad.** In iOS and iPadOS, your game can allow players to interact directly with game elements, and to control the game using virtual controls that appear on top of your game content. For design guidance, see [Touch controls](https://developer.apple.com/design/human-interface-guidelines/game-controls#Touch-controls). + +[![A sketch of a D-pad control from a game controller, suggesting gameplay. The image is tinted purple to subtly reflect the purple in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/36867292ccfd45e0f937f522d7e214f7/inputs-game-controls-thumbnail%402x.png) Game controls ](https://developer.apple.com/design/human-interface-guidelines/game-controls) + +[![A sketch of a pointing hand swiping in a curved motion toward the right, suggesting touch interaction with a device. The image is tinted purple to subtly reflect the purple in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/ca100cc13bcc2fbb7168bebc1da95af8/inputs-gestures-thumbnail%402x.png) Gestures ](https://developer.apple.com/design/human-interface-guidelines/gestures) + +[![A sketch of an arrow-shaped pointer, suggesting use of a mouse or trackpad. The image is tinted purple to subtly reflect the purple in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/69b91fbe2b281f7b638d380a3a6aa416/inputs-pointing-devices-thumbnail%402x.png) Pointing devices ](https://developer.apple.com/design/human-interface-guidelines/pointing-devices) + +## [Welcome everyone](https://developer.apple.com/design/human-interface-guidelines/designing-for-games#Welcome-everyone) + +**Prioritize perceivability.** Make sure people can perceive your gameโ€™s content whether they use sight, hearing, or touch. For example, avoid relying solely on color to convey an important detail, or providing a cutscene that doesnโ€™t include descriptive subtitles or offer other ways to read the content. For specific guidance, see: + + * Text sizes + + * Color and effects + + * Motion + + * Interactions + + * Buttons + + + + +**Help players personalize their experience.** Players have a variety of preferences and abilities that influence their interactions with your game. Because thereโ€™s no universal configuration that suits everyone, give players the ability to customize parameters like type size, game control mapping, motion intensity, and sound balance. You can take advantage of built-in [Apple accessibility technologies](https://developer.apple.com/accessibility/) to support accessibility personalizations, whether youโ€™re using system frameworks or [Unity plug-ins](https://github.com/Apple/UnityPlugins). + +**Give players the tools they need to represent themselves.** If your game encourages players to create avatars or supply names or descriptions, support the spectrum of self-identity and provide options that represent as many human characteristics as possible. + +**Avoid stereotypes in your stories and characters.** Ask yourself whether youโ€™re depicting game characters and scenarios in a way that perpetuates real-life stereotypes. For example, does your game depict enemies as having a certain race, gender, or cultural heritage? Review your game to uncover and remove biases and stereotypes and โ€” if references to real-life cultures and languages are necessary โ€” be sure theyโ€™re respectful. + +[![A sketch of the Accessibility icon. The image is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/8b7b11d06dd958f4e9449f0a5acdadbd/foundations-accessibility-thumbnail%402x.png) Accessibility ](https://developer.apple.com/design/human-interface-guidelines/accessibility) + +[![A sketch of two people, suggesting inclusion. The image is tinted yellow to subtly reflect the yellow in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/bbb606721d228138791654cb9f6f426c/foundations-inclusion-thumbnail%402x.png) Inclusion ](https://developer.apple.com/design/human-interface-guidelines/inclusion) + +## [Adopt Apple technologies](https://developer.apple.com/design/human-interface-guidelines/designing-for-games#Adopt-Apple-technologies) + +**Integrate Game Center to help players discover your game across their devices and connect with their friends.** [Game Center](https://developer.apple.com/game-center/) is Appleโ€™s social gaming network, available on all platforms. Game Center lets players keep track of their progress and achievements and allows you to set up leaderboards, challenges, and multiplayer activities in your game. For design guidance, see [Game Center](https://developer.apple.com/design/human-interface-guidelines/game-center); for developer guidance, see [GameKit](https://developer.apple.com/documentation/GameKit). + +**Let players pick up their game on any of their devices.** People often have a single iCloud account that they use across multiple Apple devices. When you support [GameSave](https://developer.apple.com/documentation/GameSave), you can help people save their game state and start back up exactly where they left off on a different device. + +**Support haptics to help players feel the action.** When you adopt Core Haptics, you can compose and play custom haptic patterns, optionally combined with custom audio content. Core Haptics is available in iOS, iPadOS, tvOS, and visionOS, and supported on many game controllers. For guidance, see [Playing haptics](https://developer.apple.com/design/human-interface-guidelines/playing-haptics); for developer guidance, see [Core Haptics](https://developer.apple.com/documentation/CoreHaptics) and [Playing Haptics on Game Controllers](https://developer.apple.com/documentation/CoreHaptics/playing-haptics-on-game-controllers). + +**Use Spatial Audio to immerse players in your gameโ€™s soundscape.** Providing multichannel audio can help your gameโ€™s audio adapt automatically to the current device, enabling an immersive Spatial Audio experience where supported. For guidance, see [Playing audio > visionOS](https://developer.apple.com/design/human-interface-guidelines/playing-audio#visionOS); for developer guidance, see [Explore Spatial Audio](https://developer.apple.com/news/?id=fakg1z5b). + +**Take advantage of Apple technologies to enable unique gameplay mechanics.** For example, you can integrate technologies like augmented reality, machine learning, and [HealthKit](https://developer.apple.com/documentation/HealthKit), and request access to location data and functionality like camera and microphone. For a full list of Apple technologies, features, and services, see [Technologies](https://developer.apple.com/design/human-interface-guidelines/technologies). + +[![A sketch of the Game Center icon. The image is tinted blue to subtly reflect the blue in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/304edbb771279f5c096419df2cd735fe/technologies-game-center-thumbnail%402x.png) Game Center ](https://developer.apple.com/design/human-interface-guidelines/game-center) + +[![A sketch of the iCloud icon. The image is tinted blue to subtly reflect the blue in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/2b14682c9fa6e5d71042fa722c12bb3f/technologies-icloud-thumbnail%402x.png) iCloud ](https://developer.apple.com/design/human-interface-guidelines/icloud) + +[![A sketch of an add button, suggesting the purchase of additional digital assets within an app. The image is tinted blue to subtly reflect the blue in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/de58b03338460608880147558546707a/technologies-in-app-purchase-thumbnail%402x.png) In-app purchase ](https://developer.apple.com/design/human-interface-guidelines/in-app-purchase) + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/designing-for-games#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/designing-for-games#Related) + +[Game Center](https://developer.apple.com/design/human-interface-guidelines/game-center) + +[Game controls](https://developer.apple.com/design/human-interface-guidelines/game-controls) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/designing-for-games#Developer-documentation) + +[Games Pathway](https://developer.apple.com/games/get-started/) + +[Create games for Apple platforms](https://developer.apple.com/games/) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/designing-for-games#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/6C097D36-BE91-4B04-854A-E6264DA86F15/9890_wide_250x141_1x.jpg) Level up your games ](https://developer.apple.com/videos/play/wwdc2025/209) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/2DB746B8-E0B0-4ED1-B250-902DB7A0F3E7/9196_wide_250x141_1x.jpg) Design advanced games for Apple platforms ](https://developer.apple.com/videos/play/wwdc2024/10085) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/designing-for-games#Change-log) + +Date| Changes +---|--- +June 9, 2025| Updated guidance for touch-based controls and Game Center. +June 10, 2024| New page. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-ios.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-ios.md new file mode 100644 index 00000000..b389da34 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-ios.md @@ -0,0 +1,66 @@ +--- +title: "Designing for iOS | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/designing-for-ios + +# Designing for iOS + +People depend on their iPhone to help them stay connected, play games, view media, accomplish tasks, and track personal data in any location and while on the go. + +![A stylized representation of an iPhone frame shown on top of a grid. The image is overlaid with rectangular and circular grid lines and is tinted green to subtly reflect the green in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/fcb6f603e6672e3443637efe652e5bdb/platforms-iOS-intro%402x.png) + +As you begin designing your app or game for iOS, start by understanding the following fundamental device characteristics and patterns that distinguish the iOS experience. Using these characteristics and patterns to inform your design decisions can help you provide an app or game that iPhone users appreciate. + +**Display.** iPhone has a medium-size, high-resolution display. + +**Ergonomics.** People generally hold their iPhone in one or both hands as they interact with it, switching between landscape and portrait orientations as needed. While people are interacting with the device, their viewing distance tends to be no more than a foot or two. + +**Inputs.** Multi-Touch [gestures](https://developer.apple.com/design/human-interface-guidelines/gestures), [virtual keyboards](https://developer.apple.com/design/human-interface-guidelines/virtual-keyboards), and [voice](https://developer.apple.com/design/human-interface-guidelines/siri) control let people perform actions and accomplish meaningful tasks while theyโ€™re on the go. In addition, people often want apps to use their [personal data](https://developer.apple.com/design/human-interface-guidelines/privacy) and input from the deviceโ€™s [gyroscope and accelerometer](https://developer.apple.com/design/human-interface-guidelines/gyro-and-accelerometer), and they may also want to participate in [spatial interactions](https://developer.apple.com/design/human-interface-guidelines/spatial-interactions). + +**App interactions.** Sometimes, people spend just a minute or two checking on event or social media updates, tracking data, or sending messages. At other times, people can spend an hour or more browsing the web, playing games, or enjoying media. People typically have multiple apps open at the same time, and they appreciate switching frequently among them. + +**System features.** iOS provides several features that help people interact with the system and their apps in familiar, consistent ways. + + * [Widgets](https://developer.apple.com/design/human-interface-guidelines/widgets) + + * [Home Screen quick actions](https://developer.apple.com/design/human-interface-guidelines/home-screen-quick-actions) + + * [Spotlight](https://developer.apple.com/design/human-interface-guidelines/searching) + + * [Shortcuts](https://developer.apple.com/design/human-interface-guidelines/siri#Shortcuts-and-suggestions) + + * [Activity views](https://developer.apple.com/design/human-interface-guidelines/activity-views) + + + + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/designing-for-ios#Best-practices) + +Great iPhone experiences integrate the platform and device capabilities that people value most. To help your design feel at home in iOS, prioritize the following ways to incorporate these features and capabilities. + + * Help people concentrate on primary tasks and content by limiting the number of onscreen controls while making secondary details and actions discoverable with minimal interaction. + + * Adapt seamlessly to appearance changes โ€” like device orientation, Dark Mode, and Dynamic Type โ€” letting people choose the configurations that work best for them. + + * Support interactions that accommodate the way people usually hold their device. For example, it tends to be easier and more comfortable for people to reach a control when itโ€™s located in the middle or bottom area of the display, so itโ€™s especially important let people swipe to navigate back or initiate actions in a list row. + + * With peopleโ€™s permission, integrate information available through platform capabilities in ways that enhance the experience without asking people to enter data. For example, you might accept payments, provide security through biometric authentication, or offer features that use the deviceโ€™s location. + + + + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/designing-for-ios#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/designing-for-ios#Related) + +[Apple Design Resources](https://developer.apple.com/design/resources/#ios-apps) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/designing-for-ios#Developer-documentation) + +[iOS Pathway](https://developer.apple.com/ios/get-started/) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/designing-for-ios#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/5CD0E251-424E-490F-89CF-1E64848209A6/9910_wide_250x141_1x.jpg) Meet Liquid Glass ](https://developer.apple.com/videos/play/wwdc2025/219) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/1AAA030E-2ECA-47D8-AE09-6D7B72A840F6/10044_wide_250x141_1x.jpg) Get to know the new design system ](https://developer.apple.com/videos/play/wwdc2025/356) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-ipados.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-ipados.md new file mode 100644 index 00000000..1fdd308f --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-ipados.md @@ -0,0 +1,64 @@ +--- +title: "Designing for iPadOS | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/designing-for-ipados + +# Designing for iPadOS + +People value the power, mobility, and flexibility of iPad as they enjoy media, play games, perform detailed productivity tasks, and bring their creations to life. + +![A stylized representation of an iPad frame shown on top of a grid. The image is overlaid with rectangular and circular grid lines and is tinted green to subtly reflect the green in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/9601c88115bc94c01906416dbb3b8be8/platforms-iPadOS-intro%402x.png) + +As you begin designing your app or game for iPad, start by understanding the following fundamental device characteristics and patterns that distinguish the iPadOS experience. Using these characteristics and patterns to inform your design decisions can help you provide an app or game that iPad users appreciate. + +**Display.** iPad has a large, high-resolution display. + +**Ergonomics.** People often hold their iPad while using it, but they might also set it on a surface or place it on a stand. Positioning the device in different ways can change the viewing distance, although people are typically within about 3 feet of the device as they interact with it. + +**Inputs.** People can interact with iPad using Multi-Touch [gestures](https://developer.apple.com/design/human-interface-guidelines/gestures) and [virtual keyboards](https://developer.apple.com/design/human-interface-guidelines/virtual-keyboards), an attached [keyboard](https://developer.apple.com/design/human-interface-guidelines/keyboards) or [pointing device](https://developer.apple.com/design/human-interface-guidelines/pointing-devices), [Apple Pencil](https://developer.apple.com/design/human-interface-guidelines/apple-pencil-and-scribble), or [voice](https://developer.apple.com/design/human-interface-guidelines/siri), and they often combine multiple input modes. + +**App interactions.** Sometimes, people perform a few quick actions on their iPad. At other times, they spend hours immersed in games, media, content creation, or productivity tasks. People frequently have multiple apps open at the same time, and they appreciate viewing more than one app onscreen at once and taking advantage of inter-app capabilities like drag and drop. + +**System features.** iPadOS provides several features that help people interact with the system and their apps in familiar, consistent ways. + + * [Multitasking](https://developer.apple.com/design/human-interface-guidelines/multitasking) + + * [Widgets](https://developer.apple.com/design/human-interface-guidelines/widgets) + + * [Drag and drop](https://developer.apple.com/design/human-interface-guidelines/drag-and-drop) + + + + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/designing-for-ipados#Best-practices) + +Great iPad experiences integrate the platform and device capabilities that people value most. To help your experience feel at home in iPadOS, prioritize the following ways to incorporate these features and capabilities. + + * Take advantage of the large display to elevate the content people care about, minimizing modal interfaces and full-screen transitions, and positioning onscreen controls where theyโ€™re easy to reach, but not in the way. + + * Use viewing distance and input mode to help you determine the size and density of the onscreen content you display. + + * Let people use Multi-Touch gestures, a physical keyboard or trackpad, or Apple Pencil, and consider supporting unique interactions that combine multiple input modes. + + * Adapt seamlessly to appearance changes โ€” like device orientation, multitasking modes, Dark Mode, and Dynamic Type โ€” and transition effortlessly to running in macOS, letting people choose the configurations that work best for them. + + + + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/designing-for-ipados#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/designing-for-ipados#Related) + +[Apple Design Resources](https://developer.apple.com/design/resources/#ios-apps) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/designing-for-ipados#Developer-documentation) + +[iPadOS Pathway](https://developer.apple.com/ipados/get-started/) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/designing-for-ipados#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/873F40BE-101A-4C0D-99F0-F5C7CE7B47A3/10046_wide_250x141_1x.jpg) Elevate the design of your iPad app ](https://developer.apple.com/videos/play/wwdc2025/208) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/5CD0E251-424E-490F-89CF-1E64848209A6/9910_wide_250x141_1x.jpg) Meet Liquid Glass ](https://developer.apple.com/videos/play/wwdc2025/219) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/1AAA030E-2ECA-47D8-AE09-6D7B72A840F6/10044_wide_250x141_1x.jpg) Get to know the new design system ](https://developer.apple.com/videos/play/wwdc2025/356) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-macos.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-macos.md new file mode 100644 index 00000000..f9c54d6d --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-macos.md @@ -0,0 +1,70 @@ +--- +title: "Designing for macOS | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/designing-for-macos + +# Designing for macOS + +People rely on the power, spaciousness, and flexibility of a Mac as they perform in-depth productivity tasks, view media or content, and play games, often using several apps at once. + +![A stylized representation of a Mac shown on top of a grid. The image is overlaid with rectangular and circular grid lines and is tinted green to subtly reflect the green in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/c553fef2b37b038dea23f7ef210433b9/platforms-macOS-intro%402x.png) + +As you begin designing your app or game for macOS, start by understanding the fundamental device characteristics and patterns that distinguish the macOS experience. Using these characteristics and patterns to inform your design decisions can help you provide an app or game that Mac users appreciate. + +**Display.** A Mac typically has a large, high-resolution display, and people can extend their workspace by connecting additional displays, including their iPad. + +**Ergonomics.** People generally use a Mac while theyโ€™re stationary, often placing the device on a desk or table. In the typical use case, the viewing distance can range from about 1 to 3 feet. + +**Inputs.** People expect to enter data and control the interface using any combination of input modes, such as physical [Keyboards](https://developer.apple.com/design/human-interface-guidelines/keyboards), [Pointing devices](https://developer.apple.com/design/human-interface-guidelines/pointing-devices), [Game controls](https://developer.apple.com/design/human-interface-guidelines/game-controls), and [Siri](https://developer.apple.com/design/human-interface-guidelines/siri). + +**App interactions.** Interactions can last anywhere from a few minutes of performing some quick tasks to several hours of deep concentration. People frequently have multiple apps open at the same time, and they expect smooth transitions between active and inactive states as they switch from one app to another. + +**System features.** macOS provides several features that help people interact with the system and their apps in familiar, consistent ways. + + * [The menu bar](https://developer.apple.com/design/human-interface-guidelines/the-menu-bar) + + * [File management](https://developer.apple.com/design/human-interface-guidelines/file-management) + + * [Going full screen](https://developer.apple.com/design/human-interface-guidelines/going-full-screen) + + * [Dock menus](https://developer.apple.com/design/human-interface-guidelines/dock-menus) + + + + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/designing-for-macos#Best-practices) + +Great Mac experiences integrate the platform and device capabilities that people value most. To help your design feel at home in macOS, prioritize the following ways to incorporate these features and capabilities. + + * Leverage large displays to present more content in fewer nested levels and with less need for modality, while maintaining a comfortable information density that doesnโ€™t make people strain to view the content they want. + + * Let people resize, hide, show, and move your windows to fit their work style and device configuration, and support full-screen mode to offer a distraction-free context. + + * Use the menu bar to give people easy access to all the commands they need to do things in your app. + + * Help people take advantage of high-precision input modes to perform pixel-perfect selections and edits. + + * Handle keyboard shortcuts to help people accelerate actions and use keyboard-only work styles. + + * Support personalization, letting people customize toolbars, configure windows to display the views they use most, and choose the colors and fonts they want to see in the interface. + + + + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/designing-for-macos#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/designing-for-macos#Related) + +[Apple Design Resources](https://developer.apple.com/design/resources/#macos-apps) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/designing-for-macos#Developer-documentation) + +[macOS Pathway](https://developer.apple.com/macos/get-started/) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/designing-for-macos#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/5CD0E251-424E-490F-89CF-1E64848209A6/9910_wide_250x141_1x.jpg) Meet Liquid Glass ](https://developer.apple.com/videos/play/wwdc2025/219) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/1AAA030E-2ECA-47D8-AE09-6D7B72A840F6/10044_wide_250x141_1x.jpg) Get to know the new design system ](https://developer.apple.com/videos/play/wwdc2025/356) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/754D8BF7-206F-4342-8705-7B836D449D9C/10015_wide_250x141_1x.jpg) Build an AppKit app with the new design ](https://developer.apple.com/videos/play/wwdc2025/310) + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-tvos.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-tvos.md new file mode 100644 index 00000000..e50ca678 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-tvos.md @@ -0,0 +1,68 @@ +--- +title: "Designing for tvOS | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/designing-for-tvos + +# Designing for tvOS + +People enjoy the vibrant content, immersive experiences, and streamlined interactions that tvOS delivers in media and games, as well as in fitness, education, and home utility apps. + +![A stylized representation of a TV screen shown on top of a grid. The image is overlaid with rectangular and circular grid lines and is tinted green to subtly reflect the green in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/cd98e6ef7baa3ddd91de67f43a979c8e/platforms-tvOS-intro%402x.png) + +As you begin designing your app or game for tvOS, start by understanding the following fundamental device characteristics and patterns that distinguish the tvOS experience. Using these characteristics and patterns to inform your design decisions can help you provide an app or game that tvOS users appreciate. + +**Display.** A TV typically has a very large, high-resolution display. + +**Ergonomics.** Although people generally remain many feet away from their stationary TV โ€” often 8 feet or more โ€” they sometimes continue to interact with content as they move around the room. + +**Inputs.** People can use a [remote](https://developer.apple.com/design/human-interface-guidelines/remotes), a [game controller](https://developer.apple.com/design/human-interface-guidelines/game-controls), their [voice](https://developer.apple.com/design/human-interface-guidelines/siri), and apps running on their other devices to interact with Apple TV. + +**App interactions.** People can get deeply immersed in a single experience โ€” often lasting hours โ€” but they also appreciate using a picture-in-picture view to simultaneously follow an alternative app or video. + +**System features.** Apple TV users expect their apps and games to integrate well with the following system experiences. + + * [Integrating with the TV app](https://developer.apple.com/design/human-interface-guidelines/playing-video#Integrating-with-the-TV-app) + + * [SharePlay](https://developer.apple.com/design/human-interface-guidelines/shareplay) + + * [Top Shelf](https://developer.apple.com/design/human-interface-guidelines/top-shelf) + + * [TV provider accounts](https://developer.apple.com/design/human-interface-guidelines/managing-accounts#TV-provider-accounts) + + + + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/designing-for-tvos#Best-practices) + +Great tvOS experiences integrate the platform and device capabilities that people value most. To help your experience feel at home in tvOS, prioritize the following ways to incorporate these features and capabilities. + + * Support powerful, delightful interactions through the fluid, familiar gestures people make with the Siri Remote. + + * Embrace the tvOS focus system, letting it gently highlight and expand onscreen items as people move among them, helping them know what to do and where they are at all times. + + * Deliver beautiful, edge-to-edge artwork, subtle and fluid animations, and engaging audio, wrapping people in a rich, cinematic experience thatโ€™s clear, legible, and captivating from across the room. + + * Enhance multiuser support by making sign-in easy and infrequent, handling shared sign-in, and automatically switching profiles when people change the current viewer. + + + + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/designing-for-tvos#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/designing-for-tvos#Related) + +[Apple Design Resources](https://developer.apple.com/design/resources/#tvos-apps) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/designing-for-tvos#Developer-documentation) + +[tvOS Pathway](https://developer.apple.com/tvos/get-started/) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/designing-for-tvos#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/49/B17CF999-313D-4C0D-A791-D2A5DD5F9242/3789_wide_250x141_1x.jpg) Build SwiftUI apps for tvOS ](https://developer.apple.com/videos/play/wwdc2020/10042) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/designing-for-tvos#Change-log) + +Date| Changes +---|--- +September 14, 2022| Refined best practices for multiuser support. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-visionos.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-visionos.md new file mode 100644 index 00000000..96da90b5 --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-visionos.md @@ -0,0 +1,85 @@ +--- +title: "Designing for visionOS | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/designing-for-visionos + +# Designing for visionOS + +When people wear Apple Vision Pro, they enter an infinite 3D space where they can engage with your app or game while staying connected to their surroundings. + +![A stylized representation of Apple Vision Pro shown on top of a grid. The image is overlaid with rectangular and circular grid lines and is tinted green to subtly reflect the green in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/9d51f701c7321cced431aac9d5212b9e/platforms-visionOS-intro%402x.png) + +As you begin designing your app or game for visionOS, start by understanding the fundamental device characteristics and patterns that distinguish the platform. Use these characteristics and patterns to inform your design decisions and help you create immersive and engaging experiences. + +**Space.** Apple Vision Pro offers a limitless canvas where people can view virtual content like [windows](https://developer.apple.com/design/human-interface-guidelines/windows), [volumes](https://developer.apple.com/design/human-interface-guidelines/windows#visionOS-volumes), and 3D objects, and choose to enter deeply immersive experiences that can transport them to different places. + +**Immersion.** In a visionOS app, people can fluidly transition between different levels of [immersion](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences). By default, an app launches in the _Shared Space_ where multiple apps can run side-by-side and people can open, close, and relocate windows. People can also choose to transition an app to a _Full Space_ , where itโ€™s the only app running. While in a Full Space app, people can view 3D content blended with their surroundings, open a portal to view another place, or enter a different world. + +**Passthrough.** [Passthrough](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences#Immersion-and-passthrough) provides live video from the deviceโ€™s external cameras, and helps people interact with virtual content while also seeing their actual surroundings. When people want to see more or less of their surroundings, they use the [Digital Crown](https://developer.apple.com/design/human-interface-guidelines/digital-crown) to control the amount of passthrough. + +**Spatial Audio.** Apple Vision Pro combines acoustic and visual-sensing technologies to model the sonic characteristics of a personโ€™s surroundings, automatically making audio sound natural in their space. When an app receives a personโ€™s permission to access information about their surroundings, it can fine-tune [Spatial Audio](https://developer.apple.com/design/human-interface-guidelines/playing-audio#visionOS) to bring custom experiences to life. + +**Eyes and hands.** In general, people perform most actions by using their [eyes](https://developer.apple.com/design/human-interface-guidelines/eyes) to look at a virtual object and making an _indirect_ [gesture](https://developer.apple.com/design/human-interface-guidelines/gestures#visionOS), like a tap, to activate it. People can also interact with a virtual object by using a _direct_ gesture, like touching it with a finger. + +**Ergonomics.** While wearing Apple Vision Pro, people rely entirely on the deviceโ€™s cameras for everything they see, both real and virtual, so maintaining visual comfort is paramount. The system helps maintain comfort by automatically placing content so itโ€™s relative to the wearerโ€™s head, regardless of the personโ€™s height or whether theyโ€™re sitting, standing, or lying down. Because visionOS brings content to people โ€” instead of making people move to reach the content โ€” people can remain at rest while engaging with apps and games. + +**Accessibility.** Apple Vision Pro supports [accessibility](https://developer.apple.com/design/human-interface-guidelines/accessibility) technologies like VoiceOver, Switch Control, Dwell Control, Guided Access, Head Pointer, and many more, so people can use the interactions that work for them. In visionOS, as in all platforms, system-provided UI components build in accessibility support by default, while system frameworks give you ways to enhance the accessibility of your app or game. + +Important + +When building your app for Apple Vision Pro, be sure to consider the unique characteristics of the device and its spatial computing environment, and pay special attention to your userโ€™s safety; for more details about these characteristics, see [Apple Vision Pro User Guide](https://support.apple.com/guide/apple-vision-pro). For example, Apple Vision Pro should not be used while operating a vehicle or heavy machinery. The device is also not designed to be used while moving around unsafe environments such as near balconies, streets, stairs, or other potential hazards. Note that Apple Vision Pro is designed to be fit and used only by individuals 13 years of age or older. + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/designing-for-visionos#Best-practices) + +Great visionOS apps and games are approachable and familiar, while offering extraordinary experiences that can surround people with beautiful content, expanded capabilities, and captivating adventures. + +**Embrace the unique features of Apple Vision Pro.** Take advantage of space, Spatial Audio, and immersion to bring life to your experiences, while integrating passthrough and spatial input from eyes and hands in ways that feel at home on the device. + +**Consider different types of immersion as you design ways to present your appโ€™s most distinctive moments.** You can present experiences in a windowed, UI-centric context, a fully immersive context, or something in between. For each key moment in your app, find the minimum level of immersion that suits it best โ€” donโ€™t assume that every moment needs to be fully immersive. + +**Use windows for contained, UI-centric experiences.** To help people perform standard tasks, prefer standard [windows](https://developer.apple.com/design/human-interface-guidelines/windows#visionOS) that appear as planes in space and contain familiar controls. In visionOS, people can relocate windows anywhere they want, and the systemโ€™s [dynamic scaling](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Scale) helps keep window content legible whether itโ€™s near or far. + +**Prioritize comfort.** To help people stay comfortable and physically relaxed as they interact with your app or game, keep the following fundamentals in mind. + + * Display content within a personโ€™s [field of view](https://developer.apple.com/design/human-interface-guidelines/spatial-layout#Field-of-view), positioning it relative to their head. Avoid placing content in places where people have to turn their head or change their position to interact with it. + + * Avoid displaying [motion](https://developer.apple.com/design/human-interface-guidelines/motion#visionOS) thatโ€™s overwhelming, jarring, too fast, or missing a stationary frame of reference. + + * Support [indirect gestures](https://developer.apple.com/design/human-interface-guidelines/gestures#visionOS) that let people interact with apps while their hands rest in their lap or at their sides. + + * If you support direct gestures, make sure the interactive content isnโ€™t too far away and that people donโ€™t need to interact with it for extended periods. + + * Avoid encouraging people to move too much while theyโ€™re in a fully [immersive experience](https://developer.apple.com/design/human-interface-guidelines/immersive-experiences). + + + + +**Help people share activities with others.** When you use [SharePlay](https://developer.apple.com/design/human-interface-guidelines/shareplay#visionOS) to support shared activities, people can view the _spatial Personas_ of other participants, making it feel like everyone is together in the same space. + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/designing-for-visionos#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/designing-for-visionos#Related) + +[Apple Design Resources](https://developer.apple.com/design/resources/#visionos-apps) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/designing-for-visionos#Developer-documentation) + +[visionOS Pathway](https://developer.apple.com/visionos/get-started/) + +[Creating your first visionOS app](https://developer.apple.com/documentation/visionOS/creating-your-first-visionos-app) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/designing-for-visionos#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/B2E0763D-C5D2-4191-AC0B-F99D56BE781F/9208_wide_250x141_1x.jpg) Design interactive experiences for visionOS ](https://developer.apple.com/videos/play/wwdc2024/10096) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/C03E6E6D-A32A-41D0-9E50-C3C6059820AA/A207EE2A-D9E6-4FAB-A557-ABEAA68840AB/9197_wide_250x141_1x.jpg) Design great visionOS apps ](https://developer.apple.com/videos/play/wwdc2024/10086) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/D35E0E85-CCB6-41A1-B227-7995ECD83ED5/15489B11-8744-483D-AD38-EF78D8962FF4/8126_wide_250x141_1x.jpg) Principles of spatial design ](https://developer.apple.com/videos/play/wwdc2023/10072) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/designing-for-visionos#Change-log) + +Date| Changes +---|--- +February 2, 2024| Included a link to Apple Vision Pro User Guide. +September 12, 2023| Updated intro artwork. +June 21, 2023| New page. + diff --git a/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-watchos.md b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-watchos.md new file mode 100644 index 00000000..69bd57cc --- /dev/null +++ b/plugins/antigravity-bundle-apple-platform-design/skills/hig-platforms/references/designing-for-watchos.md @@ -0,0 +1,74 @@ +--- +title: "Designing for watchOS | Apple Developer Documentation" +source: https://developer.apple.com/design/human-interface-guidelines/designing-for-watchos + +# Designing for watchOS + +When people glance at their Apple Watch, they know they can access essential information and perform simple, timely tasks whether theyโ€™re stationary or in motion. + +![A stylized representation of an Apple Watch frame shown on top of a grid. The image is overlaid with rectangular and circular grid lines and is tinted green to subtly reflect the green in the original six-color Apple logo.](https://docs-assets.developer.apple.com/published/4fb079e34e794b1a6b9fc03acf2c22a4/platforms-watchOS-intro%402x.png) + +As you begin designing your app for Apple Watch, start by understanding the following fundamental device characteristics and patterns that distinguish the watchOS experience. Using these characteristics and patterns to inform your design decisions can help you provide an app that Apple Watch users appreciate. + +**Display.** The small Apple Watch display fits on the wrist while delivering an easy-to-read, high-resolution experience. + +**Ergonomics.** Because people wear Apple Watch, theyโ€™re usually no more than a foot away from the display as they raise their wrist to view it and use their opposite hand to interact with the device. In addition, the Always On display lets people view information on the watch face when they drop their wrist. + +**Inputs.** People can navigate vertically or inspect data by turning the [Digital Crown](https://developer.apple.com/design/human-interface-guidelines/digital-crown), which offers consistent control on the watch face, the Home Screen, and within apps. They can provide input even while theyโ€™re in motion with standard [gestures](https://developer.apple.com/design/human-interface-guidelines/gestures) like tap, swipe, and drag. Pressing the [Action button](https://developer.apple.com/design/human-interface-guidelines/action-button) initiates an essential action without looking at the screen, and using [shortcuts](https://developer.apple.com/design/human-interface-guidelines/siri#Shortcuts-and-suggestions) helps people perform their routine tasks quickly and easily. People can also take advantage of data that device features provide, such as GPS, sensors for blood oxygen and heart function, and the altimeter, accelerometer, and gyroscope. + +**App interactions.** People glance at the Always On display many times throughout the day, performing concise app interactions that can last for less than a minute each. People frequently use a watchOS appโ€™s related experiences โ€” like complications, notifications, and Siri interactions โ€” more than they use the app itself. + +**System features.** watchOS provides several features that help people interact with the system and their apps in familiar, consistent ways. + + * [Complications](https://developer.apple.com/design/human-interface-guidelines/complications) + + * [Notifications](https://developer.apple.com/design/human-interface-guidelines/notifications) + + * [Always On](https://developer.apple.com/design/human-interface-guidelines/always-on) + + * [Watch faces](https://developer.apple.com/design/human-interface-guidelines/watch-faces) + + + + +## [Best practices](https://developer.apple.com/design/human-interface-guidelines/designing-for-watchos#Best-practices) + +Great Apple Watch experiences are streamlined and specialized, and integrate the platform and device capabilities that people value most. To help your experience feel at home in watchOS, prioritize the following ways to incorporate these features and capabilities. + + * Support quick, glanceable, single-screen interactions that deliver critical information succinctly and help people perform targeted actions with a simple gesture or two. + + * Minimize the depth of hierarchy in your appโ€™s navigation, and use the [Digital Crown](https://developer.apple.com/design/human-interface-guidelines/digital-crown) to provide vertical navigation for scrolling or switching between screens. + + * Personalize the experience by proactively anticipating peopleโ€™s needs and using on-device data to provide actionable content thatโ€™s relevant in the moment or very soon. + + * Use [complications](https://developer.apple.com/design/human-interface-guidelines/complications) to provide relevant, potentially dynamic data and graphics right on the watch face where people can view them on every wrist raise and tap them to dive straight into your app. + + * Use [notifications](https://developer.apple.com/design/human-interface-guidelines/notifications) to deliver timely, high-value information and let people perform important actions without opening your app. + + * Use background content such as [color](https://developer.apple.com/design/human-interface-guidelines/color) to convey useful supporting information, and use [materials](https://developer.apple.com/design/human-interface-guidelines/materials) to illustrate hierarchy and a sense of place. + + * Design your app to function independently, complementing your notifications and complications by providing additional details and functionality. + + + + +## [Resources](https://developer.apple.com/design/human-interface-guidelines/designing-for-watchos#Resources) + +#### [Related](https://developer.apple.com/design/human-interface-guidelines/designing-for-watchos#Related) + +[Apple Design Resources](https://developer.apple.com/design/resources/#watchos-apps) + +#### [Developer documentation](https://developer.apple.com/design/human-interface-guidelines/designing-for-watchos#Developer-documentation) + +[watchOS Pathway](https://developer.apple.com/watchos/get-started/) + +#### [Videos](https://developer.apple.com/design/human-interface-guidelines/designing-for-watchos#Videos) + +[![](https://devimages-cdn.apple.com/wwdc-services/images/3055294D-836B-4513-B7B0-0BC5666246B0/29B2363D-8DFB-4A25-A3DB-A804582858FD/9956_wide_250x141_1x.jpg) Whatโ€™s new in watchOS 26 ](https://developer.apple.com/videos/play/wwdc2025/334) + +## [Change log](https://developer.apple.com/design/human-interface-guidelines/designing-for-watchos#Change-log) + +Date| Changes +---|--- +June 5, 2023| Enhanced guidance for providing a glanceable, focused app experience, and emphasized the importance of the Digital Crown in navigation. + diff --git a/plugins/antigravity-bundle-architecture-design/.codex-plugin/plugin.json b/plugins/antigravity-bundle-architecture-design/.codex-plugin/plugin.json new file mode 100644 index 00000000..fb32ba74 --- /dev/null +++ b/plugins/antigravity-bundle-architecture-design/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-architecture-design", + "version": "8.10.0", + "description": "Install the \"Architecture & Design\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "architecture-design", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Architecture & Design", + "shortDescription": "Specialized Packs ยท 5 curated skills", + "longDescription": "For system design and technical decisions. Covers Senior Architect, Architecture Patterns, and 3 more skills.", + "developerName": "sickn33 and contributors", + "category": "Specialized Packs", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-architecture-design/skills/architecture-decision-records/SKILL.md b/plugins/antigravity-bundle-architecture-design/skills/architecture-decision-records/SKILL.md new file mode 100644 index 00000000..0ef133c1 --- /dev/null +++ b/plugins/antigravity-bundle-architecture-design/skills/architecture-decision-records/SKILL.md @@ -0,0 +1,444 @@ +--- +name: architecture-decision-records +description: "Comprehensive patterns for creating, maintaining, and managing Architecture Decision Records (ADRs) that capture the context and rationale behind significant technical decisions." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Architecture Decision Records + +Comprehensive patterns for creating, maintaining, and managing Architecture Decision Records (ADRs) that capture the context and rationale behind significant technical decisions. + +## Use this skill when + +- Making significant architectural decisions +- Documenting technology choices +- Recording design trade-offs +- Onboarding new team members +- Reviewing historical decisions +- Establishing decision-making processes + +## Do not use this skill when + +- You only need to document small implementation details +- The change is a minor patch or routine maintenance +- There is no architectural decision to capture + +## Instructions + +1. Capture the decision context, constraints, and drivers. +2. Document considered options with tradeoffs. +3. Record the decision, rationale, and consequences. +4. Link related ADRs and update status over time. + +## Core Concepts + +### 1. What is an ADR? + +An Architecture Decision Record captures: +- **Context**: Why we needed to make a decision +- **Decision**: What we decided +- **Consequences**: What happens as a result + +### 2. When to Write an ADR + +| Write ADR | Skip ADR | +|-----------|----------| +| New framework adoption | Minor version upgrades | +| Database technology choice | Bug fixes | +| API design patterns | Implementation details | +| Security architecture | Routine maintenance | +| Integration patterns | Configuration changes | + +### 3. ADR Lifecycle + +``` +Proposed โ†’ Accepted โ†’ Deprecated โ†’ Superseded + โ†“ + Rejected +``` + +## Templates + +### Template 1: Standard ADR (MADR Format) + +```markdown +# ADR-0001: Use PostgreSQL as Primary Database + +## Status + +Accepted + +## Context + +We need to select a primary database for our new e-commerce platform. The system +will handle: +- ~10,000 concurrent users +- Complex product catalog with hierarchical categories +- Transaction processing for orders and payments +- Full-text search for products +- Geospatial queries for store locator + +The team has experience with MySQL, PostgreSQL, and MongoDB. We need ACID +compliance for financial transactions. + +## Decision Drivers + +* **Must have ACID compliance** for payment processing +* **Must support complex queries** for reporting +* **Should support full-text search** to reduce infrastructure complexity +* **Should have good JSON support** for flexible product attributes +* **Team familiarity** reduces onboarding time + +## Considered Options + +### Option 1: PostgreSQL +- **Pros**: ACID compliant, excellent JSON support (JSONB), built-in full-text + search, PostGIS for geospatial, team has experience +- **Cons**: Slightly more complex replication setup than MySQL + +### Option 2: MySQL +- **Pros**: Very familiar to team, simple replication, large community +- **Cons**: Weaker JSON support, no built-in full-text search (need + Elasticsearch), no geospatial without extensions + +### Option 3: MongoDB +- **Pros**: Flexible schema, native JSON, horizontal scaling +- **Cons**: No ACID for multi-document transactions (at decision time), + team has limited experience, requires schema design discipline + +## Decision + +We will use **PostgreSQL 15** as our primary database. + +## Rationale + +PostgreSQL provides the best balance of: +1. **ACID compliance** essential for e-commerce transactions +2. **Built-in capabilities** (full-text search, JSONB, PostGIS) reduce + infrastructure complexity +3. **Team familiarity** with SQL databases reduces learning curve +4. **Mature ecosystem** with excellent tooling and community support + +The slight complexity in replication is outweighed by the reduction in +additional services (no separate Elasticsearch needed). + +## Consequences + +### Positive +- Single database handles transactions, search, and geospatial queries +- Reduced operational complexity (fewer services to manage) +- Strong consistency guarantees for financial data +- Team can leverage existing SQL expertise + +### Negative +- Need to learn PostgreSQL-specific features (JSONB, full-text search syntax) +- Vertical scaling limits may require read replicas sooner +- Some team members need PostgreSQL-specific training + +### Risks +- Full-text search may not scale as well as dedicated search engines +- Mitigation: Design for potential Elasticsearch addition if needed + +## Implementation Notes + +- Use JSONB for flexible product attributes +- Implement connection pooling with PgBouncer +- Set up streaming replication for read replicas +- Use pg_trgm extension for fuzzy search + +## Related Decisions + +- ADR-0002: Caching Strategy (Redis) - complements database choice +- ADR-0005: Search Architecture - may supersede if Elasticsearch needed + +## References + +- [PostgreSQL JSON Documentation](https://www.postgresql.org/docs/current/datatype-json.html) +- [PostgreSQL Full Text Search](https://www.postgresql.org/docs/current/textsearch.html) +- Internal: Performance benchmarks in `/docs/benchmarks/database-comparison.md` +``` + +### Template 2: Lightweight ADR + +```markdown +# ADR-0012: Adopt TypeScript for Frontend Development + +**Status**: Accepted +**Date**: 2024-01-15 +**Deciders**: @alice, @bob, @charlie + +## Context + +Our React codebase has grown to 50+ components with increasing bug reports +related to prop type mismatches and undefined errors. PropTypes provide +runtime-only checking. + +## Decision + +Adopt TypeScript for all new frontend code. Migrate existing code incrementally. + +## Consequences + +**Good**: Catch type errors at compile time, better IDE support, self-documenting +code. + +**Bad**: Learning curve for team, initial slowdown, build complexity increase. + +**Mitigations**: TypeScript training sessions, allow gradual adoption with +`allowJs: true`. +``` + +### Template 3: Y-Statement Format + +```markdown +# ADR-0015: API Gateway Selection + +In the context of **building a microservices architecture**, +facing **the need for centralized API management, authentication, and rate limiting**, +we decided for **Kong Gateway** +and against **AWS API Gateway and custom Nginx solution**, +to achieve **vendor independence, plugin extensibility, and team familiarity with Lua**, +accepting that **we need to manage Kong infrastructure ourselves**. +``` + +### Template 4: ADR for Deprecation + +```markdown +# ADR-0020: Deprecate MongoDB in Favor of PostgreSQL + +## Status + +Accepted (Supersedes ADR-0003) + +## Context + +ADR-0003 (2021) chose MongoDB for user profile storage due to schema flexibility +needs. Since then: +- MongoDB's multi-document transactions remain problematic for our use case +- Our schema has stabilized and rarely changes +- We now have PostgreSQL expertise from other services +- Maintaining two databases increases operational burden + +## Decision + +Deprecate MongoDB and migrate user profiles to PostgreSQL. + +## Migration Plan + +1. **Phase 1** (Week 1-2): Create PostgreSQL schema, dual-write enabled +2. **Phase 2** (Week 3-4): Backfill historical data, validate consistency +3. **Phase 3** (Week 5): Switch reads to PostgreSQL, monitor +4. **Phase 4** (Week 6): Remove MongoDB writes, decommission + +## Consequences + +### Positive +- Single database technology reduces operational complexity +- ACID transactions for user data +- Team can focus PostgreSQL expertise + +### Negative +- Migration effort (~4 weeks) +- Risk of data issues during migration +- Lose some schema flexibility + +## Lessons Learned + +Document from ADR-0003 experience: +- Schema flexibility benefits were overestimated +- Operational cost of multiple databases was underestimated +- Consider long-term maintenance in technology decisions +``` + +### Template 5: Request for Comments (RFC) Style + +```markdown +# RFC-0025: Adopt Event Sourcing for Order Management + +## Summary + +Propose adopting event sourcing pattern for the order management domain to +improve auditability, enable temporal queries, and support business analytics. + +## Motivation + +Current challenges: +1. Audit requirements need complete order history +2. "What was the order state at time X?" queries are impossible +3. Analytics team needs event stream for real-time dashboards +4. Order state reconstruction for customer support is manual + +## Detailed Design + +### Event Store + +``` +OrderCreated { orderId, customerId, items[], timestamp } +OrderItemAdded { orderId, item, timestamp } +OrderItemRemoved { orderId, itemId, timestamp } +PaymentReceived { orderId, amount, paymentId, timestamp } +OrderShipped { orderId, trackingNumber, timestamp } +``` + +### Projections + +- **CurrentOrderState**: Materialized view for queries +- **OrderHistory**: Complete timeline for audit +- **DailyOrderMetrics**: Analytics aggregation + +### Technology + +- Event Store: EventStoreDB (purpose-built, handles projections) +- Alternative considered: Kafka + custom projection service + +## Drawbacks + +- Learning curve for team +- Increased complexity vs. CRUD +- Need to design events carefully (immutable once stored) +- Storage growth (events never deleted) + +## Alternatives + +1. **Audit tables**: Simpler but doesn't enable temporal queries +2. **CDC from existing DB**: Complex, doesn't change data model +3. **Hybrid**: Event source only for order state changes + +## Unresolved Questions + +- [ ] Event schema versioning strategy +- [ ] Retention policy for events +- [ ] Snapshot frequency for performance + +## Implementation Plan + +1. Prototype with single order type (2 weeks) +2. Team training on event sourcing (1 week) +3. Full implementation and migration (4 weeks) +4. Monitoring and optimization (ongoing) + +## References + +- [Event Sourcing by Martin Fowler](https://martinfowler.com/eaaDev/EventSourcing.html) +- [EventStoreDB Documentation](https://www.eventstore.com/docs) +``` + +## ADR Management + +### Directory Structure + +``` +docs/ +โ”œโ”€โ”€ adr/ +โ”‚ โ”œโ”€โ”€ README.md # Index and guidelines +โ”‚ โ”œโ”€โ”€ template.md # Team's ADR template +โ”‚ โ”œโ”€โ”€ 0001-use-postgresql.md +โ”‚ โ”œโ”€โ”€ 0002-caching-strategy.md +โ”‚ โ”œโ”€โ”€ 0003-mongodb-user-profiles.md # [DEPRECATED] +โ”‚ โ””โ”€โ”€ 0020-deprecate-mongodb.md # Supersedes 0003 +``` + +### ADR Index (README.md) + +```markdown +# Architecture Decision Records + +This directory contains Architecture Decision Records (ADRs) for [Project Name]. + +## Index + +| ADR | Title | Status | Date | +|-----|-------|--------|------| +| 0001 | Use PostgreSQL as Primary Database | Accepted | 2024-01-10 | +| 0002 | Caching Strategy with Redis | Accepted | 2024-01-12 | +| 0003 | MongoDB for User Profiles | Deprecated | 2023-06-15 | +| 0020 | Deprecate MongoDB | Accepted | 2024-01-15 | + +## Creating a New ADR + +1. Copy `template.md` to `NNNN-title-with-dashes.md` +2. Fill in the template +3. Submit PR for review +4. Update this index after approval + +## ADR Status + +- **Proposed**: Under discussion +- **Accepted**: Decision made, implementing +- **Deprecated**: No longer relevant +- **Superseded**: Replaced by another ADR +- **Rejected**: Considered but not adopted +``` + +### Automation (adr-tools) + +```bash +# Install adr-tools +brew install adr-tools + +# Initialize ADR directory +adr init docs/adr + +# Create new ADR +adr new "Use PostgreSQL as Primary Database" + +# Supersede an ADR +adr new -s 3 "Deprecate MongoDB in Favor of PostgreSQL" + +# Generate table of contents +adr generate toc > docs/adr/README.md + +# Link related ADRs +adr link 2 "Complements" 1 "Is complemented by" +``` + +## Review Process + +```markdown +## ADR Review Checklist + +### Before Submission +- [ ] Context clearly explains the problem +- [ ] All viable options considered +- [ ] Pros/cons balanced and honest +- [ ] Consequences (positive and negative) documented +- [ ] Related ADRs linked + +### During Review +- [ ] At least 2 senior engineers reviewed +- [ ] Affected teams consulted +- [ ] Security implications considered +- [ ] Cost implications documented +- [ ] Reversibility assessed + +### After Acceptance +- [ ] ADR index updated +- [ ] Team notified +- [ ] Implementation tickets created +- [ ] Related documentation updated +``` + +## Best Practices + +### Do's +- **Write ADRs early** - Before implementation starts +- **Keep them short** - 1-2 pages maximum +- **Be honest about trade-offs** - Include real cons +- **Link related decisions** - Build decision graph +- **Update status** - Deprecate when superseded + +### Don'ts +- **Don't change accepted ADRs** - Write new ones to supersede +- **Don't skip context** - Future readers need background +- **Don't hide failures** - Rejected decisions are valuable +- **Don't be vague** - Specific decisions, specific consequences +- **Don't forget implementation** - ADR without action is waste + +## Resources + +- [Documenting Architecture Decisions (Michael Nygard)](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) +- [MADR Template](https://adr.github.io/madr/) +- [ADR GitHub Organization](https://adr.github.io/) +- [adr-tools](https://github.com/npryce/adr-tools) diff --git a/plugins/antigravity-bundle-architecture-design/skills/architecture-patterns/SKILL.md b/plugins/antigravity-bundle-architecture-design/skills/architecture-patterns/SKILL.md new file mode 100644 index 00000000..515387c4 --- /dev/null +++ b/plugins/antigravity-bundle-architecture-design/skills/architecture-patterns/SKILL.md @@ -0,0 +1,45 @@ +--- +name: architecture-patterns +description: "Master proven backend architecture patterns including Clean Architecture, Hexagonal Architecture, and Domain-Driven Design to build maintainable, testable, and scalable systems." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Architecture Patterns + +Master proven backend architecture patterns including Clean Architecture, Hexagonal Architecture, and Domain-Driven Design to build maintainable, testable, and scalable systems. + +## Use this skill when + +- Designing new backend systems from scratch +- Refactoring monolithic applications for better maintainability +- Establishing architecture standards for your team +- Migrating from tightly coupled to loosely coupled architectures +- Implementing domain-driven design principles +- Creating testable and mockable codebases +- Planning microservices decomposition + +## Do not use this skill when + +- You only need small, localized refactors +- The system is primarily frontend with no backend architecture changes +- You need implementation details without architectural design + +## Instructions + +1. Clarify domain boundaries, constraints, and scalability targets. +2. Select an architecture pattern that fits the domain complexity. +3. Define module boundaries, interfaces, and dependency rules. +4. Provide migration steps and validation checks. +5. For workflows that must survive failures (payments, order fulfillment, multi-step processes), use durable execution at the infrastructure layer โ€” frameworks like DBOS persist workflow state, providing crash recovery without adding architectural complexity. + +Refer to `resources/implementation-playbook.md` for detailed patterns, checklists, and templates. + +## Related Skills + +Works well with: `event-sourcing-architect`, `saga-orchestration`, `workflow-automation`, `dbos-*` + +## Resources + +- `resources/implementation-playbook.md` for detailed patterns, checklists, and templates. diff --git a/plugins/antigravity-bundle-architecture-design/skills/architecture-patterns/resources/implementation-playbook.md b/plugins/antigravity-bundle-architecture-design/skills/architecture-patterns/resources/implementation-playbook.md new file mode 100644 index 00000000..cf8de60f --- /dev/null +++ b/plugins/antigravity-bundle-architecture-design/skills/architecture-patterns/resources/implementation-playbook.md @@ -0,0 +1,479 @@ +# Architecture Patterns Implementation Playbook + +This file contains detailed patterns, checklists, and code samples referenced by the skill. + +## Core Concepts + +### 1. Clean Architecture (Uncle Bob) + +**Layers (dependency flows inward):** + +- **Entities**: Core business models +- **Use Cases**: Application business rules +- **Interface Adapters**: Controllers, presenters, gateways +- **Frameworks & Drivers**: UI, database, external services + +**Key Principles:** + +- Dependencies point inward +- Inner layers know nothing about outer layers +- Business logic independent of frameworks +- Testable without UI, database, or external services + +### 2. Hexagonal Architecture (Ports and Adapters) + +**Components:** + +- **Domain Core**: Business logic +- **Ports**: Interfaces defining interactions +- **Adapters**: Implementations of ports (database, REST, message queue) + +**Benefits:** + +- Swap implementations easily (mock for testing) +- Technology-agnostic core +- Clear separation of concerns + +### 3. Domain-Driven Design (DDD) + +**Strategic Patterns:** + +- **Bounded Contexts**: Separate models for different domains +- **Context Mapping**: How contexts relate +- **Ubiquitous Language**: Shared terminology + +**Tactical Patterns:** + +- **Entities**: Objects with identity +- **Value Objects**: Immutable objects defined by attributes +- **Aggregates**: Consistency boundaries +- **Repositories**: Data access abstraction +- **Domain Events**: Things that happened + +## Clean Architecture Pattern + +### Directory Structure + +``` +app/ +โ”œโ”€โ”€ domain/ # Entities & business rules +โ”‚ โ”œโ”€โ”€ entities/ +โ”‚ โ”‚ โ”œโ”€โ”€ user.py +โ”‚ โ”‚ โ””โ”€โ”€ order.py +โ”‚ โ”œโ”€โ”€ value_objects/ +โ”‚ โ”‚ โ”œโ”€โ”€ email.py +โ”‚ โ”‚ โ””โ”€โ”€ money.py +โ”‚ โ””โ”€โ”€ interfaces/ # Abstract interfaces +โ”‚ โ”œโ”€โ”€ user_repository.py +โ”‚ โ””โ”€โ”€ payment_gateway.py +โ”œโ”€โ”€ use_cases/ # Application business rules +โ”‚ โ”œโ”€โ”€ create_user.py +โ”‚ โ”œโ”€โ”€ process_order.py +โ”‚ โ””โ”€โ”€ send_notification.py +โ”œโ”€โ”€ adapters/ # Interface implementations +โ”‚ โ”œโ”€โ”€ repositories/ +โ”‚ โ”‚ โ”œโ”€โ”€ postgres_user_repository.py +โ”‚ โ”‚ โ””โ”€โ”€ redis_cache_repository.py +โ”‚ โ”œโ”€โ”€ controllers/ +โ”‚ โ”‚ โ””โ”€โ”€ user_controller.py +โ”‚ โ””โ”€โ”€ gateways/ +โ”‚ โ”œโ”€โ”€ stripe_payment_gateway.py +โ”‚ โ””โ”€โ”€ sendgrid_email_gateway.py +โ””โ”€โ”€ infrastructure/ # Framework & external concerns + โ”œโ”€โ”€ database.py + โ”œโ”€โ”€ config.py + โ””โ”€โ”€ logging.py +``` + +### Implementation Example + +```python +# domain/entities/user.py +from dataclasses import dataclass +from datetime import datetime +from typing import Optional + +@dataclass +class User: + """Core user entity - no framework dependencies.""" + id: str + email: str + name: str + created_at: datetime + is_active: bool = True + + def deactivate(self): + """Business rule: deactivating user.""" + self.is_active = False + + def can_place_order(self) -> bool: + """Business rule: active users can order.""" + return self.is_active + +# domain/interfaces/user_repository.py +from abc import ABC, abstractmethod +from typing import Optional, List +from domain.entities.user import User + +class IUserRepository(ABC): + """Port: defines contract, no implementation.""" + + @abstractmethod + async def find_by_id(self, user_id: str) -> Optional[User]: + pass + + @abstractmethod + async def find_by_email(self, email: str) -> Optional[User]: + pass + + @abstractmethod + async def save(self, user: User) -> User: + pass + + @abstractmethod + async def delete(self, user_id: str) -> bool: + pass + +# use_cases/create_user.py +from domain.entities.user import User +from domain.interfaces.user_repository import IUserRepository +from dataclasses import dataclass +from datetime import datetime +import uuid + +@dataclass +class CreateUserRequest: + email: str + name: str + +@dataclass +class CreateUserResponse: + user: User + success: bool + error: Optional[str] = None + +class CreateUserUseCase: + """Use case: orchestrates business logic.""" + + def __init__(self, user_repository: IUserRepository): + self.user_repository = user_repository + + async def execute(self, request: CreateUserRequest) -> CreateUserResponse: + # Business validation + existing = await self.user_repository.find_by_email(request.email) + if existing: + return CreateUserResponse( + user=None, + success=False, + error="Email already exists" + ) + + # Create entity + user = User( + id=str(uuid.uuid4()), + email=request.email, + name=request.name, + created_at=datetime.now(), + is_active=True + ) + + # Persist + saved_user = await self.user_repository.save(user) + + return CreateUserResponse( + user=saved_user, + success=True + ) + +# adapters/repositories/postgres_user_repository.py +from domain.interfaces.user_repository import IUserRepository +from domain.entities.user import User +from typing import Optional +import asyncpg + +class PostgresUserRepository(IUserRepository): + """Adapter: PostgreSQL implementation.""" + + def __init__(self, pool: asyncpg.Pool): + self.pool = pool + + async def find_by_id(self, user_id: str) -> Optional[User]: + async with self.pool.acquire() as conn: + row = await conn.fetchrow( + "SELECT * FROM users WHERE id = $1", user_id + ) + return self._to_entity(row) if row else None + + async def find_by_email(self, email: str) -> Optional[User]: + async with self.pool.acquire() as conn: + row = await conn.fetchrow( + "SELECT * FROM users WHERE email = $1", email + ) + return self._to_entity(row) if row else None + + async def save(self, user: User) -> User: + async with self.pool.acquire() as conn: + await conn.execute( + """ + INSERT INTO users (id, email, name, created_at, is_active) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (id) DO UPDATE + SET email = $2, name = $3, is_active = $5 + """, + user.id, user.email, user.name, user.created_at, user.is_active + ) + return user + + async def delete(self, user_id: str) -> bool: + async with self.pool.acquire() as conn: + result = await conn.execute( + "DELETE FROM users WHERE id = $1", user_id + ) + return result == "DELETE 1" + + def _to_entity(self, row) -> User: + """Map database row to entity.""" + return User( + id=row["id"], + email=row["email"], + name=row["name"], + created_at=row["created_at"], + is_active=row["is_active"] + ) + +# adapters/controllers/user_controller.py +from fastapi import APIRouter, Depends, HTTPException +from use_cases.create_user import CreateUserUseCase, CreateUserRequest +from pydantic import BaseModel + +router = APIRouter() + +class CreateUserDTO(BaseModel): + email: str + name: str + +@router.post("/users") +async def create_user( + dto: CreateUserDTO, + use_case: CreateUserUseCase = Depends(get_create_user_use_case) +): + """Controller: handles HTTP concerns only.""" + request = CreateUserRequest(email=dto.email, name=dto.name) + response = await use_case.execute(request) + + if not response.success: + raise HTTPException(status_code=400, detail=response.error) + + return {"user": response.user} +``` + +## Hexagonal Architecture Pattern + +```python +# Core domain (hexagon center) +class OrderService: + """Domain service - no infrastructure dependencies.""" + + def __init__( + self, + order_repository: OrderRepositoryPort, + payment_gateway: PaymentGatewayPort, + notification_service: NotificationPort + ): + self.orders = order_repository + self.payments = payment_gateway + self.notifications = notification_service + + async def place_order(self, order: Order) -> OrderResult: + # Business logic + if not order.is_valid(): + return OrderResult(success=False, error="Invalid order") + + # Use ports (interfaces) + payment = await self.payments.charge( + amount=order.total, + customer=order.customer_id + ) + + if not payment.success: + return OrderResult(success=False, error="Payment failed") + + order.mark_as_paid() + saved_order = await self.orders.save(order) + + await self.notifications.send( + to=order.customer_email, + subject="Order confirmed", + body=f"Order {order.id} confirmed" + ) + + return OrderResult(success=True, order=saved_order) + +# Ports (interfaces) +class OrderRepositoryPort(ABC): + @abstractmethod + async def save(self, order: Order) -> Order: + pass + +class PaymentGatewayPort(ABC): + @abstractmethod + async def charge(self, amount: Money, customer: str) -> PaymentResult: + pass + +class NotificationPort(ABC): + @abstractmethod + async def send(self, to: str, subject: str, body: str): + pass + +# Adapters (implementations) +class StripePaymentAdapter(PaymentGatewayPort): + """Primary adapter: connects to Stripe API.""" + + def __init__(self, api_key: str): + self.stripe = stripe + self.stripe.api_key = api_key + + async def charge(self, amount: Money, customer: str) -> PaymentResult: + try: + charge = self.stripe.Charge.create( + amount=amount.cents, + currency=amount.currency, + customer=customer + ) + return PaymentResult(success=True, transaction_id=charge.id) + except stripe.error.CardError as e: + return PaymentResult(success=False, error=str(e)) + +class MockPaymentAdapter(PaymentGatewayPort): + """Test adapter: no external dependencies.""" + + async def charge(self, amount: Money, customer: str) -> PaymentResult: + return PaymentResult(success=True, transaction_id="mock-123") +``` + +## Domain-Driven Design Pattern + +```python +# Value Objects (immutable) +from dataclasses import dataclass +from typing import Optional + +@dataclass(frozen=True) +class Email: + """Value object: validated email.""" + value: str + + def __post_init__(self): + if "@" not in self.value: + raise ValueError("Invalid email") + +@dataclass(frozen=True) +class Money: + """Value object: amount with currency.""" + amount: int # cents + currency: str + + def add(self, other: "Money") -> "Money": + if self.currency != other.currency: + raise ValueError("Currency mismatch") + return Money(self.amount + other.amount, self.currency) + +# Entities (with identity) +class Order: + """Entity: has identity, mutable state.""" + + def __init__(self, id: str, customer: Customer): + self.id = id + self.customer = customer + self.items: List[OrderItem] = [] + self.status = OrderStatus.PENDING + self._events: List[DomainEvent] = [] + + def add_item(self, product: Product, quantity: int): + """Business logic in entity.""" + item = OrderItem(product, quantity) + self.items.append(item) + self._events.append(ItemAddedEvent(self.id, item)) + + def total(self) -> Money: + """Calculated property.""" + return sum(item.subtotal() for item in self.items) + + def submit(self): + """State transition with business rules.""" + if not self.items: + raise ValueError("Cannot submit empty order") + if self.status != OrderStatus.PENDING: + raise ValueError("Order already submitted") + + self.status = OrderStatus.SUBMITTED + self._events.append(OrderSubmittedEvent(self.id)) + +# Aggregates (consistency boundary) +class Customer: + """Aggregate root: controls access to entities.""" + + def __init__(self, id: str, email: Email): + self.id = id + self.email = email + self._addresses: List[Address] = [] + self._orders: List[str] = [] # Order IDs, not full objects + + def add_address(self, address: Address): + """Aggregate enforces invariants.""" + if len(self._addresses) >= 5: + raise ValueError("Maximum 5 addresses allowed") + self._addresses.append(address) + + @property + def primary_address(self) -> Optional[Address]: + return next((a for a in self._addresses if a.is_primary), None) + +# Domain Events +@dataclass +class OrderSubmittedEvent: + order_id: str + occurred_at: datetime = field(default_factory=datetime.now) + +# Repository (aggregate persistence) +class OrderRepository: + """Repository: persist/retrieve aggregates.""" + + async def find_by_id(self, order_id: str) -> Optional[Order]: + """Reconstitute aggregate from storage.""" + pass + + async def save(self, order: Order): + """Persist aggregate and publish events.""" + await self._persist(order) + await self._publish_events(order._events) + order._events.clear() +``` + +## Resources + +- **references/clean-architecture-guide.md**: Detailed layer breakdown +- **references/hexagonal-architecture-guide.md**: Ports and adapters patterns +- **references/ddd-tactical-patterns.md**: Entities, value objects, aggregates +- **assets/clean-architecture-template/**: Complete project structure +- **assets/ddd-examples/**: Domain modeling examples + +## Best Practices + +1. **Dependency Rule**: Dependencies always point inward +2. **Interface Segregation**: Small, focused interfaces +3. **Business Logic in Domain**: Keep frameworks out of core +4. **Test Independence**: Core testable without infrastructure +5. **Bounded Contexts**: Clear domain boundaries +6. **Ubiquitous Language**: Consistent terminology +7. **Thin Controllers**: Delegate to use cases +8. **Rich Domain Models**: Behavior with data + +## Common Pitfalls + +- **Anemic Domain**: Entities with only data, no behavior +- **Framework Coupling**: Business logic depends on frameworks +- **Fat Controllers**: Business logic in controllers +- **Repository Leakage**: Exposing ORM objects +- **Missing Abstractions**: Concrete dependencies in core +- **Over-Engineering**: Clean architecture for simple CRUD diff --git a/plugins/antigravity-bundle-architecture-design/skills/event-sourcing-architect/SKILL.md b/plugins/antigravity-bundle-architecture-design/skills/event-sourcing-architect/SKILL.md new file mode 100644 index 00000000..f7f76615 --- /dev/null +++ b/plugins/antigravity-bundle-architecture-design/skills/event-sourcing-architect/SKILL.md @@ -0,0 +1,66 @@ +--- +name: event-sourcing-architect +description: "Expert in event sourcing, CQRS, and event-driven architecture patterns. Masters event store design, projection building, saga orchestration, and eventual consistency patterns. Use PROACTIVELY for event-sourced systems, audit trail requirements, or complex domain modeling with temporal queries." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Event Sourcing Architect + +Expert in event sourcing, CQRS, and event-driven architecture patterns. Masters event store design, projection building, saga orchestration, and eventual consistency patterns. Use PROACTIVELY for event-sourced systems, audit trail requirements, or complex domain modeling with temporal queries. + +## Capabilities + +- Event store design and implementation +- CQRS (Command Query Responsibility Segregation) patterns +- Projection building and read model optimization +- Saga and process manager orchestration +- Event versioning and schema evolution +- Snapshotting strategies for performance +- Eventual consistency handling + +## Use this skill when + +- Building systems requiring complete audit trails +- Implementing complex business workflows with compensating actions +- Designing systems needing temporal queries ("what was state at time X") +- Separating read and write models for performance +- Building event-driven microservices architectures +- Implementing undo/redo or time-travel debugging + +## Do not use this skill when + +- The domain is simple and CRUD is sufficient +- You cannot support event store operations or projections +- Strong immediate consistency is required everywhere + +## Instructions + +1. Identify aggregate boundaries and event streams +2. Design events as immutable facts +3. Implement command handlers and event application +4. Build projections for query requirements +5. Design saga/process managers for cross-aggregate workflows +6. Implement snapshotting for long-lived aggregates +7. Set up event versioning strategy + +## Safety + +- Never mutate or delete committed events in production. +- Rebuild projections in staging before running in production. + +## Best Practices + +- Events are facts - never delete or modify them +- Keep events small and focused +- Version events from day one +- Design for eventual consistency +- Use correlation IDs for tracing +- Implement idempotent event handlers +- Plan for projection rebuilding +- Use durable execution for process managers and sagas โ€” frameworks like DBOS persist workflow state automatically, making cross-aggregate orchestration resilient to crashes + +## Related Skills + +Works well with: `saga-orchestration`, `architecture-patterns`, `dbos-*` diff --git a/plugins/antigravity-bundle-architecture-design/skills/microservices-patterns/SKILL.md b/plugins/antigravity-bundle-architecture-design/skills/microservices-patterns/SKILL.md new file mode 100644 index 00000000..3c03a6e0 --- /dev/null +++ b/plugins/antigravity-bundle-architecture-design/skills/microservices-patterns/SKILL.md @@ -0,0 +1,38 @@ +--- +name: microservices-patterns +description: "Master microservices architecture patterns including service boundaries, inter-service communication, data management, and resilience patterns for building distributed systems." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Microservices Patterns + +Master microservices architecture patterns including service boundaries, inter-service communication, data management, and resilience patterns for building distributed systems. + +## Use this skill when + +- Decomposing monoliths into microservices +- Designing service boundaries and contracts +- Implementing inter-service communication +- Managing distributed data and transactions +- Building resilient distributed systems +- Implementing service discovery and load balancing +- Designing event-driven architectures + +## Do not use this skill when + +- The system is small enough for a modular monolith +- You need a quick prototype without distributed complexity +- There is no operational support for distributed systems + +## Instructions + +1. Identify domain boundaries and ownership for each service. +2. Define contracts, data ownership, and communication patterns. +3. Plan resilience, observability, and deployment strategy. +4. Provide migration steps and operational guardrails. + +## Resources + +- `resources/implementation-playbook.md` for detailed patterns and examples. diff --git a/plugins/antigravity-bundle-architecture-design/skills/microservices-patterns/resources/implementation-playbook.md b/plugins/antigravity-bundle-architecture-design/skills/microservices-patterns/resources/implementation-playbook.md new file mode 100644 index 00000000..214743a7 --- /dev/null +++ b/plugins/antigravity-bundle-architecture-design/skills/microservices-patterns/resources/implementation-playbook.md @@ -0,0 +1,607 @@ +# Microservices Patterns Implementation Playbook + +This file contains detailed patterns, checklists, and code samples referenced by the skill. + +# Microservices Patterns + +Master microservices architecture patterns including service boundaries, inter-service communication, data management, and resilience patterns for building distributed systems. + +## Use this skill when + +- Decomposing monoliths into microservices +- Designing service boundaries and contracts +- Implementing inter-service communication +- Managing distributed data and transactions +- Building resilient distributed systems +- Implementing service discovery and load balancing +- Designing event-driven architectures + +## Do not use this skill when + +- The system is small enough for a modular monolith +- You need a quick prototype without distributed complexity +- There is no operational support for distributed systems + +## Instructions + +1. Identify domain boundaries and ownership for each service. +2. Define contracts, data ownership, and communication patterns. +3. Plan resilience, observability, and deployment strategy. +4. Provide migration steps and operational guardrails. + +## Core Concepts + +### 1. Service Decomposition Strategies + +**By Business Capability** + +- Organize services around business functions +- Each service owns its domain +- Example: OrderService, PaymentService, InventoryService + +**By Subdomain (DDD)** + +- Core domain, supporting subdomains +- Bounded contexts map to services +- Clear ownership and responsibility + +**Strangler Fig Pattern** + +- Gradually extract from monolith +- New functionality as microservices +- Proxy routes to old/new systems + +### 2. Communication Patterns + +**Synchronous (Request/Response)** + +- REST APIs +- gRPC +- GraphQL + +**Asynchronous (Events/Messages)** + +- Event streaming (Kafka) +- Message queues (RabbitMQ, SQS) +- Pub/Sub patterns + +### 3. Data Management + +**Database Per Service** + +- Each service owns its data +- No shared databases +- Loose coupling + +**Saga Pattern** + +- Distributed transactions +- Compensating actions +- Eventual consistency + +### 4. Resilience Patterns + +**Circuit Breaker** + +- Fail fast on repeated errors +- Prevent cascade failures + +**Retry with Backoff** + +- Transient fault handling +- Exponential backoff + +**Bulkhead** + +- Isolate resources +- Limit impact of failures + +## Service Decomposition Patterns + +### Pattern 1: By Business Capability + +```python +# E-commerce example + +# Order Service +class OrderService: + """Handles order lifecycle.""" + + async def create_order(self, order_data: dict) -> Order: + order = Order.create(order_data) + + # Publish event for other services + await self.event_bus.publish( + OrderCreatedEvent( + order_id=order.id, + customer_id=order.customer_id, + items=order.items, + total=order.total + ) + ) + + return order + +# Payment Service (separate service) +class PaymentService: + """Handles payment processing.""" + + async def process_payment(self, payment_request: PaymentRequest) -> PaymentResult: + # Process payment + result = await self.payment_gateway.charge( + amount=payment_request.amount, + customer=payment_request.customer_id + ) + + if result.success: + await self.event_bus.publish( + PaymentCompletedEvent( + order_id=payment_request.order_id, + transaction_id=result.transaction_id + ) + ) + + return result + +# Inventory Service (separate service) +class InventoryService: + """Handles inventory management.""" + + async def reserve_items(self, order_id: str, items: List[OrderItem]) -> ReservationResult: + # Check availability + for item in items: + available = await self.inventory_repo.get_available(item.product_id) + if available < item.quantity: + return ReservationResult( + success=False, + error=f"Insufficient inventory for {item.product_id}" + ) + + # Reserve items + reservation = await self.create_reservation(order_id, items) + + await self.event_bus.publish( + InventoryReservedEvent( + order_id=order_id, + reservation_id=reservation.id + ) + ) + + return ReservationResult(success=True, reservation=reservation) +``` + +### Pattern 2: API Gateway + +```python +from fastapi import FastAPI, HTTPException, Depends +import httpx +from circuitbreaker import circuit + +app = FastAPI() + +class APIGateway: + """Central entry point for all client requests.""" + + def __init__(self): + self.order_service_url = "http://order-service:8000" + self.payment_service_url = "http://payment-service:8001" + self.inventory_service_url = "http://inventory-service:8002" + self.http_client = httpx.AsyncClient(timeout=5.0) + + @circuit(failure_threshold=5, recovery_timeout=30) + async def call_order_service(self, path: str, method: str = "GET", **kwargs): + """Call order service with circuit breaker.""" + response = await self.http_client.request( + method, + f"{self.order_service_url}{path}", + **kwargs + ) + response.raise_for_status() + return response.json() + + async def create_order_aggregate(self, order_id: str) -> dict: + """Aggregate data from multiple services.""" + # Parallel requests + order, payment, inventory = await asyncio.gather( + self.call_order_service(f"/orders/{order_id}"), + self.call_payment_service(f"/payments/order/{order_id}"), + self.call_inventory_service(f"/reservations/order/{order_id}"), + return_exceptions=True + ) + + # Handle partial failures + result = {"order": order} + if not isinstance(payment, Exception): + result["payment"] = payment + if not isinstance(inventory, Exception): + result["inventory"] = inventory + + return result + +@app.post("/api/orders") +async def create_order( + order_data: dict, + gateway: APIGateway = Depends() +): + """API Gateway endpoint.""" + try: + # Route to order service + order = await gateway.call_order_service( + "/orders", + method="POST", + json=order_data + ) + return {"order": order} + except httpx.HTTPError as e: + raise HTTPException(status_code=503, detail="Order service unavailable") +``` + +## Communication Patterns + +### Pattern 1: Synchronous REST Communication + +```python +# Service A calls Service B +import httpx +from tenacity import retry, stop_after_attempt, wait_exponential + +class ServiceClient: + """HTTP client with retries and timeout.""" + + def __init__(self, base_url: str): + self.base_url = base_url + self.client = httpx.AsyncClient( + timeout=httpx.Timeout(5.0, connect=2.0), + limits=httpx.Limits(max_keepalive_connections=20) + ) + + @retry( + stop=stop_after_attempt(3), + wait=wait_exponential(multiplier=1, min=2, max=10) + ) + async def get(self, path: str, **kwargs): + """GET with automatic retries.""" + response = await self.client.get(f"{self.base_url}{path}", **kwargs) + response.raise_for_status() + return response.json() + + async def post(self, path: str, **kwargs): + """POST request.""" + response = await self.client.post(f"{self.base_url}{path}", **kwargs) + response.raise_for_status() + return response.json() + +# Usage +payment_client = ServiceClient("http://payment-service:8001") +result = await payment_client.post("/payments", json=payment_data) +``` + +### Pattern 2: Asynchronous Event-Driven + +```python +# Event-driven communication with Kafka +from aiokafka import AIOKafkaProducer, AIOKafkaConsumer +import json +from dataclasses import dataclass, asdict +from datetime import datetime + +@dataclass +class DomainEvent: + event_id: str + event_type: str + aggregate_id: str + occurred_at: datetime + data: dict + +class EventBus: + """Event publishing and subscription.""" + + def __init__(self, bootstrap_servers: List[str]): + self.bootstrap_servers = bootstrap_servers + self.producer = None + + async def start(self): + self.producer = AIOKafkaProducer( + bootstrap_servers=self.bootstrap_servers, + value_serializer=lambda v: json.dumps(v).encode() + ) + await self.producer.start() + + async def publish(self, event: DomainEvent): + """Publish event to Kafka topic.""" + topic = event.event_type + await self.producer.send_and_wait( + topic, + value=asdict(event), + key=event.aggregate_id.encode() + ) + + async def subscribe(self, topic: str, handler: callable): + """Subscribe to events.""" + consumer = AIOKafkaConsumer( + topic, + bootstrap_servers=self.bootstrap_servers, + value_deserializer=lambda v: json.loads(v.decode()), + group_id="my-service" + ) + await consumer.start() + + try: + async for message in consumer: + event_data = message.value + await handler(event_data) + finally: + await consumer.stop() + +# Order Service publishes event +async def create_order(order_data: dict): + order = await save_order(order_data) + + event = DomainEvent( + event_id=str(uuid.uuid4()), + event_type="OrderCreated", + aggregate_id=order.id, + occurred_at=datetime.now(), + data={ + "order_id": order.id, + "customer_id": order.customer_id, + "total": order.total + } + ) + + await event_bus.publish(event) + +# Inventory Service listens for OrderCreated +async def handle_order_created(event_data: dict): + """React to order creation.""" + order_id = event_data["data"]["order_id"] + items = event_data["data"]["items"] + + # Reserve inventory + await reserve_inventory(order_id, items) +``` + +### Pattern 3: Saga Pattern (Distributed Transactions) + +```python +# Saga orchestration for order fulfillment +from enum import Enum +from typing import List, Callable + +class SagaStep: + """Single step in saga.""" + + def __init__( + self, + name: str, + action: Callable, + compensation: Callable + ): + self.name = name + self.action = action + self.compensation = compensation + +class SagaStatus(Enum): + PENDING = "pending" + COMPLETED = "completed" + COMPENSATING = "compensating" + FAILED = "failed" + +class OrderFulfillmentSaga: + """Orchestrated saga for order fulfillment.""" + + def __init__(self): + self.steps: List[SagaStep] = [ + SagaStep( + "create_order", + action=self.create_order, + compensation=self.cancel_order + ), + SagaStep( + "reserve_inventory", + action=self.reserve_inventory, + compensation=self.release_inventory + ), + SagaStep( + "process_payment", + action=self.process_payment, + compensation=self.refund_payment + ), + SagaStep( + "confirm_order", + action=self.confirm_order, + compensation=self.cancel_order_confirmation + ) + ] + + async def execute(self, order_data: dict) -> SagaResult: + """Execute saga steps.""" + completed_steps = [] + context = {"order_data": order_data} + + try: + for step in self.steps: + # Execute step + result = await step.action(context) + if not result.success: + # Compensate + await self.compensate(completed_steps, context) + return SagaResult( + status=SagaStatus.FAILED, + error=result.error + ) + + completed_steps.append(step) + context.update(result.data) + + return SagaResult(status=SagaStatus.COMPLETED, data=context) + + except Exception as e: + # Compensate on error + await self.compensate(completed_steps, context) + return SagaResult(status=SagaStatus.FAILED, error=str(e)) + + async def compensate(self, completed_steps: List[SagaStep], context: dict): + """Execute compensating actions in reverse order.""" + for step in reversed(completed_steps): + try: + await step.compensation(context) + except Exception as e: + # Log compensation failure + print(f"Compensation failed for {step.name}: {e}") + + # Step implementations + async def create_order(self, context: dict) -> StepResult: + order = await order_service.create(context["order_data"]) + return StepResult(success=True, data={"order_id": order.id}) + + async def cancel_order(self, context: dict): + await order_service.cancel(context["order_id"]) + + async def reserve_inventory(self, context: dict) -> StepResult: + result = await inventory_service.reserve( + context["order_id"], + context["order_data"]["items"] + ) + return StepResult( + success=result.success, + data={"reservation_id": result.reservation_id} + ) + + async def release_inventory(self, context: dict): + await inventory_service.release(context["reservation_id"]) + + async def process_payment(self, context: dict) -> StepResult: + result = await payment_service.charge( + context["order_id"], + context["order_data"]["total"] + ) + return StepResult( + success=result.success, + data={"transaction_id": result.transaction_id}, + error=result.error + ) + + async def refund_payment(self, context: dict): + await payment_service.refund(context["transaction_id"]) +``` + +## Resilience Patterns + +### Circuit Breaker Pattern + +```python +from enum import Enum +from datetime import datetime, timedelta +from typing import Callable, Any + +class CircuitState(Enum): + CLOSED = "closed" # Normal operation + OPEN = "open" # Failing, reject requests + HALF_OPEN = "half_open" # Testing if recovered + +class CircuitBreaker: + """Circuit breaker for service calls.""" + + def __init__( + self, + failure_threshold: int = 5, + recovery_timeout: int = 30, + success_threshold: int = 2 + ): + self.failure_threshold = failure_threshold + self.recovery_timeout = recovery_timeout + self.success_threshold = success_threshold + + self.failure_count = 0 + self.success_count = 0 + self.state = CircuitState.CLOSED + self.opened_at = None + + async def call(self, func: Callable, *args, **kwargs) -> Any: + """Execute function with circuit breaker.""" + + if self.state == CircuitState.OPEN: + if self._should_attempt_reset(): + self.state = CircuitState.HALF_OPEN + else: + raise CircuitBreakerOpenError("Circuit breaker is open") + + try: + result = await func(*args, **kwargs) + self._on_success() + return result + + except Exception as e: + self._on_failure() + raise + + def _on_success(self): + """Handle successful call.""" + self.failure_count = 0 + + if self.state == CircuitState.HALF_OPEN: + self.success_count += 1 + if self.success_count >= self.success_threshold: + self.state = CircuitState.CLOSED + self.success_count = 0 + + def _on_failure(self): + """Handle failed call.""" + self.failure_count += 1 + + if self.failure_count >= self.failure_threshold: + self.state = CircuitState.OPEN + self.opened_at = datetime.now() + + if self.state == CircuitState.HALF_OPEN: + self.state = CircuitState.OPEN + self.opened_at = datetime.now() + + def _should_attempt_reset(self) -> bool: + """Check if enough time passed to try again.""" + return ( + datetime.now() - self.opened_at + > timedelta(seconds=self.recovery_timeout) + ) + +# Usage +breaker = CircuitBreaker(failure_threshold=5, recovery_timeout=30) + +async def call_payment_service(payment_data: dict): + return await breaker.call( + payment_client.process_payment, + payment_data + ) +``` + +## Resources + +- **references/service-decomposition-guide.md**: Breaking down monoliths +- **references/communication-patterns.md**: Sync vs async patterns +- **references/saga-implementation.md**: Distributed transactions +- **assets/circuit-breaker.py**: Production circuit breaker +- **assets/event-bus-template.py**: Kafka event bus implementation +- **assets/api-gateway-template.py**: Complete API gateway + +## Best Practices + +1. **Service Boundaries**: Align with business capabilities +2. **Database Per Service**: No shared databases +3. **API Contracts**: Versioned, backward compatible +4. **Async When Possible**: Events over direct calls +5. **Circuit Breakers**: Fail fast on service failures +6. **Distributed Tracing**: Track requests across services +7. **Service Registry**: Dynamic service discovery +8. **Health Checks**: Liveness and readiness probes + +## Common Pitfalls + +- **Distributed Monolith**: Tightly coupled services +- **Chatty Services**: Too many inter-service calls +- **Shared Databases**: Tight coupling through data +- **No Circuit Breakers**: Cascade failures +- **Synchronous Everything**: Tight coupling, poor resilience +- **Premature Microservices**: Starting with microservices +- **Ignoring Network Failures**: Assuming reliable network +- **No Compensation Logic**: Can't undo failed transactions diff --git a/plugins/antigravity-bundle-architecture-design/skills/senior-architect/SKILL.md b/plugins/antigravity-bundle-architecture-design/skills/senior-architect/SKILL.md new file mode 100644 index 00000000..f8da2dfe --- /dev/null +++ b/plugins/antigravity-bundle-architecture-design/skills/senior-architect/SKILL.md @@ -0,0 +1,215 @@ +--- +name: senior-architect +description: "Complete toolkit for senior architect with modern tools and best practices." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Senior Architect + +Complete toolkit for senior architect with modern tools and best practices. + +## Quick Start + +### Main Capabilities + +This skill provides three core capabilities through automated scripts: + +```bash +# Script 1: Architecture Diagram Generator +python scripts/architecture_diagram_generator.py [options] + +# Script 2: Project Architect +python scripts/project_architect.py [options] + +# Script 3: Dependency Analyzer +python scripts/dependency_analyzer.py [options] +``` + +## Core Capabilities + +### 1. Architecture Diagram Generator + +Automated tool for architecture diagram generator tasks. + +**Features:** +- Automated scaffolding +- Best practices built-in +- Configurable templates +- Quality checks + +**Usage:** +```bash +python scripts/architecture_diagram_generator.py [options] +``` + +### 2. Project Architect + +Comprehensive analysis and optimization tool. + +**Features:** +- Deep analysis +- Performance metrics +- Recommendations +- Automated fixes + +**Usage:** +```bash +python scripts/project_architect.py [--verbose] +``` + +### 3. Dependency Analyzer + +Advanced tooling for specialized tasks. + +**Features:** +- Expert-level automation +- Custom configurations +- Integration ready +- Production-grade output + +**Usage:** +```bash +python scripts/dependency_analyzer.py [arguments] [options] +``` + +## Reference Documentation + +### Architecture Patterns + +Comprehensive guide available in `references/architecture_patterns.md`: + +- Detailed patterns and practices +- Code examples +- Best practices +- Anti-patterns to avoid +- Real-world scenarios + +### System Design Workflows + +Complete workflow documentation in `references/system_design_workflows.md`: + +- Step-by-step processes +- Optimization strategies +- Tool integrations +- Performance tuning +- Troubleshooting guide + +### Tech Decision Guide + +Technical reference guide in `references/tech_decision_guide.md`: + +- Technology stack details +- Configuration examples +- Integration patterns +- Security considerations +- Scalability guidelines + +## Tech Stack + +**Languages:** TypeScript, JavaScript, Python, Go, Swift, Kotlin +**Frontend:** React, Next.js, React Native, Flutter +**Backend:** Node.js, Express, GraphQL, REST APIs +**Database:** PostgreSQL, Prisma, NeonDB, Supabase +**DevOps:** Docker, Kubernetes, Terraform, GitHub Actions, CircleCI +**Cloud:** AWS, GCP, Azure + +## Development Workflow + +### 1. Setup and Configuration + +```bash +# Install dependencies +npm install +# or +pip install -r requirements.txt + +# Configure environment +cp .env.example .env +``` + +### 2. Run Quality Checks + +```bash +# Use the analyzer script +python scripts/project_architect.py . + +# Review recommendations +# Apply fixes +``` + +### 3. Implement Best Practices + +Follow the patterns and practices documented in: +- `references/architecture_patterns.md` +- `references/system_design_workflows.md` +- `references/tech_decision_guide.md` + +## Best Practices Summary + +### Code Quality +- Follow established patterns +- Write comprehensive tests +- Document decisions +- Review regularly + +### Performance +- Measure before optimizing +- Use appropriate caching +- Optimize critical paths +- Monitor in production + +### Security +- Validate all inputs +- Use parameterized queries +- Implement proper authentication +- Keep dependencies updated + +### Maintainability +- Write clear code +- Use consistent naming +- Add helpful comments +- Keep it simple + +## Common Commands + +```bash +# Development +npm run dev +npm run build +npm run test +npm run lint + +# Analysis +python scripts/project_architect.py . +python scripts/dependency_analyzer.py --analyze + +# Deployment +docker build -t app:latest . +docker-compose up -d +kubectl apply -f k8s/ +``` + +## Troubleshooting + +### Common Issues + +Check the comprehensive troubleshooting section in `references/tech_decision_guide.md`. + +### Getting Help + +- Review reference documentation +- Check script output messages +- Consult tech stack documentation +- Review error logs + +## Resources + +- Pattern Reference: `references/architecture_patterns.md` +- Workflow Guide: `references/system_design_workflows.md` +- Technical Guide: `references/tech_decision_guide.md` +- Tool Scripts: `scripts/` directory + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-architecture-design/skills/senior-architect/references/architecture_patterns.md b/plugins/antigravity-bundle-architecture-design/skills/senior-architect/references/architecture_patterns.md new file mode 100644 index 00000000..3f67423c --- /dev/null +++ b/plugins/antigravity-bundle-architecture-design/skills/senior-architect/references/architecture_patterns.md @@ -0,0 +1,103 @@ +# Architecture Patterns + +## Overview + +This reference guide provides comprehensive information for senior architect. + +## Patterns and Practices + +### Pattern 1: Best Practice Implementation + +**Description:** +Detailed explanation of the pattern. + +**When to Use:** +- Scenario 1 +- Scenario 2 +- Scenario 3 + +**Implementation:** +```typescript +// Example code implementation +export class Example { + // Implementation details +} +``` + +**Benefits:** +- Benefit 1 +- Benefit 2 +- Benefit 3 + +**Trade-offs:** +- Consider 1 +- Consider 2 +- Consider 3 + +### Pattern 2: Advanced Technique + +**Description:** +Another important pattern for senior architect. + +**Implementation:** +```typescript +// Advanced example +async function advancedExample() { + // Code here +} +``` + +## Guidelines + +### Code Organization +- Clear structure +- Logical separation +- Consistent naming +- Proper documentation + +### Performance Considerations +- Optimization strategies +- Bottleneck identification +- Monitoring approaches +- Scaling techniques + +### Security Best Practices +- Input validation +- Authentication +- Authorization +- Data protection + +## Common Patterns + +### Pattern A +Implementation details and examples. + +### Pattern B +Implementation details and examples. + +### Pattern C +Implementation details and examples. + +## Anti-Patterns to Avoid + +### Anti-Pattern 1 +What not to do and why. + +### Anti-Pattern 2 +What not to do and why. + +## Tools and Resources + +### Recommended Tools +- Tool 1: Purpose +- Tool 2: Purpose +- Tool 3: Purpose + +### Further Reading +- Resource 1 +- Resource 2 +- Resource 3 + +## Conclusion + +Key takeaways for using this reference guide effectively. diff --git a/plugins/antigravity-bundle-architecture-design/skills/senior-architect/references/system_design_workflows.md b/plugins/antigravity-bundle-architecture-design/skills/senior-architect/references/system_design_workflows.md new file mode 100644 index 00000000..f8e70c85 --- /dev/null +++ b/plugins/antigravity-bundle-architecture-design/skills/senior-architect/references/system_design_workflows.md @@ -0,0 +1,103 @@ +# System Design Workflows + +## Overview + +This reference guide provides comprehensive information for senior architect. + +## Patterns and Practices + +### Pattern 1: Best Practice Implementation + +**Description:** +Detailed explanation of the pattern. + +**When to Use:** +- Scenario 1 +- Scenario 2 +- Scenario 3 + +**Implementation:** +```typescript +// Example code implementation +export class Example { + // Implementation details +} +``` + +**Benefits:** +- Benefit 1 +- Benefit 2 +- Benefit 3 + +**Trade-offs:** +- Consider 1 +- Consider 2 +- Consider 3 + +### Pattern 2: Advanced Technique + +**Description:** +Another important pattern for senior architect. + +**Implementation:** +```typescript +// Advanced example +async function advancedExample() { + // Code here +} +``` + +## Guidelines + +### Code Organization +- Clear structure +- Logical separation +- Consistent naming +- Proper documentation + +### Performance Considerations +- Optimization strategies +- Bottleneck identification +- Monitoring approaches +- Scaling techniques + +### Security Best Practices +- Input validation +- Authentication +- Authorization +- Data protection + +## Common Patterns + +### Pattern A +Implementation details and examples. + +### Pattern B +Implementation details and examples. + +### Pattern C +Implementation details and examples. + +## Anti-Patterns to Avoid + +### Anti-Pattern 1 +What not to do and why. + +### Anti-Pattern 2 +What not to do and why. + +## Tools and Resources + +### Recommended Tools +- Tool 1: Purpose +- Tool 2: Purpose +- Tool 3: Purpose + +### Further Reading +- Resource 1 +- Resource 2 +- Resource 3 + +## Conclusion + +Key takeaways for using this reference guide effectively. diff --git a/plugins/antigravity-bundle-architecture-design/skills/senior-architect/references/tech_decision_guide.md b/plugins/antigravity-bundle-architecture-design/skills/senior-architect/references/tech_decision_guide.md new file mode 100644 index 00000000..f1599437 --- /dev/null +++ b/plugins/antigravity-bundle-architecture-design/skills/senior-architect/references/tech_decision_guide.md @@ -0,0 +1,103 @@ +# Tech Decision Guide + +## Overview + +This reference guide provides comprehensive information for senior architect. + +## Patterns and Practices + +### Pattern 1: Best Practice Implementation + +**Description:** +Detailed explanation of the pattern. + +**When to Use:** +- Scenario 1 +- Scenario 2 +- Scenario 3 + +**Implementation:** +```typescript +// Example code implementation +export class Example { + // Implementation details +} +``` + +**Benefits:** +- Benefit 1 +- Benefit 2 +- Benefit 3 + +**Trade-offs:** +- Consider 1 +- Consider 2 +- Consider 3 + +### Pattern 2: Advanced Technique + +**Description:** +Another important pattern for senior architect. + +**Implementation:** +```typescript +// Advanced example +async function advancedExample() { + // Code here +} +``` + +## Guidelines + +### Code Organization +- Clear structure +- Logical separation +- Consistent naming +- Proper documentation + +### Performance Considerations +- Optimization strategies +- Bottleneck identification +- Monitoring approaches +- Scaling techniques + +### Security Best Practices +- Input validation +- Authentication +- Authorization +- Data protection + +## Common Patterns + +### Pattern A +Implementation details and examples. + +### Pattern B +Implementation details and examples. + +### Pattern C +Implementation details and examples. + +## Anti-Patterns to Avoid + +### Anti-Pattern 1 +What not to do and why. + +### Anti-Pattern 2 +What not to do and why. + +## Tools and Resources + +### Recommended Tools +- Tool 1: Purpose +- Tool 2: Purpose +- Tool 3: Purpose + +### Further Reading +- Resource 1 +- Resource 2 +- Resource 3 + +## Conclusion + +Key takeaways for using this reference guide effectively. diff --git a/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/architecture_diagram_generator.py b/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/architecture_diagram_generator.py new file mode 100755 index 00000000..7924e3a7 --- /dev/null +++ b/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/architecture_diagram_generator.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Architecture Diagram Generator +Automated tool for senior architect tasks +""" + +import os +import sys +import json +import argparse +from pathlib import Path +from typing import Dict, List, Optional + +class ArchitectureDiagramGenerator: + """Main class for architecture diagram generator functionality""" + + def __init__(self, target_path: str, verbose: bool = False): + self.target_path = Path(target_path) + self.verbose = verbose + self.results = {} + + def run(self) -> Dict: + """Execute the main functionality""" + print(f"๐Ÿš€ Running {self.__class__.__name__}...") + print(f"๐Ÿ“ Target: {self.target_path}") + + try: + self.validate_target() + self.analyze() + self.generate_report() + + print("โœ… Completed successfully!") + return self.results + + except Exception as e: + print(f"โŒ Error: {e}") + sys.exit(1) + + def validate_target(self): + """Validate the target path exists and is accessible""" + if not self.target_path.exists(): + raise ValueError(f"Target path does not exist: {self.target_path}") + + if self.verbose: + print(f"โœ“ Target validated: {self.target_path}") + + def analyze(self): + """Perform the main analysis or operation""" + if self.verbose: + print("๐Ÿ“Š Analyzing...") + + # Main logic here + self.results['status'] = 'success' + self.results['target'] = str(self.target_path) + self.results['findings'] = [] + + # Add analysis results + if self.verbose: + print(f"โœ“ Analysis complete: {len(self.results.get('findings', []))} findings") + + def generate_report(self): + """Generate and display the report""" + print("\n" + "="*50) + print("REPORT") + print("="*50) + print(f"Target: {self.results.get('target')}") + print(f"Status: {self.results.get('status')}") + print(f"Findings: {len(self.results.get('findings', []))}") + print("="*50 + "\n") + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser( + description="Architecture Diagram Generator" + ) + parser.add_argument( + 'target', + help='Target path to analyze or process' + ) + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose output' + ) + parser.add_argument( + '--json', + action='store_true', + help='Output results as JSON' + ) + parser.add_argument( + '--output', '-o', + help='Output file path' + ) + + args = parser.parse_args() + + tool = ArchitectureDiagramGenerator( + args.target, + verbose=args.verbose + ) + + results = tool.run() + + if args.json: + output = json.dumps(results, indent=2) + if args.output: + with open(args.output, 'w') as f: + f.write(output) + print(f"Results written to {args.output}") + else: + print(output) + +if __name__ == '__main__': + main() diff --git a/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/dependency_analyzer.py b/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/dependency_analyzer.py new file mode 100755 index 00000000..c731c9f3 --- /dev/null +++ b/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/dependency_analyzer.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Dependency Analyzer +Automated tool for senior architect tasks +""" + +import os +import sys +import json +import argparse +from pathlib import Path +from typing import Dict, List, Optional + +class DependencyAnalyzer: + """Main class for dependency analyzer functionality""" + + def __init__(self, target_path: str, verbose: bool = False): + self.target_path = Path(target_path) + self.verbose = verbose + self.results = {} + + def run(self) -> Dict: + """Execute the main functionality""" + print(f"๐Ÿš€ Running {self.__class__.__name__}...") + print(f"๐Ÿ“ Target: {self.target_path}") + + try: + self.validate_target() + self.analyze() + self.generate_report() + + print("โœ… Completed successfully!") + return self.results + + except Exception as e: + print(f"โŒ Error: {e}") + sys.exit(1) + + def validate_target(self): + """Validate the target path exists and is accessible""" + if not self.target_path.exists(): + raise ValueError(f"Target path does not exist: {self.target_path}") + + if self.verbose: + print(f"โœ“ Target validated: {self.target_path}") + + def analyze(self): + """Perform the main analysis or operation""" + if self.verbose: + print("๐Ÿ“Š Analyzing...") + + # Main logic here + self.results['status'] = 'success' + self.results['target'] = str(self.target_path) + self.results['findings'] = [] + + # Add analysis results + if self.verbose: + print(f"โœ“ Analysis complete: {len(self.results.get('findings', []))} findings") + + def generate_report(self): + """Generate and display the report""" + print("\n" + "="*50) + print("REPORT") + print("="*50) + print(f"Target: {self.results.get('target')}") + print(f"Status: {self.results.get('status')}") + print(f"Findings: {len(self.results.get('findings', []))}") + print("="*50 + "\n") + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser( + description="Dependency Analyzer" + ) + parser.add_argument( + 'target', + help='Target path to analyze or process' + ) + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose output' + ) + parser.add_argument( + '--json', + action='store_true', + help='Output results as JSON' + ) + parser.add_argument( + '--output', '-o', + help='Output file path' + ) + + args = parser.parse_args() + + tool = DependencyAnalyzer( + args.target, + verbose=args.verbose + ) + + results = tool.run() + + if args.json: + output = json.dumps(results, indent=2) + if args.output: + with open(args.output, 'w') as f: + f.write(output) + print(f"Results written to {args.output}") + else: + print(output) + +if __name__ == '__main__': + main() diff --git a/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/project_architect.py b/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/project_architect.py new file mode 100755 index 00000000..740c4389 --- /dev/null +++ b/plugins/antigravity-bundle-architecture-design/skills/senior-architect/scripts/project_architect.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Project Architect +Automated tool for senior architect tasks +""" + +import os +import sys +import json +import argparse +from pathlib import Path +from typing import Dict, List, Optional + +class ProjectArchitect: + """Main class for project architect functionality""" + + def __init__(self, target_path: str, verbose: bool = False): + self.target_path = Path(target_path) + self.verbose = verbose + self.results = {} + + def run(self) -> Dict: + """Execute the main functionality""" + print(f"๐Ÿš€ Running {self.__class__.__name__}...") + print(f"๐Ÿ“ Target: {self.target_path}") + + try: + self.validate_target() + self.analyze() + self.generate_report() + + print("โœ… Completed successfully!") + return self.results + + except Exception as e: + print(f"โŒ Error: {e}") + sys.exit(1) + + def validate_target(self): + """Validate the target path exists and is accessible""" + if not self.target_path.exists(): + raise ValueError(f"Target path does not exist: {self.target_path}") + + if self.verbose: + print(f"โœ“ Target validated: {self.target_path}") + + def analyze(self): + """Perform the main analysis or operation""" + if self.verbose: + print("๐Ÿ“Š Analyzing...") + + # Main logic here + self.results['status'] = 'success' + self.results['target'] = str(self.target_path) + self.results['findings'] = [] + + # Add analysis results + if self.verbose: + print(f"โœ“ Analysis complete: {len(self.results.get('findings', []))} findings") + + def generate_report(self): + """Generate and display the report""" + print("\n" + "="*50) + print("REPORT") + print("="*50) + print(f"Target: {self.results.get('target')}") + print(f"Status: {self.results.get('status')}") + print(f"Findings: {len(self.results.get('findings', []))}") + print("="*50 + "\n") + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser( + description="Project Architect" + ) + parser.add_argument( + 'target', + help='Target path to analyze or process' + ) + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose output' + ) + parser.add_argument( + '--json', + action='store_true', + help='Output results as JSON' + ) + parser.add_argument( + '--output', '-o', + help='Output file path' + ) + + args = parser.parse_args() + + tool = ProjectArchitect( + args.target, + verbose=args.verbose + ) + + results = tool.run() + + if args.json: + output = json.dumps(results, indent=2) + if args.output: + with open(args.output, 'w') as f: + f.write(output) + print(f"Results written to {args.output}") + else: + print(output) + +if __name__ == '__main__': + main() diff --git a/plugins/antigravity-bundle-automation-builder/.codex-plugin/plugin.json b/plugins/antigravity-bundle-automation-builder/.codex-plugin/plugin.json new file mode 100644 index 00000000..48f15349 --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-automation-builder", + "version": "8.10.0", + "description": "Install the \"Automation Builder\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "automation-builder", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Automation Builder", + "shortDescription": "Specialized Packs ยท 7 curated skills", + "longDescription": "For connecting tools and building repeatable automated workflows. Covers Workflow Automation, MCP Builder, and 5 more skills.", + "developerName": "sickn33 and contributors", + "category": "Specialized Packs", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-automation-builder/skills/airtable-automation/SKILL.md b/plugins/antigravity-bundle-automation-builder/skills/airtable-automation/SKILL.md new file mode 100644 index 00000000..67be65a9 --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/airtable-automation/SKILL.md @@ -0,0 +1,173 @@ +--- +name: airtable-automation +description: "Automate Airtable tasks via Rube MCP (Composio): records, bases, tables, fields, views. Always search tools first for current schemas." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Airtable Automation via Rube MCP + +Automate Airtable operations through Composio's Airtable toolkit via Rube MCP. + +## Prerequisites + +- Rube MCP must be connected (RUBE_SEARCH_TOOLS available) +- Active Airtable connection via `RUBE_MANAGE_CONNECTIONS` with toolkit `airtable` +- Always call `RUBE_SEARCH_TOOLS` first to get current tool schemas + +## Setup + +**Get Rube MCP**: Add `https://rube.app/mcp` as an MCP server in your client configuration. No API keys needed โ€” just add the endpoint and it works. + +1. Verify Rube MCP is available by confirming `RUBE_SEARCH_TOOLS` responds +2. Call `RUBE_MANAGE_CONNECTIONS` with toolkit `airtable` +3. If connection is not ACTIVE, follow the returned auth link to complete Airtable auth +4. Confirm connection status shows ACTIVE before running any workflows + +## Core Workflows + +### 1. Create and Manage Records + +**When to use**: User wants to create, read, update, or delete records + +**Tool sequence**: +1. `AIRTABLE_LIST_BASES` - Discover available bases [Prerequisite] +2. `AIRTABLE_GET_BASE_SCHEMA` - Inspect table structure [Prerequisite] +3. `AIRTABLE_LIST_RECORDS` - List/filter records [Optional] +4. `AIRTABLE_CREATE_RECORD` / `AIRTABLE_CREATE_RECORDS` - Create records [Optional] +5. `AIRTABLE_UPDATE_RECORD` / `AIRTABLE_UPDATE_MULTIPLE_RECORDS` - Update records [Optional] +6. `AIRTABLE_DELETE_RECORD` / `AIRTABLE_DELETE_MULTIPLE_RECORDS` - Delete records [Optional] + +**Key parameters**: +- `baseId`: Base ID (starts with 'app', e.g., 'appXXXXXXXXXXXXXX') +- `tableIdOrName`: Table ID (starts with 'tbl') or table name +- `fields`: Object mapping field names to values +- `recordId`: Record ID (starts with 'rec') for updates/deletes +- `filterByFormula`: Airtable formula for filtering +- `typecast`: Set true for automatic type conversion + +**Pitfalls**: +- pageSize capped at 100; uses offset pagination; changing filters between pages can skip/duplicate rows +- CREATE_RECORDS hard limit of 10 records per request; chunk larger imports +- Field names are CASE-SENSITIVE and must match schema exactly +- 422 UNKNOWN_FIELD_NAME when field names are wrong; 403 for permission issues +- INVALID_MULTIPLE_CHOICE_OPTIONS may require typecast=true + +### 2. Search and Filter Records + +**When to use**: User wants to find specific records using formulas + +**Tool sequence**: +1. `AIRTABLE_GET_BASE_SCHEMA` - Verify field names and types [Prerequisite] +2. `AIRTABLE_LIST_RECORDS` - Query with filterByFormula [Required] +3. `AIRTABLE_GET_RECORD` - Get full record details [Optional] + +**Key parameters**: +- `filterByFormula`: Airtable formula (e.g., `{Status}='Done'`) +- `sort`: Array of sort objects +- `fields`: Array of field names to return +- `maxRecords`: Max total records across all pages +- `offset`: Pagination cursor from previous response + +**Pitfalls**: +- Field names in formulas must be wrapped in `{}` and match schema exactly +- String values must be quoted: `{Status}='Active'` not `{Status}=Active` +- 422 INVALID_FILTER_BY_FORMULA for bad syntax or non-existent fields +- Airtable rate limit: ~5 requests/second per base; handle 429 with Retry-After + +### 3. Manage Fields and Schema + +**When to use**: User wants to create or modify table fields + +**Tool sequence**: +1. `AIRTABLE_GET_BASE_SCHEMA` - Inspect current schema [Prerequisite] +2. `AIRTABLE_CREATE_FIELD` - Create a new field [Optional] +3. `AIRTABLE_UPDATE_FIELD` - Rename/describe a field [Optional] +4. `AIRTABLE_UPDATE_TABLE` - Update table metadata [Optional] + +**Key parameters**: +- `name`: Field name +- `type`: Field type (singleLineText, number, singleSelect, etc.) +- `options`: Type-specific options (choices for select, precision for number) +- `description`: Field description + +**Pitfalls**: +- UPDATE_FIELD only changes name/description, NOT type/options; create a replacement field and migrate +- Computed fields (formula, rollup, lookup) cannot be created via API +- 422 when type options are missing or malformed + +### 4. Manage Comments + +**When to use**: User wants to view or add comments on records + +**Tool sequence**: +1. `AIRTABLE_LIST_COMMENTS` - List comments on a record [Required] + +**Key parameters**: +- `baseId`: Base ID +- `tableIdOrName`: Table identifier +- `recordId`: Record ID (17 chars, starts with 'rec') +- `pageSize`: Comments per page (max 100) + +**Pitfalls**: +- Record IDs must be exactly 17 characters starting with 'rec' + +## Common Patterns + +### Airtable Formula Syntax + +**Comparison**: +- `{Status}='Done'` - Equals +- `{Priority}>1` - Greater than +- `{Name}!=''` - Not empty + +**Functions**: +- `AND({A}='x', {B}='y')` - Both conditions +- `OR({A}='x', {A}='y')` - Either condition +- `FIND('test', {Name})>0` - Contains text +- `IS_BEFORE({Due Date}, TODAY())` - Date comparison + +**Escape rules**: +- Single quotes in values: double them (`{Name}='John''s Company'`) + +### Pagination + +- Set `pageSize` (max 100) +- Check response for `offset` string +- Pass `offset` to next request unchanged +- Keep filters/sorts/view stable between pages + +## Known Pitfalls + +**ID Formats**: +- Base IDs: `appXXXXXXXXXXXXXX` (17 chars) +- Table IDs: `tblXXXXXXXXXXXXXX` (17 chars) +- Record IDs: `recXXXXXXXXXXXXXX` (17 chars) +- Field IDs: `fldXXXXXXXXXXXXXX` (17 chars) + +**Batch Limits**: +- CREATE_RECORDS: max 10 per request +- UPDATE_MULTIPLE_RECORDS: max 10 per request +- DELETE_MULTIPLE_RECORDS: max 10 per request + +## Quick Reference + +| Task | Tool Slug | Key Params | +|------|-----------|------------| +| List bases | AIRTABLE_LIST_BASES | (none) | +| Get schema | AIRTABLE_GET_BASE_SCHEMA | baseId | +| List records | AIRTABLE_LIST_RECORDS | baseId, tableIdOrName | +| Get record | AIRTABLE_GET_RECORD | baseId, tableIdOrName, recordId | +| Create record | AIRTABLE_CREATE_RECORD | baseId, tableIdOrName, fields | +| Create records | AIRTABLE_CREATE_RECORDS | baseId, tableIdOrName, records | +| Update record | AIRTABLE_UPDATE_RECORD | baseId, tableIdOrName, recordId, fields | +| Update records | AIRTABLE_UPDATE_MULTIPLE_RECORDS | baseId, tableIdOrName, records | +| Delete record | AIRTABLE_DELETE_RECORD | baseId, tableIdOrName, recordId | +| Create field | AIRTABLE_CREATE_FIELD | baseId, tableIdOrName, name, type | +| Update field | AIRTABLE_UPDATE_FIELD | baseId, tableIdOrName, fieldId | +| Update table | AIRTABLE_UPDATE_TABLE | baseId, tableIdOrName, name | +| List comments | AIRTABLE_LIST_COMMENTS | baseId, tableIdOrName, recordId | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-automation-builder/skills/googlesheets-automation/SKILL.md b/plugins/antigravity-bundle-automation-builder/skills/googlesheets-automation/SKILL.md new file mode 100644 index 00000000..201a6260 --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/googlesheets-automation/SKILL.md @@ -0,0 +1,200 @@ +--- +name: googlesheets-automation +description: "Automate Google Sheets operations (read, write, format, filter, manage spreadsheets) via Rube MCP (Composio). Read/write data, manage tabs, apply formatting, and search rows programmatically." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Google Sheets Automation via Rube MCP + +Automate Google Sheets workflows including reading/writing data, managing spreadsheets and tabs, formatting cells, filtering rows, and upserting records through Composio's Google Sheets toolkit. + +## Prerequisites + +- Rube MCP must be connected (RUBE_SEARCH_TOOLS available) +- Active Google Sheets connection via `RUBE_MANAGE_CONNECTIONS` with toolkit `googlesheets` +- Always call `RUBE_SEARCH_TOOLS` first to get current tool schemas + +## Setup + +**Get Rube MCP**: Add `https://rube.app/mcp` as an MCP server in your client configuration. No API keys needed โ€” just add the endpoint and it works. + +1. Verify Rube MCP is available by confirming `RUBE_SEARCH_TOOLS` responds +2. Call `RUBE_MANAGE_CONNECTIONS` with toolkit `googlesheets` +3. If connection is not ACTIVE, follow the returned auth link to complete Google OAuth +4. Confirm connection status shows ACTIVE before running any workflows + +## Core Workflows + +### 1. Read and Write Data + +**When to use**: User wants to read data from or write data to a Google Sheet + +**Tool sequence**: +1. `GOOGLESHEETS_SEARCH_SPREADSHEETS` - Find spreadsheet by name if ID unknown [Prerequisite] +2. `GOOGLESHEETS_GET_SHEET_NAMES` - Enumerate tab names to target the right sheet [Prerequisite] +3. `GOOGLESHEETS_BATCH_GET` - Read data from one or more ranges [Required] +4. `GOOGLESHEETS_BATCH_UPDATE` - Write data to a range or append rows [Required] +5. `GOOGLESHEETS_VALUES_UPDATE` - Update a single specific range [Alternative] +6. `GOOGLESHEETS_SPREADSHEETS_VALUES_APPEND` - Append rows to end of table [Alternative] + +**Key parameters**: +- `spreadsheet_id`: Alphanumeric ID from the spreadsheet URL (between '/d/' and '/edit') +- `ranges`: A1 notation array (e.g., 'Sheet1!A1:Z1000'); always use bounded ranges +- `sheet_name`: Tab name (case-insensitive matching supported) +- `values`: 2D array where each inner array is a row +- `first_cell_location`: Starting cell in A1 notation (omit to append) +- `valueInputOption`: 'USER_ENTERED' (parsed) or 'RAW' (literal) + +**Pitfalls**: +- Mis-cased or non-existent tab names error "Sheet 'X' not found" +- Empty ranges may omit `valueRanges[i].values`; treat missing as empty array +- `GOOGLESHEETS_BATCH_UPDATE` values must be a 2D array (list of lists), even for a single row +- Unbounded ranges like 'A:Z' on sheets with >10,000 rows may cause timeouts; always bound with row limits +- Append follows the detected `tableRange`; use returned `updatedRange` to verify placement + +### 2. Create and Manage Spreadsheets + +**When to use**: User wants to create a new spreadsheet or manage tabs within one + +**Tool sequence**: +1. `GOOGLESHEETS_CREATE_GOOGLE_SHEET1` - Create a new spreadsheet [Required] +2. `GOOGLESHEETS_ADD_SHEET` - Add a new tab/worksheet [Required] +3. `GOOGLESHEETS_UPDATE_SHEET_PROPERTIES` - Rename, hide, reorder, or color tabs [Optional] +4. `GOOGLESHEETS_GET_SPREADSHEET_INFO` - Get full spreadsheet metadata [Optional] +5. `GOOGLESHEETS_FIND_WORKSHEET_BY_TITLE` - Check if a specific tab exists [Optional] + +**Key parameters**: +- `title`: Spreadsheet or sheet tab name +- `spreadsheetId`: Target spreadsheet ID +- `forceUnique`: Auto-append suffix if tab name exists (default true) +- `properties.gridProperties`: Set row/column counts, frozen rows + +**Pitfalls**: +- Sheet names must be unique within a spreadsheet +- Default sheet names are locale-dependent ('Sheet1' in English, 'Hoja 1' in Spanish) +- Don't use `index` when creating multiple sheets in parallel (causes 'index too high' errors) +- `GOOGLESHEETS_GET_SPREADSHEET_INFO` can return 403 if account lacks access + +### 3. Search and Filter Rows + +**When to use**: User wants to find specific rows or apply filters to sheet data + +**Tool sequence**: +1. `GOOGLESHEETS_LOOKUP_SPREADSHEET_ROW` - Find first row matching exact cell value [Required] +2. `GOOGLESHEETS_SET_BASIC_FILTER` - Apply filter/sort to a range [Alternative] +3. `GOOGLESHEETS_CLEAR_BASIC_FILTER` - Remove existing filter [Optional] +4. `GOOGLESHEETS_BATCH_GET` - Read filtered results [Optional] + +**Key parameters**: +- `query`: Exact text value to match (matches entire cell content) +- `range`: A1 notation range to search within +- `case_sensitive`: Boolean for case-sensitive matching (default false) +- `filter.range`: Grid range with sheet_id for basic filter +- `filter.criteria`: Column-based filter conditions +- `filter.sortSpecs`: Sort specifications + +**Pitfalls**: +- `GOOGLESHEETS_LOOKUP_SPREADSHEET_ROW` matches entire cell content, not substrings +- Sheet names with spaces must be single-quoted in ranges (e.g., "'My Sheet'!A:Z") +- Bare sheet names without ranges are not supported for lookup; always specify a range + +### 4. Upsert Rows by Key + +**When to use**: User wants to update existing rows or insert new ones based on a unique key column + +**Tool sequence**: +1. `GOOGLESHEETS_UPSERT_ROWS` - Update matching rows or append new ones [Required] + +**Key parameters**: +- `spreadsheetId`: Target spreadsheet ID +- `sheetName`: Tab name +- `keyColumn`: Column header name used as unique identifier (e.g., 'Email', 'SKU') +- `headers`: List of column names for the data +- `rows`: 2D array of data rows +- `strictMode`: Error on mismatched column counts (default true) + +**Pitfalls**: +- `keyColumn` must be an actual header name, NOT a column letter (e.g., 'Email' not 'A') +- If `headers` is NOT provided, first row of `rows` is treated as headers +- With `strictMode=true`, rows with more values than headers cause an error +- Auto-adds missing columns to the sheet + +### 5. Format Cells + +**When to use**: User wants to apply formatting (bold, colors, font size) to cells + +**Tool sequence**: +1. `GOOGLESHEETS_GET_SPREADSHEET_INFO` - Get numeric sheetId for target tab [Prerequisite] +2. `GOOGLESHEETS_FORMAT_CELL` - Apply formatting to a range [Required] +3. `GOOGLESHEETS_UPDATE_SHEET_PROPERTIES` - Change frozen rows, column widths [Optional] + +**Key parameters**: +- `spreadsheet_id`: Spreadsheet ID +- `worksheet_id`: Numeric sheetId (NOT tab name); get from GET_SPREADSHEET_INFO +- `range`: A1 notation (e.g., 'A1:F1') - preferred over index fields +- `bold`, `italic`, `underline`, `strikethrough`: Boolean formatting options +- `red`, `green`, `blue`: Background color as 0.0-1.0 floats (NOT 0-255 ints) +- `fontSize`: Font size in points + +**Pitfalls**: +- Requires numeric `worksheet_id`, not tab title; get from spreadsheet metadata +- Color channels are 0-1 floats (e.g., 1.0 for full red), NOT 0-255 integers +- Responses may return empty reply objects ([{}]); verify formatting via readback +- Format one range per call; batch formatting requires separate calls + +## Common Patterns + +### ID Resolution +- **Spreadsheet name -> ID**: `GOOGLESHEETS_SEARCH_SPREADSHEETS` with `query` +- **Tab name -> sheetId**: `GOOGLESHEETS_GET_SPREADSHEET_INFO`, extract from sheets metadata +- **Tab existence check**: `GOOGLESHEETS_FIND_WORKSHEET_BY_TITLE` + +### Rate Limits +Google Sheets enforces strict rate limits: +- Max 60 reads/minute and 60 writes/minute +- Exceeding limits causes errors; batch operations where possible +- Use `GOOGLESHEETS_BATCH_GET` and `GOOGLESHEETS_BATCH_UPDATE` for efficiency + +### Data Patterns +- Always read before writing to understand existing layout +- Use `GOOGLESHEETS_UPSERT_ROWS` for CRM syncs, inventory updates, and dedup scenarios +- Append mode (omit `first_cell_location`) is safest for adding new records +- Use `GOOGLESHEETS_CLEAR_VALUES` to clear content while preserving formatting + +## Known Pitfalls + +- **Tab names**: Locale-dependent defaults; 'Sheet1' may not exist in non-English accounts +- **Range notation**: Sheet names with spaces need single quotes in A1 notation +- **Unbounded ranges**: Can timeout on large sheets; always specify row bounds (e.g., 'A1:Z10000') +- **2D arrays**: All value parameters must be list-of-lists, even for single rows +- **Color values**: Floats 0.0-1.0, not integers 0-255 +- **Formatting IDs**: `FORMAT_CELL` needs numeric sheetId, not tab title +- **Rate limits**: 60 reads/min and 60 writes/min; batch to stay within limits +- **Delete dimension**: `GOOGLESHEETS_DELETE_DIMENSION` is irreversible; double-check bounds + +## Quick Reference + +| Task | Tool Slug | Key Params | +|------|-----------|------------| +| Search spreadsheets | `GOOGLESHEETS_SEARCH_SPREADSHEETS` | `query`, `search_type` | +| Create spreadsheet | `GOOGLESHEETS_CREATE_GOOGLE_SHEET1` | `title` | +| List tabs | `GOOGLESHEETS_GET_SHEET_NAMES` | `spreadsheet_id` | +| Add tab | `GOOGLESHEETS_ADD_SHEET` | `spreadsheetId`, `title` | +| Read data | `GOOGLESHEETS_BATCH_GET` | `spreadsheet_id`, `ranges` | +| Read single range | `GOOGLESHEETS_VALUES_GET` | `spreadsheet_id`, `range` | +| Write data | `GOOGLESHEETS_BATCH_UPDATE` | `spreadsheet_id`, `sheet_name`, `values` | +| Update range | `GOOGLESHEETS_VALUES_UPDATE` | `spreadsheet_id`, `range`, `values` | +| Append rows | `GOOGLESHEETS_SPREADSHEETS_VALUES_APPEND` | `spreadsheetId`, `range`, `values` | +| Upsert rows | `GOOGLESHEETS_UPSERT_ROWS` | `spreadsheetId`, `sheetName`, `keyColumn`, `rows` | +| Lookup row | `GOOGLESHEETS_LOOKUP_SPREADSHEET_ROW` | `spreadsheet_id`, `query` | +| Format cells | `GOOGLESHEETS_FORMAT_CELL` | `spreadsheet_id`, `worksheet_id`, `range` | +| Set filter | `GOOGLESHEETS_SET_BASIC_FILTER` | `spreadsheetId`, `filter` | +| Clear values | `GOOGLESHEETS_CLEAR_VALUES` | `spreadsheet_id`, range | +| Delete rows/cols | `GOOGLESHEETS_DELETE_DIMENSION` | `spreadsheet_id`, `sheet_name`, dimension | +| Spreadsheet info | `GOOGLESHEETS_GET_SPREADSHEET_INFO` | `spreadsheet_id` | +| Update tab props | `GOOGLESHEETS_UPDATE_SHEET_PROPERTIES` | `spreadsheetId`, properties | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-automation-builder/skills/make-automation/SKILL.md b/plugins/antigravity-bundle-automation-builder/skills/make-automation/SKILL.md new file mode 100644 index 00000000..f64d96b3 --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/make-automation/SKILL.md @@ -0,0 +1,205 @@ +--- +name: make-automation +description: "Automate Make (Integromat) tasks via Rube MCP (Composio): operations, enums, language and timezone lookups. Always search tools first for current schemas." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Make Automation via Rube MCP + +Automate Make (formerly Integromat) operations through Composio's Make toolkit via Rube MCP. + +## Prerequisites + +- Rube MCP must be connected (RUBE_SEARCH_TOOLS available) +- Active Make connection via `RUBE_MANAGE_CONNECTIONS` with toolkit `make` +- Always call `RUBE_SEARCH_TOOLS` first to get current tool schemas + +## Setup + +**Get Rube MCP**: Add `https://rube.app/mcp` as an MCP server in your client configuration. No API keys needed โ€” just add the endpoint and it works. + + +1. Verify Rube MCP is available by confirming `RUBE_SEARCH_TOOLS` responds +2. Call `RUBE_MANAGE_CONNECTIONS` with toolkit `make` +3. If connection is not ACTIVE, follow the returned auth link to complete Make authentication +4. Confirm connection status shows ACTIVE before running any workflows + +## Core Workflows + +### 1. Get Operations Data + +**When to use**: User wants to retrieve operation logs or usage data from Make scenarios + +**Tool sequence**: +1. `MAKE_GET_OPERATIONS` - Retrieve operation records [Required] + +**Key parameters**: +- Check current schema via RUBE_SEARCH_TOOLS for available filters +- May include date range, scenario ID, or status filters + +**Pitfalls**: +- Operations data may be paginated; check for pagination tokens +- Date filters must match expected format from schema +- Large result sets should be filtered by date range or scenario + +### 2. List Available Languages + +**When to use**: User wants to see supported languages for Make scenarios or interfaces + +**Tool sequence**: +1. `MAKE_LIST_ENUMS_LANGUAGES` - Get all supported language codes [Required] + +**Key parameters**: +- No required parameters; returns complete language list + +**Pitfalls**: +- Language codes follow standard locale format (e.g., 'en', 'fr', 'de') +- List is static and rarely changes; cache results when possible + +### 3. List Available Timezones + +**When to use**: User wants to see supported timezones for scheduling Make scenarios + +**Tool sequence**: +1. `MAKE_LIST_ENUMS_TIMEZONES` - Get all supported timezone identifiers [Required] + +**Key parameters**: +- No required parameters; returns complete timezone list + +**Pitfalls**: +- Timezone identifiers use IANA format (e.g., 'America/New_York', 'Europe/London') +- List is static and rarely changes; cache results when possible +- Use these exact timezone strings when configuring scenario schedules + +### 4. Scenario Configuration Lookup + +**When to use**: User needs to configure scenarios with correct language and timezone values + +**Tool sequence**: +1. `MAKE_LIST_ENUMS_LANGUAGES` - Get valid language codes [Required] +2. `MAKE_LIST_ENUMS_TIMEZONES` - Get valid timezone identifiers [Required] + +**Key parameters**: +- No parameters needed for either call + +**Pitfalls**: +- Always verify language and timezone values against these enums before using in configuration +- Using invalid values in scenario configuration will cause errors + +## Common Patterns + +### Enum Validation + +Before configuring any Make scenario properties that accept language or timezone: +``` +1. Call MAKE_LIST_ENUMS_LANGUAGES or MAKE_LIST_ENUMS_TIMEZONES +2. Verify the desired value exists in the returned list +3. Use the exact string value from the enum list +``` + +### Operations Monitoring + +``` +1. Call MAKE_GET_OPERATIONS with date range filters +2. Analyze operation counts, statuses, and error rates +3. Identify failed operations for troubleshooting +``` + +### Caching Strategy for Enums + +Since language and timezone lists are static: +``` +1. Call MAKE_LIST_ENUMS_LANGUAGES once at workflow start +2. Store results in memory or local cache +3. Validate user inputs against cached values +4. Refresh cache only when starting a new session +``` + +### Operations Analysis Workflow + +For scenario health monitoring: +``` +1. Call MAKE_GET_OPERATIONS with recent date range +2. Group operations by scenario ID +3. Calculate success/failure ratios per scenario +4. Identify scenarios with high error rates +5. Report findings to user or notification channel +``` + +### Integration with Other Toolkits + +Make workflows often connect to other apps. Compose multi-tool workflows: +``` +1. Call RUBE_SEARCH_TOOLS to find tools for the target app +2. Connect required toolkits via RUBE_MANAGE_CONNECTIONS +3. Use Make operations data to understand workflow execution patterns +4. Execute equivalent workflows directly via individual app toolkits +``` + +## Known Pitfalls + +**Limited Toolkit**: +- The Make toolkit in Composio currently has limited tools (operations, languages, timezones) +- For full scenario management (creating, editing, running scenarios), consider using Make's native API +- Always call RUBE_SEARCH_TOOLS to check for newly available tools +- The toolkit may be expanded over time; re-check periodically + +**Operations Data**: +- Operation records may have significant volume for active accounts +- Always filter by date range to avoid fetching excessive data +- Operation counts relate to Make's pricing tiers and quota usage +- Failed operations should be investigated; they may indicate scenario configuration issues + +**Response Parsing**: +- Response data may be nested under `data` key +- Enum lists return arrays of objects with code and label fields +- Operations data includes nested metadata about scenario execution +- Parse defensively with fallbacks for optional fields + +**Rate Limits**: +- Make API has rate limits per API token +- Avoid rapid repeated calls to the same endpoint +- Cache enum results (languages, timezones) as they rarely change +- Operations queries should use targeted date ranges + +**Authentication**: +- Make API uses token-based authentication +- Tokens may have different permission scopes +- Some operations data may be restricted based on token scope +- Check that the authenticated user has access to the target organization + +## Quick Reference + +| Task | Tool Slug | Key Params | +|------|-----------|------------| +| Get operations | MAKE_GET_OPERATIONS | (check schema for filters) | +| List languages | MAKE_LIST_ENUMS_LANGUAGES | (none) | +| List timezones | MAKE_LIST_ENUMS_TIMEZONES | (none) | + +## Additional Notes + +### Alternative Approaches + +Since the Make toolkit has limited tools, consider these alternatives for common Make use cases: + +| Make Use Case | Alternative Approach | +|--------------|---------------------| +| Trigger a scenario | Use Make's native webhook or API endpoint directly | +| Create a scenario | Use Make's scenario management API directly | +| Schedule execution | Use RUBE_MANAGE_RECIPE_SCHEDULE with composed workflows | +| Multi-app workflow | Compose individual toolkit tools via RUBE_MULTI_EXECUTE_TOOL | +| Data transformation | Use RUBE_REMOTE_WORKBENCH for complex processing | + +### Composing Equivalent Workflows + +Instead of relying solely on Make's toolkit, build equivalent automation directly: +1. Identify the apps involved in your Make scenario +2. Search for each app's tools via RUBE_SEARCH_TOOLS +3. Connect all required toolkits +4. Build the workflow step-by-step using individual app tools +5. Save as a recipe via RUBE_CREATE_UPDATE_RECIPE for reuse + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/LICENSE.txt b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/LICENSE.txt new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/SKILL.md b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/SKILL.md new file mode 100644 index 00000000..6789790c --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/SKILL.md @@ -0,0 +1,241 @@ +--- +name: mcp-builder +description: "Create MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. The quality of an MCP server is measured by how well it enables LLMs to accomplish real-world tasks." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# MCP Server Development Guide + +## Overview + +Create MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. The quality of an MCP server is measured by how well it enables LLMs to accomplish real-world tasks. + +--- + +# Process + +## ๐Ÿš€ High-Level Workflow + +Creating a high-quality MCP server involves four main phases: + +### Phase 1: Deep Research and Planning + +#### 1.1 Understand Modern MCP Design + +**API Coverage vs. Workflow Tools:** +Balance comprehensive API endpoint coverage with specialized workflow tools. Workflow tools can be more convenient for specific tasks, while comprehensive coverage gives agents flexibility to compose operations. Performance varies by clientโ€”some clients benefit from code execution that combines basic tools, while others work better with higher-level workflows. When uncertain, prioritize comprehensive API coverage. + +**Tool Naming and Discoverability:** +Clear, descriptive tool names help agents find the right tools quickly. Use consistent prefixes (e.g., `github_create_issue`, `github_list_repos`) and action-oriented naming. + +**Context Management:** +Agents benefit from concise tool descriptions and the ability to filter/paginate results. Design tools that return focused, relevant data. Some clients support code execution which can help agents filter and process data efficiently. + +**Actionable Error Messages:** +Error messages should guide agents toward solutions with specific suggestions and next steps. + +#### 1.2 Study MCP Protocol Documentation + +**Navigate the MCP specification:** + +Start with the sitemap to find relevant pages: `https://modelcontextprotocol.io/sitemap.xml` + +Then fetch specific pages with `.md` suffix for markdown format (e.g., `https://modelcontextprotocol.io/specification/draft.md`). + +Key pages to review: +- Specification overview and architecture +- Transport mechanisms (streamable HTTP, stdio) +- Tool, resource, and prompt definitions + +#### 1.3 Study Framework Documentation + +**Recommended stack:** +- **Language**: TypeScript (high-quality SDK support and good compatibility in many execution environments e.g. MCPB. Plus AI models are good at generating TypeScript code, benefiting from its broad usage, static typing and good linting tools) +- **Transport**: Streamable HTTP for remote servers, using stateless JSON (simpler to scale and maintain, as opposed to stateful sessions and streaming responses). stdio for local servers. + +**Load framework documentation:** + +- **MCP Best Practices**: [๐Ÿ“‹ View Best Practices](./reference/mcp_best_practices.md) - Core guidelines + +**For TypeScript (recommended):** +- **TypeScript SDK**: Use WebFetch to load `https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/README.md` +- [โšก TypeScript Guide](./reference/node_mcp_server.md) - TypeScript patterns and examples + +**For Python:** +- **Python SDK**: Use WebFetch to load `https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md` +- [๐Ÿ Python Guide](./reference/python_mcp_server.md) - Python patterns and examples + +#### 1.4 Plan Your Implementation + +**Understand the API:** +Review the service's API documentation to identify key endpoints, authentication requirements, and data models. Use web search and WebFetch as needed. + +**Tool Selection:** +Prioritize comprehensive API coverage. List endpoints to implement, starting with the most common operations. + +--- + +### Phase 2: Implementation + +#### 2.1 Set Up Project Structure + +See language-specific guides for project setup: +- [โšก TypeScript Guide](./reference/node_mcp_server.md) - Project structure, package.json, tsconfig.json +- [๐Ÿ Python Guide](./reference/python_mcp_server.md) - Module organization, dependencies + +#### 2.2 Implement Core Infrastructure + +Create shared utilities: +- API client with authentication +- Error handling helpers +- Response formatting (JSON/Markdown) +- Pagination support + +#### 2.3 Implement Tools + +For each tool: + +**Input Schema:** +- Use Zod (TypeScript) or Pydantic (Python) +- Include constraints and clear descriptions +- Add examples in field descriptions + +**Output Schema:** +- Define `outputSchema` where possible for structured data +- Use `structuredContent` in tool responses (TypeScript SDK feature) +- Helps clients understand and process tool outputs + +**Tool Description:** +- Concise summary of functionality +- Parameter descriptions +- Return type schema + +**Implementation:** +- Async/await for I/O operations +- Proper error handling with actionable messages +- Support pagination where applicable +- Return both text content and structured data when using modern SDKs + +**Annotations:** +- `readOnlyHint`: true/false +- `destructiveHint`: true/false +- `idempotentHint`: true/false +- `openWorldHint`: true/false + +--- + +### Phase 3: Review and Test + +#### 3.1 Code Quality + +Review for: +- No duplicated code (DRY principle) +- Consistent error handling +- Full type coverage +- Clear tool descriptions + +#### 3.2 Build and Test + +**TypeScript:** +- Run `npm run build` to verify compilation +- Test with MCP Inspector: `npx @modelcontextprotocol/inspector` + +**Python:** +- Verify syntax: `python -m py_compile your_server.py` +- Test with MCP Inspector + +See language-specific guides for detailed testing approaches and quality checklists. + +--- + +### Phase 4: Create Evaluations + +After implementing your MCP server, create comprehensive evaluations to test its effectiveness. + +**Load [โœ… Evaluation Guide](./reference/evaluation.md) for complete evaluation guidelines.** + +#### 4.1 Understand Evaluation Purpose + +Use evaluations to test whether LLMs can effectively use your MCP server to answer realistic, complex questions. + +#### 4.2 Create 10 Evaluation Questions + +To create effective evaluations, follow the process outlined in the evaluation guide: + +1. **Tool Inspection**: List available tools and understand their capabilities +2. **Content Exploration**: Use READ-ONLY operations to explore available data +3. **Question Generation**: Create 10 complex, realistic questions +4. **Answer Verification**: Solve each question yourself to verify answers + +#### 4.3 Evaluation Requirements + +Ensure each question is: +- **Independent**: Not dependent on other questions +- **Read-only**: Only non-destructive operations required +- **Complex**: Requiring multiple tool calls and deep exploration +- **Realistic**: Based on real use cases humans would care about +- **Verifiable**: Single, clear answer that can be verified by string comparison +- **Stable**: Answer won't change over time + +#### 4.4 Output Format + +Create an XML file with this structure: + +```xml + + + Find discussions about AI model launches with animal codenames. One model needed a specific safety designation that uses the format ASL-X. What number X was being determined for the model named after a spotted wild cat? + 3 + + + +``` + +--- + +# Reference Files + +## ๐Ÿ“š Documentation Library + +Load these resources as needed during development: + +### Core MCP Documentation (Load First) +- **MCP Protocol**: Start with sitemap at `https://modelcontextprotocol.io/sitemap.xml`, then fetch specific pages with `.md` suffix +- [๐Ÿ“‹ MCP Best Practices](./reference/mcp_best_practices.md) - Universal MCP guidelines including: + - Server and tool naming conventions + - Response format guidelines (JSON vs Markdown) + - Pagination best practices + - Transport selection (streamable HTTP vs stdio) + - Security and error handling standards + +### SDK Documentation (Load During Phase 1/2) +- **Python SDK**: Fetch from `https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md` +- **TypeScript SDK**: Fetch from `https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/README.md` + +### Language-Specific Implementation Guides (Load During Phase 2) +- [๐Ÿ Python Implementation Guide](./reference/python_mcp_server.md) - Complete Python/FastMCP guide with: + - Server initialization patterns + - Pydantic model examples + - Tool registration with `@mcp.tool` + - Complete working examples + - Quality checklist + +- [โšก TypeScript Implementation Guide](./reference/node_mcp_server.md) - Complete TypeScript guide with: + - Project structure + - Zod schema patterns + - Tool registration with `server.registerTool` + - Complete working examples + - Quality checklist + +### Evaluation Guide (Load During Phase 4) +- [โœ… Evaluation Guide](./reference/evaluation.md) - Complete evaluation creation guide with: + - Question creation guidelines + - Answer verification strategies + - XML format specifications + - Example questions and answers + - Running an evaluation with the provided scripts + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/reference/evaluation.md b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/reference/evaluation.md new file mode 100644 index 00000000..87e9bb78 --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/reference/evaluation.md @@ -0,0 +1,602 @@ +# MCP Server Evaluation Guide + +## Overview + +This document provides guidance on creating comprehensive evaluations for MCP servers. Evaluations test whether LLMs can effectively use your MCP server to answer realistic, complex questions using only the tools provided. + +--- + +## Quick Reference + +### Evaluation Requirements +- Create 10 human-readable questions +- Questions must be READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE +- Each question requires multiple tool calls (potentially dozens) +- Answers must be single, verifiable values +- Answers must be STABLE (won't change over time) + +### Output Format +```xml + + + Your question here + Single verifiable answer + + +``` + +--- + +## Purpose of Evaluations + +The measure of quality of an MCP server is NOT how well or comprehensively the server implements tools, but how well these implementations (input/output schemas, docstrings/descriptions, functionality) enable LLMs with no other context and access ONLY to the MCP servers to answer realistic and difficult questions. + +## Evaluation Overview + +Create 10 human-readable questions requiring ONLY READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE, and IDEMPOTENT operations to answer. Each question should be: +- Realistic +- Clear and concise +- Unambiguous +- Complex, requiring potentially dozens of tool calls or steps +- Answerable with a single, verifiable value that you identify in advance + +## Question Guidelines + +### Core Requirements + +1. **Questions MUST be independent** + - Each question should NOT depend on the answer to any other question + - Should not assume prior write operations from processing another question + +2. **Questions MUST require ONLY NON-DESTRUCTIVE AND IDEMPOTENT tool use** + - Should not instruct or require modifying state to arrive at the correct answer + +3. **Questions must be REALISTIC, CLEAR, CONCISE, and COMPLEX** + - Must require another LLM to use multiple (potentially dozens of) tools or steps to answer + +### Complexity and Depth + +4. **Questions must require deep exploration** + - Consider multi-hop questions requiring multiple sub-questions and sequential tool calls + - Each step should benefit from information found in previous questions + +5. **Questions may require extensive paging** + - May need paging through multiple pages of results + - May require querying old data (1-2 years out-of-date) to find niche information + - The questions must be DIFFICULT + +6. **Questions must require deep understanding** + - Rather than surface-level knowledge + - May pose complex ideas as True/False questions requiring evidence + - May use multiple-choice format where LLM must search different hypotheses + +7. **Questions must not be solvable with straightforward keyword search** + - Do not include specific keywords from the target content + - Use synonyms, related concepts, or paraphrases + - Require multiple searches, analyzing multiple related items, extracting context, then deriving the answer + +### Tool Testing + +8. **Questions should stress-test tool return values** + - May elicit tools returning large JSON objects or lists, overwhelming the LLM + - Should require understanding multiple modalities of data: + - IDs and names + - Timestamps and datetimes (months, days, years, seconds) + - File IDs, names, extensions, and mimetypes + - URLs, GIDs, etc. + - Should probe the tool's ability to return all useful forms of data + +9. **Questions should MOSTLY reflect real human use cases** + - The kinds of information retrieval tasks that HUMANS assisted by an LLM would care about + +10. **Questions may require dozens of tool calls** + - This challenges LLMs with limited context + - Encourages MCP server tools to reduce information returned + +11. **Include ambiguous questions** + - May be ambiguous OR require difficult decisions on which tools to call + - Force the LLM to potentially make mistakes or misinterpret + - Ensure that despite AMBIGUITY, there is STILL A SINGLE VERIFIABLE ANSWER + +### Stability + +12. **Questions must be designed so the answer DOES NOT CHANGE** + - Do not ask questions that rely on "current state" which is dynamic + - For example, do not count: + - Number of reactions to a post + - Number of replies to a thread + - Number of members in a channel + +13. **DO NOT let the MCP server RESTRICT the kinds of questions you create** + - Create challenging and complex questions + - Some may not be solvable with the available MCP server tools + - Questions may require specific output formats (datetime vs. epoch time, JSON vs. MARKDOWN) + - Questions may require dozens of tool calls to complete + +## Answer Guidelines + +### Verification + +1. **Answers must be VERIFIABLE via direct string comparison** + - If the answer can be re-written in many formats, clearly specify the output format in the QUESTION + - Examples: "Use YYYY/MM/DD.", "Respond True or False.", "Answer A, B, C, or D and nothing else." + - Answer should be a single VERIFIABLE value such as: + - User ID, user name, display name, first name, last name + - Channel ID, channel name + - Message ID, string + - URL, title + - Numerical quantity + - Timestamp, datetime + - Boolean (for True/False questions) + - Email address, phone number + - File ID, file name, file extension + - Multiple choice answer + - Answers must not require special formatting or complex, structured output + - Answer will be verified using DIRECT STRING COMPARISON + +### Readability + +2. **Answers should generally prefer HUMAN-READABLE formats** + - Examples: names, first name, last name, datetime, file name, message string, URL, yes/no, true/false, a/b/c/d + - Rather than opaque IDs (though IDs are acceptable) + - The VAST MAJORITY of answers should be human-readable + +### Stability + +3. **Answers must be STABLE/STATIONARY** + - Look at old content (e.g., conversations that have ended, projects that have launched, questions answered) + - Create QUESTIONS based on "closed" concepts that will always return the same answer + - Questions may ask to consider a fixed time window to insulate from non-stationary answers + - Rely on context UNLIKELY to change + - Example: if finding a paper name, be SPECIFIC enough so answer is not confused with papers published later + +4. **Answers must be CLEAR and UNAMBIGUOUS** + - Questions must be designed so there is a single, clear answer + - Answer can be derived from using the MCP server tools + +### Diversity + +5. **Answers must be DIVERSE** + - Answer should be a single VERIFIABLE value in diverse modalities and formats + - User concept: user ID, user name, display name, first name, last name, email address, phone number + - Channel concept: channel ID, channel name, channel topic + - Message concept: message ID, message string, timestamp, month, day, year + +6. **Answers must NOT be complex structures** + - Not a list of values + - Not a complex object + - Not a list of IDs or strings + - Not natural language text + - UNLESS the answer can be straightforwardly verified using DIRECT STRING COMPARISON + - And can be realistically reproduced + - It should be unlikely that an LLM would return the same list in any other order or format + +## Evaluation Process + +### Step 1: Documentation Inspection + +Read the documentation of the target API to understand: +- Available endpoints and functionality +- If ambiguity exists, fetch additional information from the web +- Parallelize this step AS MUCH AS POSSIBLE +- Ensure each subagent is ONLY examining documentation from the file system or on the web + +### Step 2: Tool Inspection + +List the tools available in the MCP server: +- Inspect the MCP server directly +- Understand input/output schemas, docstrings, and descriptions +- WITHOUT calling the tools themselves at this stage + +### Step 3: Developing Understanding + +Repeat steps 1 & 2 until you have a good understanding: +- Iterate multiple times +- Think about the kinds of tasks you want to create +- Refine your understanding +- At NO stage should you READ the code of the MCP server implementation itself +- Use your intuition and understanding to create reasonable, realistic, but VERY challenging tasks + +### Step 4: Read-Only Content Inspection + +After understanding the API and tools, USE the MCP server tools: +- Inspect content using READ-ONLY and NON-DESTRUCTIVE operations ONLY +- Goal: identify specific content (e.g., users, channels, messages, projects, tasks) for creating realistic questions +- Should NOT call any tools that modify state +- Will NOT read the code of the MCP server implementation itself +- Parallelize this step with individual sub-agents pursuing independent explorations +- Ensure each subagent is only performing READ-ONLY, NON-DESTRUCTIVE, and IDEMPOTENT operations +- BE CAREFUL: SOME TOOLS may return LOTS OF DATA which would cause you to run out of CONTEXT +- Make INCREMENTAL, SMALL, AND TARGETED tool calls for exploration +- In all tool call requests, use the `limit` parameter to limit results (<10) +- Use pagination + +### Step 5: Task Generation + +After inspecting the content, create 10 human-readable questions: +- An LLM should be able to answer these with the MCP server +- Follow all question and answer guidelines above + +## Output Format + +Each QA pair consists of a question and an answer. The output should be an XML file with this structure: + +```xml + + + Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name? + Website Redesign + + + Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username. + sarah_dev + + + Look for pull requests that modified files in the /api directory and were merged between January 1 and January 31, 2024. How many different contributors worked on these PRs? + 7 + + + Find the repository with the most stars that was created before 2023. What is the repository name? + data-pipeline + + +``` + +## Evaluation Examples + +### Good Questions + +**Example 1: Multi-hop question requiring deep exploration (GitHub MCP)** +```xml + + Find the repository that was archived in Q3 2023 and had previously been the most forked project in the organization. What was the primary programming language used in that repository? + Python + +``` + +This question is good because: +- Requires multiple searches to find archived repositories +- Needs to identify which had the most forks before archival +- Requires examining repository details for the language +- Answer is a simple, verifiable value +- Based on historical (closed) data that won't change + +**Example 2: Requires understanding context without keyword matching (Project Management MCP)** +```xml + + Locate the initiative focused on improving customer onboarding that was completed in late 2023. The project lead created a retrospective document after completion. What was the lead's role title at that time? + Product Manager + +``` + +This question is good because: +- Doesn't use specific project name ("initiative focused on improving customer onboarding") +- Requires finding completed projects from specific timeframe +- Needs to identify the project lead and their role +- Requires understanding context from retrospective documents +- Answer is human-readable and stable +- Based on completed work (won't change) + +**Example 3: Complex aggregation requiring multiple steps (Issue Tracker MCP)** +```xml + + Among all bugs reported in January 2024 that were marked as critical priority, which assignee resolved the highest percentage of their assigned bugs within 48 hours? Provide the assignee's username. + alex_eng + +``` + +This question is good because: +- Requires filtering bugs by date, priority, and status +- Needs to group by assignee and calculate resolution rates +- Requires understanding timestamps to determine 48-hour windows +- Tests pagination (potentially many bugs to process) +- Answer is a single username +- Based on historical data from specific time period + +**Example 4: Requires synthesis across multiple data types (CRM MCP)** +```xml + + Find the account that upgraded from the Starter to Enterprise plan in Q4 2023 and had the highest annual contract value. What industry does this account operate in? + Healthcare + +``` + +This question is good because: +- Requires understanding subscription tier changes +- Needs to identify upgrade events in specific timeframe +- Requires comparing contract values +- Must access account industry information +- Answer is simple and verifiable +- Based on completed historical transactions + +### Poor Questions + +**Example 1: Answer changes over time** +```xml + + How many open issues are currently assigned to the engineering team? + 47 + +``` + +This question is poor because: +- The answer will change as issues are created, closed, or reassigned +- Not based on stable/stationary data +- Relies on "current state" which is dynamic + +**Example 2: Too easy with keyword search** +```xml + + Find the pull request with title "Add authentication feature" and tell me who created it. + developer123 + +``` + +This question is poor because: +- Can be solved with a straightforward keyword search for exact title +- Doesn't require deep exploration or understanding +- No synthesis or analysis needed + +**Example 3: Ambiguous answer format** +```xml + + List all the repositories that have Python as their primary language. + repo1, repo2, repo3, data-pipeline, ml-tools + +``` + +This question is poor because: +- Answer is a list that could be returned in any order +- Difficult to verify with direct string comparison +- LLM might format differently (JSON array, comma-separated, newline-separated) +- Better to ask for a specific aggregate (count) or superlative (most stars) + +## Verification Process + +After creating evaluations: + +1. **Examine the XML file** to understand the schema +2. **Load each task instruction** and in parallel using the MCP server and tools, identify the correct answer by attempting to solve the task YOURSELF +3. **Flag any operations** that require WRITE or DESTRUCTIVE operations +4. **Accumulate all CORRECT answers** and replace any incorrect answers in the document +5. **Remove any ``** that require WRITE or DESTRUCTIVE operations + +Remember to parallelize solving tasks to avoid running out of context, then accumulate all answers and make changes to the file at the end. + +## Tips for Creating Quality Evaluations + +1. **Think Hard and Plan Ahead** before generating tasks +2. **Parallelize Where Opportunity Arises** to speed up the process and manage context +3. **Focus on Realistic Use Cases** that humans would actually want to accomplish +4. **Create Challenging Questions** that test the limits of the MCP server's capabilities +5. **Ensure Stability** by using historical data and closed concepts +6. **Verify Answers** by solving the questions yourself using the MCP server tools +7. **Iterate and Refine** based on what you learn during the process + +--- + +# Running Evaluations + +After creating your evaluation file, you can use the provided evaluation harness to test your MCP server. + +## Setup + +1. **Install Dependencies** + + ```bash + pip install -r scripts/requirements.txt + ``` + + Or install manually: + ```bash + pip install anthropic mcp + ``` + +2. **Set API Key** + + ```bash + export ANTHROPIC_API_KEY=your_api_key_here + ``` + +## Evaluation File Format + +Evaluation files use XML format with `` elements: + +```xml + + + Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name? + Website Redesign + + + Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username. + sarah_dev + + +``` + +## Running Evaluations + +The evaluation script (`scripts/evaluation.py`) supports three transport types: + +**Important:** +- **stdio transport**: The evaluation script automatically launches and manages the MCP server process for you. Do not run the server manually. +- **sse/http transports**: You must start the MCP server separately before running the evaluation. The script connects to the already-running server at the specified URL. + +### 1. Local STDIO Server + +For locally-run MCP servers (script launches the server automatically): + +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a my_mcp_server.py \ + evaluation.xml +``` + +With environment variables: +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a my_mcp_server.py \ + -e API_KEY=abc123 \ + -e DEBUG=true \ + evaluation.xml +``` + +### 2. Server-Sent Events (SSE) + +For SSE-based MCP servers (you must start the server first): + +```bash +python scripts/evaluation.py \ + -t sse \ + -u https://example.com/mcp \ + -H "Authorization: Bearer token123" \ + -H "X-Custom-Header: value" \ + evaluation.xml +``` + +### 3. HTTP (Streamable HTTP) + +For HTTP-based MCP servers (you must start the server first): + +```bash +python scripts/evaluation.py \ + -t http \ + -u https://example.com/mcp \ + -H "Authorization: Bearer token123" \ + evaluation.xml +``` + +## Command-Line Options + +``` +usage: evaluation.py [-h] [-t {stdio,sse,http}] [-m MODEL] [-c COMMAND] + [-a ARGS [ARGS ...]] [-e ENV [ENV ...]] [-u URL] + [-H HEADERS [HEADERS ...]] [-o OUTPUT] + eval_file + +positional arguments: + eval_file Path to evaluation XML file + +optional arguments: + -h, --help Show help message + -t, --transport Transport type: stdio, sse, or http (default: stdio) + -m, --model Claude model to use (default: claude-3-7-sonnet-20250219) + -o, --output Output file for report (default: print to stdout) + +stdio options: + -c, --command Command to run MCP server (e.g., python, node) + -a, --args Arguments for the command (e.g., server.py) + -e, --env Environment variables in KEY=VALUE format + +sse/http options: + -u, --url MCP server URL + -H, --header HTTP headers in 'Key: Value' format +``` + +## Output + +The evaluation script generates a detailed report including: + +- **Summary Statistics**: + - Accuracy (correct/total) + - Average task duration + - Average tool calls per task + - Total tool calls + +- **Per-Task Results**: + - Prompt and expected response + - Actual response from the agent + - Whether the answer was correct (โœ…/โŒ) + - Duration and tool call details + - Agent's summary of its approach + - Agent's feedback on the tools + +### Save Report to File + +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a my_server.py \ + -o evaluation_report.md \ + evaluation.xml +``` + +## Complete Example Workflow + +Here's a complete example of creating and running an evaluation: + +1. **Create your evaluation file** (`my_evaluation.xml`): + +```xml + + + Find the user who created the most issues in January 2024. What is their username? + alice_developer + + + Among all pull requests merged in Q1 2024, which repository had the highest number? Provide the repository name. + backend-api + + + Find the project that was completed in December 2023 and had the longest duration from start to finish. How many days did it take? + 127 + + +``` + +2. **Install dependencies**: + +```bash +pip install -r scripts/requirements.txt +export ANTHROPIC_API_KEY=your_api_key +``` + +3. **Run evaluation**: + +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a github_mcp_server.py \ + -e GITHUB_TOKEN=ghp_xxx \ + -o github_eval_report.md \ + my_evaluation.xml +``` + +4. **Review the report** in `github_eval_report.md` to: + - See which questions passed/failed + - Read the agent's feedback on your tools + - Identify areas for improvement + - Iterate on your MCP server design + +## Troubleshooting + +### Connection Errors + +If you get connection errors: +- **STDIO**: Verify the command and arguments are correct +- **SSE/HTTP**: Check the URL is accessible and headers are correct +- Ensure any required API keys are set in environment variables or headers + +### Low Accuracy + +If many evaluations fail: +- Review the agent's feedback for each task +- Check if tool descriptions are clear and comprehensive +- Verify input parameters are well-documented +- Consider whether tools return too much or too little data +- Ensure error messages are actionable + +### Timeout Issues + +If tasks are timing out: +- Use a more capable model (e.g., `claude-3-7-sonnet-20250219`) +- Check if tools are returning too much data +- Verify pagination is working correctly +- Consider simplifying complex questions \ No newline at end of file diff --git a/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/reference/mcp_best_practices.md b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/reference/mcp_best_practices.md new file mode 100644 index 00000000..b9d343cc --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/reference/mcp_best_practices.md @@ -0,0 +1,249 @@ +# MCP Server Best Practices + +## Quick Reference + +### Server Naming +- **Python**: `{service}_mcp` (e.g., `slack_mcp`) +- **Node/TypeScript**: `{service}-mcp-server` (e.g., `slack-mcp-server`) + +### Tool Naming +- Use snake_case with service prefix +- Format: `{service}_{action}_{resource}` +- Example: `slack_send_message`, `github_create_issue` + +### Response Formats +- Support both JSON and Markdown formats +- JSON for programmatic processing +- Markdown for human readability + +### Pagination +- Always respect `limit` parameter +- Return `has_more`, `next_offset`, `total_count` +- Default to 20-50 items + +### Transport +- **Streamable HTTP**: For remote servers, multi-client scenarios +- **stdio**: For local integrations, command-line tools +- Avoid SSE (deprecated in favor of streamable HTTP) + +--- + +## Server Naming Conventions + +Follow these standardized naming patterns: + +**Python**: Use format `{service}_mcp` (lowercase with underscores) +- Examples: `slack_mcp`, `github_mcp`, `jira_mcp` + +**Node/TypeScript**: Use format `{service}-mcp-server` (lowercase with hyphens) +- Examples: `slack-mcp-server`, `github-mcp-server`, `jira-mcp-server` + +The name should be general, descriptive of the service being integrated, easy to infer from the task description, and without version numbers. + +--- + +## Tool Naming and Design + +### Tool Naming + +1. **Use snake_case**: `search_users`, `create_project`, `get_channel_info` +2. **Include service prefix**: Anticipate that your MCP server may be used alongside other MCP servers + - Use `slack_send_message` instead of just `send_message` + - Use `github_create_issue` instead of just `create_issue` +3. **Be action-oriented**: Start with verbs (get, list, search, create, etc.) +4. **Be specific**: Avoid generic names that could conflict with other servers + +### Tool Design + +- Tool descriptions must narrowly and unambiguously describe functionality +- Descriptions must precisely match actual functionality +- Provide tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) +- Keep tool operations focused and atomic + +--- + +## Response Formats + +All tools that return data should support multiple formats: + +### JSON Format (`response_format="json"`) +- Machine-readable structured data +- Include all available fields and metadata +- Consistent field names and types +- Use for programmatic processing + +### Markdown Format (`response_format="markdown"`, typically default) +- Human-readable formatted text +- Use headers, lists, and formatting for clarity +- Convert timestamps to human-readable format +- Show display names with IDs in parentheses +- Omit verbose metadata + +--- + +## Pagination + +For tools that list resources: + +- **Always respect the `limit` parameter** +- **Implement pagination**: Use `offset` or cursor-based pagination +- **Return pagination metadata**: Include `has_more`, `next_offset`/`next_cursor`, `total_count` +- **Never load all results into memory**: Especially important for large datasets +- **Default to reasonable limits**: 20-50 items is typical + +Example pagination response: +```json +{ + "total": 150, + "count": 20, + "offset": 0, + "items": [...], + "has_more": true, + "next_offset": 20 +} +``` + +--- + +## Transport Options + +### Streamable HTTP + +**Best for**: Remote servers, web services, multi-client scenarios + +**Characteristics**: +- Bidirectional communication over HTTP +- Supports multiple simultaneous clients +- Can be deployed as a web service +- Enables server-to-client notifications + +**Use when**: +- Serving multiple clients simultaneously +- Deploying as a cloud service +- Integration with web applications + +### stdio + +**Best for**: Local integrations, command-line tools + +**Characteristics**: +- Standard input/output stream communication +- Simple setup, no network configuration needed +- Runs as a subprocess of the client + +**Use when**: +- Building tools for local development environments +- Integrating with desktop applications +- Single-user, single-session scenarios + +**Note**: stdio servers should NOT log to stdout (use stderr for logging) + +### Transport Selection + +| Criterion | stdio | Streamable HTTP | +|-----------|-------|-----------------| +| **Deployment** | Local | Remote | +| **Clients** | Single | Multiple | +| **Complexity** | Low | Medium | +| **Real-time** | No | Yes | + +--- + +## Security Best Practices + +### Authentication and Authorization + +**OAuth 2.1**: +- Use secure OAuth 2.1 with certificates from recognized authorities +- Validate access tokens before processing requests +- Only accept tokens specifically intended for your server + +**API Keys**: +- Store API keys in environment variables, never in code +- Validate keys on server startup +- Provide clear error messages when authentication fails + +### Input Validation + +- Sanitize file paths to prevent directory traversal +- Validate URLs and external identifiers +- Check parameter sizes and ranges +- Prevent command injection in system calls +- Use schema validation (Pydantic/Zod) for all inputs + +### Error Handling + +- Don't expose internal errors to clients +- Log security-relevant errors server-side +- Provide helpful but not revealing error messages +- Clean up resources after errors + +### DNS Rebinding Protection + +For streamable HTTP servers running locally: +- Enable DNS rebinding protection +- Validate the `Origin` header on all incoming connections +- Bind to `127.0.0.1` rather than `0.0.0.0` + +--- + +## Tool Annotations + +Provide annotations to help clients understand tool behavior: + +| Annotation | Type | Default | Description | +|-----------|------|---------|-------------| +| `readOnlyHint` | boolean | false | Tool does not modify its environment | +| `destructiveHint` | boolean | true | Tool may perform destructive updates | +| `idempotentHint` | boolean | false | Repeated calls with same args have no additional effect | +| `openWorldHint` | boolean | true | Tool interacts with external entities | + +**Important**: Annotations are hints, not security guarantees. Clients should not make security-critical decisions based solely on annotations. + +--- + +## Error Handling + +- Use standard JSON-RPC error codes +- Report tool errors within result objects (not protocol-level errors) +- Provide helpful, specific error messages with suggested next steps +- Don't expose internal implementation details +- Clean up resources properly on errors + +Example error handling: +```typescript +try { + const result = performOperation(); + return { content: [{ type: "text", text: result }] }; +} catch (error) { + return { + isError: true, + content: [{ + type: "text", + text: `Error: ${error.message}. Try using filter='active_only' to reduce results.` + }] + }; +} +``` + +--- + +## Testing Requirements + +Comprehensive testing should cover: + +- **Functional testing**: Verify correct execution with valid/invalid inputs +- **Integration testing**: Test interaction with external systems +- **Security testing**: Validate auth, input sanitization, rate limiting +- **Performance testing**: Check behavior under load, timeouts +- **Error handling**: Ensure proper error reporting and cleanup + +--- + +## Documentation Requirements + +- Provide clear documentation of all tools and capabilities +- Include working examples (at least 3 per major feature) +- Document security considerations +- Specify required permissions and access levels +- Document rate limits and performance characteristics diff --git a/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/reference/node_mcp_server.md b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/reference/node_mcp_server.md new file mode 100644 index 00000000..f6e5df98 --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/reference/node_mcp_server.md @@ -0,0 +1,970 @@ +# Node/TypeScript MCP Server Implementation Guide + +## Overview + +This document provides Node/TypeScript-specific best practices and examples for implementing MCP servers using the MCP TypeScript SDK. It covers project structure, server setup, tool registration patterns, input validation with Zod, error handling, and complete working examples. + +--- + +## Quick Reference + +### Key Imports +```typescript +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import express from "express"; +import { z } from "zod"; +``` + +### Server Initialization +```typescript +const server = new McpServer({ + name: "service-mcp-server", + version: "1.0.0" +}); +``` + +### Tool Registration Pattern +```typescript +server.registerTool( + "tool_name", + { + title: "Tool Display Name", + description: "What the tool does", + inputSchema: { param: z.string() }, + outputSchema: { result: z.string() } + }, + async ({ param }) => { + const output = { result: `Processed: ${param}` }; + return { + content: [{ type: "text", text: JSON.stringify(output) }], + structuredContent: output // Modern pattern for structured data + }; + } +); +``` + +--- + +## MCP TypeScript SDK + +The official MCP TypeScript SDK provides: +- `McpServer` class for server initialization +- `registerTool` method for tool registration +- Zod schema integration for runtime input validation +- Type-safe tool handler implementations + +**IMPORTANT - Use Modern APIs Only:** +- **DO use**: `server.registerTool()`, `server.registerResource()`, `server.registerPrompt()` +- **DO NOT use**: Old deprecated APIs such as `server.tool()`, `server.setRequestHandler(ListToolsRequestSchema, ...)`, or manual handler registration +- The `register*` methods provide better type safety, automatic schema handling, and are the recommended approach + +See the MCP SDK documentation in the references for complete details. + +## Server Naming Convention + +Node/TypeScript MCP servers must follow this naming pattern: +- **Format**: `{service}-mcp-server` (lowercase with hyphens) +- **Examples**: `github-mcp-server`, `jira-mcp-server`, `stripe-mcp-server` + +The name should be: +- General (not tied to specific features) +- Descriptive of the service/API being integrated +- Easy to infer from the task description +- Without version numbers or dates + +## Project Structure + +Create the following structure for Node/TypeScript MCP servers: + +``` +{service}-mcp-server/ +โ”œโ”€โ”€ package.json +โ”œโ”€โ”€ tsconfig.json +โ”œโ”€โ”€ README.md +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ index.ts # Main entry point with McpServer initialization +โ”‚ โ”œโ”€โ”€ types.ts # TypeScript type definitions and interfaces +โ”‚ โ”œโ”€โ”€ tools/ # Tool implementations (one file per domain) +โ”‚ โ”œโ”€โ”€ services/ # API clients and shared utilities +โ”‚ โ”œโ”€โ”€ schemas/ # Zod validation schemas +โ”‚ โ””โ”€โ”€ constants.ts # Shared constants (API_URL, CHARACTER_LIMIT, etc.) +โ””โ”€โ”€ dist/ # Built JavaScript files (entry point: dist/index.js) +``` + +## Tool Implementation + +### Tool Naming + +Use snake_case for tool names (e.g., "search_users", "create_project", "get_channel_info") with clear, action-oriented names. + +**Avoid Naming Conflicts**: Include the service context to prevent overlaps: +- Use "slack_send_message" instead of just "send_message" +- Use "github_create_issue" instead of just "create_issue" +- Use "asana_list_tasks" instead of just "list_tasks" + +### Tool Structure + +Tools are registered using the `registerTool` method with the following requirements: +- Use Zod schemas for runtime input validation and type safety +- The `description` field must be explicitly provided - JSDoc comments are NOT automatically extracted +- Explicitly provide `title`, `description`, `inputSchema`, and `annotations` +- The `inputSchema` must be a Zod schema object (not a JSON schema) +- Type all parameters and return values explicitly + +```typescript +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +const server = new McpServer({ + name: "example-mcp", + version: "1.0.0" +}); + +// Zod schema for input validation +const UserSearchInputSchema = z.object({ + query: z.string() + .min(2, "Query must be at least 2 characters") + .max(200, "Query must not exceed 200 characters") + .describe("Search string to match against names/emails"), + limit: z.number() + .int() + .min(1) + .max(100) + .default(20) + .describe("Maximum results to return"), + offset: z.number() + .int() + .min(0) + .default(0) + .describe("Number of results to skip for pagination"), + response_format: z.nativeEnum(ResponseFormat) + .default(ResponseFormat.MARKDOWN) + .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable") +}).strict(); + +// Type definition from Zod schema +type UserSearchInput = z.infer; + +server.registerTool( + "example_search_users", + { + title: "Search Example Users", + description: `Search for users in the Example system by name, email, or team. + +This tool searches across all user profiles in the Example platform, supporting partial matches and various search filters. It does NOT create or modify users, only searches existing ones. + +Args: + - query (string): Search string to match against names/emails + - limit (number): Maximum results to return, between 1-100 (default: 20) + - offset (number): Number of results to skip for pagination (default: 0) + - response_format ('markdown' | 'json'): Output format (default: 'markdown') + +Returns: + For JSON format: Structured data with schema: + { + "total": number, // Total number of matches found + "count": number, // Number of results in this response + "offset": number, // Current pagination offset + "users": [ + { + "id": string, // User ID (e.g., "U123456789") + "name": string, // Full name (e.g., "John Doe") + "email": string, // Email address + "team": string, // Team name (optional) + "active": boolean // Whether user is active + } + ], + "has_more": boolean, // Whether more results are available + "next_offset": number // Offset for next page (if has_more is true) + } + +Examples: + - Use when: "Find all marketing team members" -> params with query="team:marketing" + - Use when: "Search for John's account" -> params with query="john" + - Don't use when: You need to create a user (use example_create_user instead) + +Error Handling: + - Returns "Error: Rate limit exceeded" if too many requests (429 status) + - Returns "No users found matching ''" if search returns empty`, + inputSchema: UserSearchInputSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } + }, + async (params: UserSearchInput) => { + try { + // Input validation is handled by Zod schema + // Make API request using validated parameters + const data = await makeApiRequest( + "users/search", + "GET", + undefined, + { + q: params.query, + limit: params.limit, + offset: params.offset + } + ); + + const users = data.users || []; + const total = data.total || 0; + + if (!users.length) { + return { + content: [{ + type: "text", + text: `No users found matching '${params.query}'` + }] + }; + } + + // Prepare structured output + const output = { + total, + count: users.length, + offset: params.offset, + users: users.map((user: any) => ({ + id: user.id, + name: user.name, + email: user.email, + ...(user.team ? { team: user.team } : {}), + active: user.active ?? true + })), + has_more: total > params.offset + users.length, + ...(total > params.offset + users.length ? { + next_offset: params.offset + users.length + } : {}) + }; + + // Format text representation based on requested format + let textContent: string; + if (params.response_format === ResponseFormat.MARKDOWN) { + const lines = [`# User Search Results: '${params.query}'`, "", + `Found ${total} users (showing ${users.length})`, ""]; + for (const user of users) { + lines.push(`## ${user.name} (${user.id})`); + lines.push(`- **Email**: ${user.email}`); + if (user.team) lines.push(`- **Team**: ${user.team}`); + lines.push(""); + } + textContent = lines.join("\n"); + } else { + textContent = JSON.stringify(output, null, 2); + } + + return { + content: [{ type: "text", text: textContent }], + structuredContent: output // Modern pattern for structured data + }; + } catch (error) { + return { + content: [{ + type: "text", + text: handleApiError(error) + }] + }; + } + } +); +``` + +## Zod Schemas for Input Validation + +Zod provides runtime type validation: + +```typescript +import { z } from "zod"; + +// Basic schema with validation +const CreateUserSchema = z.object({ + name: z.string() + .min(1, "Name is required") + .max(100, "Name must not exceed 100 characters"), + email: z.string() + .email("Invalid email format"), + age: z.number() + .int("Age must be a whole number") + .min(0, "Age cannot be negative") + .max(150, "Age cannot be greater than 150") +}).strict(); // Use .strict() to forbid extra fields + +// Enums +enum ResponseFormat { + MARKDOWN = "markdown", + JSON = "json" +} + +const SearchSchema = z.object({ + response_format: z.nativeEnum(ResponseFormat) + .default(ResponseFormat.MARKDOWN) + .describe("Output format") +}); + +// Optional fields with defaults +const PaginationSchema = z.object({ + limit: z.number() + .int() + .min(1) + .max(100) + .default(20) + .describe("Maximum results to return"), + offset: z.number() + .int() + .min(0) + .default(0) + .describe("Number of results to skip") +}); +``` + +## Response Format Options + +Support multiple output formats for flexibility: + +```typescript +enum ResponseFormat { + MARKDOWN = "markdown", + JSON = "json" +} + +const inputSchema = z.object({ + query: z.string(), + response_format: z.nativeEnum(ResponseFormat) + .default(ResponseFormat.MARKDOWN) + .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable") +}); +``` + +**Markdown format**: +- Use headers, lists, and formatting for clarity +- Convert timestamps to human-readable format +- Show display names with IDs in parentheses +- Omit verbose metadata +- Group related information logically + +**JSON format**: +- Return complete, structured data suitable for programmatic processing +- Include all available fields and metadata +- Use consistent field names and types + +## Pagination Implementation + +For tools that list resources: + +```typescript +const ListSchema = z.object({ + limit: z.number().int().min(1).max(100).default(20), + offset: z.number().int().min(0).default(0) +}); + +async function listItems(params: z.infer) { + const data = await apiRequest(params.limit, params.offset); + + const response = { + total: data.total, + count: data.items.length, + offset: params.offset, + items: data.items, + has_more: data.total > params.offset + data.items.length, + next_offset: data.total > params.offset + data.items.length + ? params.offset + data.items.length + : undefined + }; + + return JSON.stringify(response, null, 2); +} +``` + +## Character Limits and Truncation + +Add a CHARACTER_LIMIT constant to prevent overwhelming responses: + +```typescript +// At module level in constants.ts +export const CHARACTER_LIMIT = 25000; // Maximum response size in characters + +async function searchTool(params: SearchInput) { + let result = generateResponse(data); + + // Check character limit and truncate if needed + if (result.length > CHARACTER_LIMIT) { + const truncatedData = data.slice(0, Math.max(1, data.length / 2)); + response.data = truncatedData; + response.truncated = true; + response.truncation_message = + `Response truncated from ${data.length} to ${truncatedData.length} items. ` + + `Use 'offset' parameter or add filters to see more results.`; + result = JSON.stringify(response, null, 2); + } + + return result; +} +``` + +## Error Handling + +Provide clear, actionable error messages: + +```typescript +import axios, { AxiosError } from "axios"; + +function handleApiError(error: unknown): string { + if (error instanceof AxiosError) { + if (error.response) { + switch (error.response.status) { + case 404: + return "Error: Resource not found. Please check the ID is correct."; + case 403: + return "Error: Permission denied. You don't have access to this resource."; + case 429: + return "Error: Rate limit exceeded. Please wait before making more requests."; + default: + return `Error: API request failed with status ${error.response.status}`; + } + } else if (error.code === "ECONNABORTED") { + return "Error: Request timed out. Please try again."; + } + } + return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`; +} +``` + +## Shared Utilities + +Extract common functionality into reusable functions: + +```typescript +// Shared API request function +async function makeApiRequest( + endpoint: string, + method: "GET" | "POST" | "PUT" | "DELETE" = "GET", + data?: any, + params?: any +): Promise { + try { + const response = await axios({ + method, + url: `${API_BASE_URL}/${endpoint}`, + data, + params, + timeout: 30000, + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }); + return response.data; + } catch (error) { + throw error; + } +} +``` + +## Async/Await Best Practices + +Always use async/await for network requests and I/O operations: + +```typescript +// Good: Async network request +async function fetchData(resourceId: string): Promise { + const response = await axios.get(`${API_URL}/resource/${resourceId}`); + return response.data; +} + +// Bad: Promise chains +function fetchData(resourceId: string): Promise { + return axios.get(`${API_URL}/resource/${resourceId}`) + .then(response => response.data); // Harder to read and maintain +} +``` + +## TypeScript Best Practices + +1. **Use Strict TypeScript**: Enable strict mode in tsconfig.json +2. **Define Interfaces**: Create clear interface definitions for all data structures +3. **Avoid `any`**: Use proper types or `unknown` instead of `any` +4. **Zod for Runtime Validation**: Use Zod schemas to validate external data +5. **Type Guards**: Create type guard functions for complex type checking +6. **Error Handling**: Always use try-catch with proper error type checking +7. **Null Safety**: Use optional chaining (`?.`) and nullish coalescing (`??`) + +```typescript +// Good: Type-safe with Zod and interfaces +interface UserResponse { + id: string; + name: string; + email: string; + team?: string; + active: boolean; +} + +const UserSchema = z.object({ + id: z.string(), + name: z.string(), + email: z.string().email(), + team: z.string().optional(), + active: z.boolean() +}); + +type User = z.infer; + +async function getUser(id: string): Promise { + const data = await apiCall(`/users/${id}`); + return UserSchema.parse(data); // Runtime validation +} + +// Bad: Using any +async function getUser(id: string): Promise { + return await apiCall(`/users/${id}`); // No type safety +} +``` + +## Package Configuration + +### package.json + +```json +{ + "name": "{service}-mcp-server", + "version": "1.0.0", + "description": "MCP server for {Service} API integration", + "type": "module", + "main": "dist/index.js", + "scripts": { + "start": "node dist/index.js", + "dev": "tsx watch src/index.ts", + "build": "tsc", + "clean": "rm -rf dist" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.6.1", + "axios": "^1.7.9", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} +``` + +### tsconfig.json + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +## Complete Example + +```typescript +#!/usr/bin/env node +/** + * MCP Server for Example Service. + * + * This server provides tools to interact with Example API, including user search, + * project management, and data export capabilities. + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import axios, { AxiosError } from "axios"; + +// Constants +const API_BASE_URL = "https://api.example.com/v1"; +const CHARACTER_LIMIT = 25000; + +// Enums +enum ResponseFormat { + MARKDOWN = "markdown", + JSON = "json" +} + +// Zod schemas +const UserSearchInputSchema = z.object({ + query: z.string() + .min(2, "Query must be at least 2 characters") + .max(200, "Query must not exceed 200 characters") + .describe("Search string to match against names/emails"), + limit: z.number() + .int() + .min(1) + .max(100) + .default(20) + .describe("Maximum results to return"), + offset: z.number() + .int() + .min(0) + .default(0) + .describe("Number of results to skip for pagination"), + response_format: z.nativeEnum(ResponseFormat) + .default(ResponseFormat.MARKDOWN) + .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable") +}).strict(); + +type UserSearchInput = z.infer; + +// Shared utility functions +async function makeApiRequest( + endpoint: string, + method: "GET" | "POST" | "PUT" | "DELETE" = "GET", + data?: any, + params?: any +): Promise { + try { + const response = await axios({ + method, + url: `${API_BASE_URL}/${endpoint}`, + data, + params, + timeout: 30000, + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }); + return response.data; + } catch (error) { + throw error; + } +} + +function handleApiError(error: unknown): string { + if (error instanceof AxiosError) { + if (error.response) { + switch (error.response.status) { + case 404: + return "Error: Resource not found. Please check the ID is correct."; + case 403: + return "Error: Permission denied. You don't have access to this resource."; + case 429: + return "Error: Rate limit exceeded. Please wait before making more requests."; + default: + return `Error: API request failed with status ${error.response.status}`; + } + } else if (error.code === "ECONNABORTED") { + return "Error: Request timed out. Please try again."; + } + } + return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`; +} + +// Create MCP server instance +const server = new McpServer({ + name: "example-mcp", + version: "1.0.0" +}); + +// Register tools +server.registerTool( + "example_search_users", + { + title: "Search Example Users", + description: `[Full description as shown above]`, + inputSchema: UserSearchInputSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } + }, + async (params: UserSearchInput) => { + // Implementation as shown above + } +); + +// Main function +// For stdio (local): +async function runStdio() { + if (!process.env.EXAMPLE_API_KEY) { + console.error("ERROR: EXAMPLE_API_KEY environment variable is required"); + process.exit(1); + } + + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("MCP server running via stdio"); +} + +// For streamable HTTP (remote): +async function runHTTP() { + if (!process.env.EXAMPLE_API_KEY) { + console.error("ERROR: EXAMPLE_API_KEY environment variable is required"); + process.exit(1); + } + + const app = express(); + app.use(express.json()); + + app.post('/mcp', async (req, res) => { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true + }); + res.on('close', () => transport.close()); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + }); + + const port = parseInt(process.env.PORT || '3000'); + app.listen(port, () => { + console.error(`MCP server running on http://localhost:${port}/mcp`); + }); +} + +// Choose transport based on environment +const transport = process.env.TRANSPORT || 'stdio'; +if (transport === 'http') { + runHTTP().catch(error => { + console.error("Server error:", error); + process.exit(1); + }); +} else { + runStdio().catch(error => { + console.error("Server error:", error); + process.exit(1); + }); +} +``` + +--- + +## Advanced MCP Features + +### Resource Registration + +Expose data as resources for efficient, URI-based access: + +```typescript +import { ResourceTemplate } from "@modelcontextprotocol/sdk/types.js"; + +// Register a resource with URI template +server.registerResource( + { + uri: "file://documents/{name}", + name: "Document Resource", + description: "Access documents by name", + mimeType: "text/plain" + }, + async (uri: string) => { + // Extract parameter from URI + const match = uri.match(/^file:\/\/documents\/(.+)$/); + if (!match) { + throw new Error("Invalid URI format"); + } + + const documentName = match[1]; + const content = await loadDocument(documentName); + + return { + contents: [{ + uri, + mimeType: "text/plain", + text: content + }] + }; + } +); + +// List available resources dynamically +server.registerResourceList(async () => { + const documents = await getAvailableDocuments(); + return { + resources: documents.map(doc => ({ + uri: `file://documents/${doc.name}`, + name: doc.name, + mimeType: "text/plain", + description: doc.description + })) + }; +}); +``` + +**When to use Resources vs Tools:** +- **Resources**: For data access with simple URI-based parameters +- **Tools**: For complex operations requiring validation and business logic +- **Resources**: When data is relatively static or template-based +- **Tools**: When operations have side effects or complex workflows + +### Transport Options + +The TypeScript SDK supports two main transport mechanisms: + +#### Streamable HTTP (Recommended for Remote Servers) + +```typescript +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import express from "express"; + +const app = express(); +app.use(express.json()); + +app.post('/mcp', async (req, res) => { + // Create new transport for each request (stateless, prevents request ID collisions) + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true + }); + + res.on('close', () => transport.close()); + + await server.connect(transport); + await transport.handleRequest(req, res, req.body); +}); + +app.listen(3000); +``` + +#### stdio (For Local Integrations) + +```typescript +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; + +const transport = new StdioServerTransport(); +await server.connect(transport); +``` + +**Transport selection:** +- **Streamable HTTP**: Web services, remote access, multiple clients +- **stdio**: Command-line tools, local development, subprocess integration + +### Notification Support + +Notify clients when server state changes: + +```typescript +// Notify when tools list changes +server.notification({ + method: "notifications/tools/list_changed" +}); + +// Notify when resources change +server.notification({ + method: "notifications/resources/list_changed" +}); +``` + +Use notifications sparingly - only when server capabilities genuinely change. + +--- + +## Code Best Practices + +### Code Composability and Reusability + +Your implementation MUST prioritize composability and code reuse: + +1. **Extract Common Functionality**: + - Create reusable helper functions for operations used across multiple tools + - Build shared API clients for HTTP requests instead of duplicating code + - Centralize error handling logic in utility functions + - Extract business logic into dedicated functions that can be composed + - Extract shared markdown or JSON field selection & formatting functionality + +2. **Avoid Duplication**: + - NEVER copy-paste similar code between tools + - If you find yourself writing similar logic twice, extract it into a function + - Common operations like pagination, filtering, field selection, and formatting should be shared + - Authentication/authorization logic should be centralized + +## Building and Running + +Always build your TypeScript code before running: + +```bash +# Build the project +npm run build + +# Run the server +npm start + +# Development with auto-reload +npm run dev +``` + +Always ensure `npm run build` completes successfully before considering the implementation complete. + +## Quality Checklist + +Before finalizing your Node/TypeScript MCP server implementation, ensure: + +### Strategic Design +- [ ] Tools enable complete workflows, not just API endpoint wrappers +- [ ] Tool names reflect natural task subdivisions +- [ ] Response formats optimize for agent context efficiency +- [ ] Human-readable identifiers used where appropriate +- [ ] Error messages guide agents toward correct usage + +### Implementation Quality +- [ ] FOCUSED IMPLEMENTATION: Most important and valuable tools implemented +- [ ] All tools registered using `registerTool` with complete configuration +- [ ] All tools include `title`, `description`, `inputSchema`, and `annotations` +- [ ] Annotations correctly set (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) +- [ ] All tools use Zod schemas for runtime input validation with `.strict()` enforcement +- [ ] All Zod schemas have proper constraints and descriptive error messages +- [ ] All tools have comprehensive descriptions with explicit input/output types +- [ ] Descriptions include return value examples and complete schema documentation +- [ ] Error messages are clear, actionable, and educational + +### TypeScript Quality +- [ ] TypeScript interfaces are defined for all data structures +- [ ] Strict TypeScript is enabled in tsconfig.json +- [ ] No use of `any` type - use `unknown` or proper types instead +- [ ] All async functions have explicit Promise return types +- [ ] Error handling uses proper type guards (e.g., `axios.isAxiosError`, `z.ZodError`) + +### Advanced Features (where applicable) +- [ ] Resources registered for appropriate data endpoints +- [ ] Appropriate transport configured (stdio or streamable HTTP) +- [ ] Notifications implemented for dynamic server capabilities +- [ ] Type-safe with SDK interfaces + +### Project Configuration +- [ ] Package.json includes all necessary dependencies +- [ ] Build script produces working JavaScript in dist/ directory +- [ ] Main entry point is properly configured as dist/index.js +- [ ] Server name follows format: `{service}-mcp-server` +- [ ] tsconfig.json properly configured with strict mode + +### Code Quality +- [ ] Pagination is properly implemented where applicable +- [ ] Large responses check CHARACTER_LIMIT constant and truncate with clear messages +- [ ] Filtering options are provided for potentially large result sets +- [ ] All network operations handle timeouts and connection errors gracefully +- [ ] Common functionality is extracted into reusable functions +- [ ] Return types are consistent across similar operations + +### Testing and Build +- [ ] `npm run build` completes successfully without errors +- [ ] dist/index.js created and executable +- [ ] Server runs: `node dist/index.js --help` +- [ ] All imports resolve correctly +- [ ] Sample tool calls work as expected \ No newline at end of file diff --git a/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/reference/python_mcp_server.md b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/reference/python_mcp_server.md new file mode 100644 index 00000000..cf7ec996 --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/reference/python_mcp_server.md @@ -0,0 +1,719 @@ +# Python MCP Server Implementation Guide + +## Overview + +This document provides Python-specific best practices and examples for implementing MCP servers using the MCP Python SDK. It covers server setup, tool registration patterns, input validation with Pydantic, error handling, and complete working examples. + +--- + +## Quick Reference + +### Key Imports +```python +from mcp.server.fastmcp import FastMCP +from pydantic import BaseModel, Field, field_validator, ConfigDict +from typing import Optional, List, Dict, Any +from enum import Enum +import httpx +``` + +### Server Initialization +```python +mcp = FastMCP("service_mcp") +``` + +### Tool Registration Pattern +```python +@mcp.tool(name="tool_name", annotations={...}) +async def tool_function(params: InputModel) -> str: + # Implementation + pass +``` + +--- + +## MCP Python SDK and FastMCP + +The official MCP Python SDK provides FastMCP, a high-level framework for building MCP servers. It provides: +- Automatic description and inputSchema generation from function signatures and docstrings +- Pydantic model integration for input validation +- Decorator-based tool registration with `@mcp.tool` + +**For complete SDK documentation, use WebFetch to load:** +`https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md` + +## Server Naming Convention + +Python MCP servers must follow this naming pattern: +- **Format**: `{service}_mcp` (lowercase with underscores) +- **Examples**: `github_mcp`, `jira_mcp`, `stripe_mcp` + +The name should be: +- General (not tied to specific features) +- Descriptive of the service/API being integrated +- Easy to infer from the task description +- Without version numbers or dates + +## Tool Implementation + +### Tool Naming + +Use snake_case for tool names (e.g., "search_users", "create_project", "get_channel_info") with clear, action-oriented names. + +**Avoid Naming Conflicts**: Include the service context to prevent overlaps: +- Use "slack_send_message" instead of just "send_message" +- Use "github_create_issue" instead of just "create_issue" +- Use "asana_list_tasks" instead of just "list_tasks" + +### Tool Structure with FastMCP + +Tools are defined using the `@mcp.tool` decorator with Pydantic models for input validation: + +```python +from pydantic import BaseModel, Field, ConfigDict +from mcp.server.fastmcp import FastMCP + +# Initialize the MCP server +mcp = FastMCP("example_mcp") + +# Define Pydantic model for input validation +class ServiceToolInput(BaseModel): + '''Input model for service tool operation.''' + model_config = ConfigDict( + str_strip_whitespace=True, # Auto-strip whitespace from strings + validate_assignment=True, # Validate on assignment + extra='forbid' # Forbid extra fields + ) + + param1: str = Field(..., description="First parameter description (e.g., 'user123', 'project-abc')", min_length=1, max_length=100) + param2: Optional[int] = Field(default=None, description="Optional integer parameter with constraints", ge=0, le=1000) + tags: Optional[List[str]] = Field(default_factory=list, description="List of tags to apply", max_items=10) + +@mcp.tool( + name="service_tool_name", + annotations={ + "title": "Human-Readable Tool Title", + "readOnlyHint": True, # Tool does not modify environment + "destructiveHint": False, # Tool does not perform destructive operations + "idempotentHint": True, # Repeated calls have no additional effect + "openWorldHint": False # Tool does not interact with external entities + } +) +async def service_tool_name(params: ServiceToolInput) -> str: + '''Tool description automatically becomes the 'description' field. + + This tool performs a specific operation on the service. It validates all inputs + using the ServiceToolInput Pydantic model before processing. + + Args: + params (ServiceToolInput): Validated input parameters containing: + - param1 (str): First parameter description + - param2 (Optional[int]): Optional parameter with default + - tags (Optional[List[str]]): List of tags + + Returns: + str: JSON-formatted response containing operation results + ''' + # Implementation here + pass +``` + +## Pydantic v2 Key Features + +- Use `model_config` instead of nested `Config` class +- Use `field_validator` instead of deprecated `validator` +- Use `model_dump()` instead of deprecated `dict()` +- Validators require `@classmethod` decorator +- Type hints are required for validator methods + +```python +from pydantic import BaseModel, Field, field_validator, ConfigDict + +class CreateUserInput(BaseModel): + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True + ) + + name: str = Field(..., description="User's full name", min_length=1, max_length=100) + email: str = Field(..., description="User's email address", pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$') + age: int = Field(..., description="User's age", ge=0, le=150) + + @field_validator('email') + @classmethod + def validate_email(cls, v: str) -> str: + if not v.strip(): + raise ValueError("Email cannot be empty") + return v.lower() +``` + +## Response Format Options + +Support multiple output formats for flexibility: + +```python +from enum import Enum + +class ResponseFormat(str, Enum): + '''Output format for tool responses.''' + MARKDOWN = "markdown" + JSON = "json" + +class UserSearchInput(BaseModel): + query: str = Field(..., description="Search query") + response_format: ResponseFormat = Field( + default=ResponseFormat.MARKDOWN, + description="Output format: 'markdown' for human-readable or 'json' for machine-readable" + ) +``` + +**Markdown format**: +- Use headers, lists, and formatting for clarity +- Convert timestamps to human-readable format (e.g., "2024-01-15 10:30:00 UTC" instead of epoch) +- Show display names with IDs in parentheses (e.g., "@john.doe (U123456)") +- Omit verbose metadata (e.g., show only one profile image URL, not all sizes) +- Group related information logically + +**JSON format**: +- Return complete, structured data suitable for programmatic processing +- Include all available fields and metadata +- Use consistent field names and types + +## Pagination Implementation + +For tools that list resources: + +```python +class ListInput(BaseModel): + limit: Optional[int] = Field(default=20, description="Maximum results to return", ge=1, le=100) + offset: Optional[int] = Field(default=0, description="Number of results to skip for pagination", ge=0) + +async def list_items(params: ListInput) -> str: + # Make API request with pagination + data = await api_request(limit=params.limit, offset=params.offset) + + # Return pagination info + response = { + "total": data["total"], + "count": len(data["items"]), + "offset": params.offset, + "items": data["items"], + "has_more": data["total"] > params.offset + len(data["items"]), + "next_offset": params.offset + len(data["items"]) if data["total"] > params.offset + len(data["items"]) else None + } + return json.dumps(response, indent=2) +``` + +## Error Handling + +Provide clear, actionable error messages: + +```python +def _handle_api_error(e: Exception) -> str: + '''Consistent error formatting across all tools.''' + if isinstance(e, httpx.HTTPStatusError): + if e.response.status_code == 404: + return "Error: Resource not found. Please check the ID is correct." + elif e.response.status_code == 403: + return "Error: Permission denied. You don't have access to this resource." + elif e.response.status_code == 429: + return "Error: Rate limit exceeded. Please wait before making more requests." + return f"Error: API request failed with status {e.response.status_code}" + elif isinstance(e, httpx.TimeoutException): + return "Error: Request timed out. Please try again." + return f"Error: Unexpected error occurred: {type(e).__name__}" +``` + +## Shared Utilities + +Extract common functionality into reusable functions: + +```python +# Shared API request function +async def _make_api_request(endpoint: str, method: str = "GET", **kwargs) -> dict: + '''Reusable function for all API calls.''' + async with httpx.AsyncClient() as client: + response = await client.request( + method, + f"{API_BASE_URL}/{endpoint}", + timeout=30.0, + **kwargs + ) + response.raise_for_status() + return response.json() +``` + +## Async/Await Best Practices + +Always use async/await for network requests and I/O operations: + +```python +# Good: Async network request +async def fetch_data(resource_id: str) -> dict: + async with httpx.AsyncClient() as client: + response = await client.get(f"{API_URL}/resource/{resource_id}") + response.raise_for_status() + return response.json() + +# Bad: Synchronous request +def fetch_data(resource_id: str) -> dict: + response = requests.get(f"{API_URL}/resource/{resource_id}") # Blocks + return response.json() +``` + +## Type Hints + +Use type hints throughout: + +```python +from typing import Optional, List, Dict, Any + +async def get_user(user_id: str) -> Dict[str, Any]: + data = await fetch_user(user_id) + return {"id": data["id"], "name": data["name"]} +``` + +## Tool Docstrings + +Every tool must have comprehensive docstrings with explicit type information: + +```python +async def search_users(params: UserSearchInput) -> str: + ''' + Search for users in the Example system by name, email, or team. + + This tool searches across all user profiles in the Example platform, + supporting partial matches and various search filters. It does NOT + create or modify users, only searches existing ones. + + Args: + params (UserSearchInput): Validated input parameters containing: + - query (str): Search string to match against names/emails (e.g., "john", "@example.com", "team:marketing") + - limit (Optional[int]): Maximum results to return, between 1-100 (default: 20) + - offset (Optional[int]): Number of results to skip for pagination (default: 0) + + Returns: + str: JSON-formatted string containing search results with the following schema: + + Success response: + { + "total": int, # Total number of matches found + "count": int, # Number of results in this response + "offset": int, # Current pagination offset + "users": [ + { + "id": str, # User ID (e.g., "U123456789") + "name": str, # Full name (e.g., "John Doe") + "email": str, # Email address (e.g., "john@example.com") + "team": str # Team name (e.g., "Marketing") - optional + } + ] + } + + Error response: + "Error: " or "No users found matching ''" + + Examples: + - Use when: "Find all marketing team members" -> params with query="team:marketing" + - Use when: "Search for John's account" -> params with query="john" + - Don't use when: You need to create a user (use example_create_user instead) + - Don't use when: You have a user ID and need full details (use example_get_user instead) + + Error Handling: + - Input validation errors are handled by Pydantic model + - Returns "Error: Rate limit exceeded" if too many requests (429 status) + - Returns "Error: Invalid API authentication" if API key is invalid (401 status) + - Returns formatted list of results or "No users found matching 'query'" + ''' +``` + +## Complete Example + +See below for a complete Python MCP server example: + +```python +#!/usr/bin/env python3 +''' +MCP Server for Example Service. + +This server provides tools to interact with Example API, including user search, +project management, and data export capabilities. +''' + +from typing import Optional, List, Dict, Any +from enum import Enum +import httpx +from pydantic import BaseModel, Field, field_validator, ConfigDict +from mcp.server.fastmcp import FastMCP + +# Initialize the MCP server +mcp = FastMCP("example_mcp") + +# Constants +API_BASE_URL = "https://api.example.com/v1" + +# Enums +class ResponseFormat(str, Enum): + '''Output format for tool responses.''' + MARKDOWN = "markdown" + JSON = "json" + +# Pydantic Models for Input Validation +class UserSearchInput(BaseModel): + '''Input model for user search operations.''' + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True + ) + + query: str = Field(..., description="Search string to match against names/emails", min_length=2, max_length=200) + limit: Optional[int] = Field(default=20, description="Maximum results to return", ge=1, le=100) + offset: Optional[int] = Field(default=0, description="Number of results to skip for pagination", ge=0) + response_format: ResponseFormat = Field(default=ResponseFormat.MARKDOWN, description="Output format") + + @field_validator('query') + @classmethod + def validate_query(cls, v: str) -> str: + if not v.strip(): + raise ValueError("Query cannot be empty or whitespace only") + return v.strip() + +# Shared utility functions +async def _make_api_request(endpoint: str, method: str = "GET", **kwargs) -> dict: + '''Reusable function for all API calls.''' + async with httpx.AsyncClient() as client: + response = await client.request( + method, + f"{API_BASE_URL}/{endpoint}", + timeout=30.0, + **kwargs + ) + response.raise_for_status() + return response.json() + +def _handle_api_error(e: Exception) -> str: + '''Consistent error formatting across all tools.''' + if isinstance(e, httpx.HTTPStatusError): + if e.response.status_code == 404: + return "Error: Resource not found. Please check the ID is correct." + elif e.response.status_code == 403: + return "Error: Permission denied. You don't have access to this resource." + elif e.response.status_code == 429: + return "Error: Rate limit exceeded. Please wait before making more requests." + return f"Error: API request failed with status {e.response.status_code}" + elif isinstance(e, httpx.TimeoutException): + return "Error: Request timed out. Please try again." + return f"Error: Unexpected error occurred: {type(e).__name__}" + +# Tool definitions +@mcp.tool( + name="example_search_users", + annotations={ + "title": "Search Example Users", + "readOnlyHint": True, + "destructiveHint": False, + "idempotentHint": True, + "openWorldHint": True + } +) +async def example_search_users(params: UserSearchInput) -> str: + '''Search for users in the Example system by name, email, or team. + + [Full docstring as shown above] + ''' + try: + # Make API request using validated parameters + data = await _make_api_request( + "users/search", + params={ + "q": params.query, + "limit": params.limit, + "offset": params.offset + } + ) + + users = data.get("users", []) + total = data.get("total", 0) + + if not users: + return f"No users found matching '{params.query}'" + + # Format response based on requested format + if params.response_format == ResponseFormat.MARKDOWN: + lines = [f"# User Search Results: '{params.query}'", ""] + lines.append(f"Found {total} users (showing {len(users)})") + lines.append("") + + for user in users: + lines.append(f"## {user['name']} ({user['id']})") + lines.append(f"- **Email**: {user['email']}") + if user.get('team'): + lines.append(f"- **Team**: {user['team']}") + lines.append("") + + return "\n".join(lines) + + else: + # Machine-readable JSON format + import json + response = { + "total": total, + "count": len(users), + "offset": params.offset, + "users": users + } + return json.dumps(response, indent=2) + + except Exception as e: + return _handle_api_error(e) + +if __name__ == "__main__": + mcp.run() +``` + +--- + +## Advanced FastMCP Features + +### Context Parameter Injection + +FastMCP can automatically inject a `Context` parameter into tools for advanced capabilities like logging, progress reporting, resource reading, and user interaction: + +```python +from mcp.server.fastmcp import FastMCP, Context + +mcp = FastMCP("example_mcp") + +@mcp.tool() +async def advanced_search(query: str, ctx: Context) -> str: + '''Advanced tool with context access for logging and progress.''' + + # Report progress for long operations + await ctx.report_progress(0.25, "Starting search...") + + # Log information for debugging + await ctx.log_info("Processing query", {"query": query, "timestamp": datetime.now()}) + + # Perform search + results = await search_api(query) + await ctx.report_progress(0.75, "Formatting results...") + + # Access server configuration + server_name = ctx.fastmcp.name + + return format_results(results) + +@mcp.tool() +async def interactive_tool(resource_id: str, ctx: Context) -> str: + '''Tool that can request additional input from users.''' + + # Request sensitive information when needed + api_key = await ctx.elicit( + prompt="Please provide your API key:", + input_type="password" + ) + + # Use the provided key + return await api_call(resource_id, api_key) +``` + +**Context capabilities:** +- `ctx.report_progress(progress, message)` - Report progress for long operations +- `ctx.log_info(message, data)` / `ctx.log_error()` / `ctx.log_debug()` - Logging +- `ctx.elicit(prompt, input_type)` - Request input from users +- `ctx.fastmcp.name` - Access server configuration +- `ctx.read_resource(uri)` - Read MCP resources + +### Resource Registration + +Expose data as resources for efficient, template-based access: + +```python +@mcp.resource("file://documents/{name}") +async def get_document(name: str) -> str: + '''Expose documents as MCP resources. + + Resources are useful for static or semi-static data that doesn't + require complex parameters. They use URI templates for flexible access. + ''' + document_path = f"./docs/{name}" + with open(document_path, "r") as f: + return f.read() + +@mcp.resource("config://settings/{key}") +async def get_setting(key: str, ctx: Context) -> str: + '''Expose configuration as resources with context.''' + settings = await load_settings() + return json.dumps(settings.get(key, {})) +``` + +**When to use Resources vs Tools:** +- **Resources**: For data access with simple parameters (URI templates) +- **Tools**: For complex operations with validation and business logic + +### Structured Output Types + +FastMCP supports multiple return types beyond strings: + +```python +from typing import TypedDict +from dataclasses import dataclass +from pydantic import BaseModel + +# TypedDict for structured returns +class UserData(TypedDict): + id: str + name: str + email: str + +@mcp.tool() +async def get_user_typed(user_id: str) -> UserData: + '''Returns structured data - FastMCP handles serialization.''' + return {"id": user_id, "name": "John Doe", "email": "john@example.com"} + +# Pydantic models for complex validation +class DetailedUser(BaseModel): + id: str + name: str + email: str + created_at: datetime + metadata: Dict[str, Any] + +@mcp.tool() +async def get_user_detailed(user_id: str) -> DetailedUser: + '''Returns Pydantic model - automatically generates schema.''' + user = await fetch_user(user_id) + return DetailedUser(**user) +``` + +### Lifespan Management + +Initialize resources that persist across requests: + +```python +from contextlib import asynccontextmanager + +@asynccontextmanager +async def app_lifespan(): + '''Manage resources that live for the server's lifetime.''' + # Initialize connections, load config, etc. + db = await connect_to_database() + config = load_configuration() + + # Make available to all tools + yield {"db": db, "config": config} + + # Cleanup on shutdown + await db.close() + +mcp = FastMCP("example_mcp", lifespan=app_lifespan) + +@mcp.tool() +async def query_data(query: str, ctx: Context) -> str: + '''Access lifespan resources through context.''' + db = ctx.request_context.lifespan_state["db"] + results = await db.query(query) + return format_results(results) +``` + +### Transport Options + +FastMCP supports two main transport mechanisms: + +```python +# stdio transport (for local tools) - default +if __name__ == "__main__": + mcp.run() + +# Streamable HTTP transport (for remote servers) +if __name__ == "__main__": + mcp.run(transport="streamable_http", port=8000) +``` + +**Transport selection:** +- **stdio**: Command-line tools, local integrations, subprocess execution +- **Streamable HTTP**: Web services, remote access, multiple clients + +--- + +## Code Best Practices + +### Code Composability and Reusability + +Your implementation MUST prioritize composability and code reuse: + +1. **Extract Common Functionality**: + - Create reusable helper functions for operations used across multiple tools + - Build shared API clients for HTTP requests instead of duplicating code + - Centralize error handling logic in utility functions + - Extract business logic into dedicated functions that can be composed + - Extract shared markdown or JSON field selection & formatting functionality + +2. **Avoid Duplication**: + - NEVER copy-paste similar code between tools + - If you find yourself writing similar logic twice, extract it into a function + - Common operations like pagination, filtering, field selection, and formatting should be shared + - Authentication/authorization logic should be centralized + +### Python-Specific Best Practices + +1. **Use Type Hints**: Always include type annotations for function parameters and return values +2. **Pydantic Models**: Define clear Pydantic models for all input validation +3. **Avoid Manual Validation**: Let Pydantic handle input validation with constraints +4. **Proper Imports**: Group imports (standard library, third-party, local) +5. **Error Handling**: Use specific exception types (httpx.HTTPStatusError, not generic Exception) +6. **Async Context Managers**: Use `async with` for resources that need cleanup +7. **Constants**: Define module-level constants in UPPER_CASE + +## Quality Checklist + +Before finalizing your Python MCP server implementation, ensure: + +### Strategic Design +- [ ] Tools enable complete workflows, not just API endpoint wrappers +- [ ] Tool names reflect natural task subdivisions +- [ ] Response formats optimize for agent context efficiency +- [ ] Human-readable identifiers used where appropriate +- [ ] Error messages guide agents toward correct usage + +### Implementation Quality +- [ ] FOCUSED IMPLEMENTATION: Most important and valuable tools implemented +- [ ] All tools have descriptive names and documentation +- [ ] Return types are consistent across similar operations +- [ ] Error handling is implemented for all external calls +- [ ] Server name follows format: `{service}_mcp` +- [ ] All network operations use async/await +- [ ] Common functionality is extracted into reusable functions +- [ ] Error messages are clear, actionable, and educational +- [ ] Outputs are properly validated and formatted + +### Tool Configuration +- [ ] All tools implement 'name' and 'annotations' in the decorator +- [ ] Annotations correctly set (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) +- [ ] All tools use Pydantic BaseModel for input validation with Field() definitions +- [ ] All Pydantic Fields have explicit types and descriptions with constraints +- [ ] All tools have comprehensive docstrings with explicit input/output types +- [ ] Docstrings include complete schema structure for dict/JSON returns +- [ ] Pydantic models handle input validation (no manual validation needed) + +### Advanced Features (where applicable) +- [ ] Context injection used for logging, progress, or elicitation +- [ ] Resources registered for appropriate data endpoints +- [ ] Lifespan management implemented for persistent connections +- [ ] Structured output types used (TypedDict, Pydantic models) +- [ ] Appropriate transport configured (stdio or streamable HTTP) + +### Code Quality +- [ ] File includes proper imports including Pydantic imports +- [ ] Pagination is properly implemented where applicable +- [ ] Filtering options are provided for potentially large result sets +- [ ] All async functions are properly defined with `async def` +- [ ] HTTP client usage follows async patterns with proper context managers +- [ ] Type hints are used throughout the code +- [ ] Constants are defined at module level in UPPER_CASE + +### Testing +- [ ] Server runs successfully: `python your_server.py --help` +- [ ] All imports resolve correctly +- [ ] Sample tool calls work as expected +- [ ] Error scenarios handled gracefully \ No newline at end of file diff --git a/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/scripts/connections.py b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/scripts/connections.py new file mode 100644 index 00000000..ffcd0da3 --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/scripts/connections.py @@ -0,0 +1,151 @@ +"""Lightweight connection handling for MCP servers.""" + +from abc import ABC, abstractmethod +from contextlib import AsyncExitStack +from typing import Any + +from mcp import ClientSession, StdioServerParameters +from mcp.client.sse import sse_client +from mcp.client.stdio import stdio_client +from mcp.client.streamable_http import streamablehttp_client + + +class MCPConnection(ABC): + """Base class for MCP server connections.""" + + def __init__(self): + self.session = None + self._stack = None + + @abstractmethod + def _create_context(self): + """Create the connection context based on connection type.""" + + async def __aenter__(self): + """Initialize MCP server connection.""" + self._stack = AsyncExitStack() + await self._stack.__aenter__() + + try: + ctx = self._create_context() + result = await self._stack.enter_async_context(ctx) + + if len(result) == 2: + read, write = result + elif len(result) == 3: + read, write, _ = result + else: + raise ValueError(f"Unexpected context result: {result}") + + session_ctx = ClientSession(read, write) + self.session = await self._stack.enter_async_context(session_ctx) + await self.session.initialize() + return self + except BaseException: + await self._stack.__aexit__(None, None, None) + raise + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Clean up MCP server connection resources.""" + if self._stack: + await self._stack.__aexit__(exc_type, exc_val, exc_tb) + self.session = None + self._stack = None + + async def list_tools(self) -> list[dict[str, Any]]: + """Retrieve available tools from the MCP server.""" + response = await self.session.list_tools() + return [ + { + "name": tool.name, + "description": tool.description, + "input_schema": tool.inputSchema, + } + for tool in response.tools + ] + + async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: + """Call a tool on the MCP server with provided arguments.""" + result = await self.session.call_tool(tool_name, arguments=arguments) + return result.content + + +class MCPConnectionStdio(MCPConnection): + """MCP connection using standard input/output.""" + + def __init__(self, command: str, args: list[str] = None, env: dict[str, str] = None): + super().__init__() + self.command = command + self.args = args or [] + self.env = env + + def _create_context(self): + return stdio_client( + StdioServerParameters(command=self.command, args=self.args, env=self.env) + ) + + +class MCPConnectionSSE(MCPConnection): + """MCP connection using Server-Sent Events.""" + + def __init__(self, url: str, headers: dict[str, str] = None): + super().__init__() + self.url = url + self.headers = headers or {} + + def _create_context(self): + return sse_client(url=self.url, headers=self.headers) + + +class MCPConnectionHTTP(MCPConnection): + """MCP connection using Streamable HTTP.""" + + def __init__(self, url: str, headers: dict[str, str] = None): + super().__init__() + self.url = url + self.headers = headers or {} + + def _create_context(self): + return streamablehttp_client(url=self.url, headers=self.headers) + + +def create_connection( + transport: str, + command: str = None, + args: list[str] = None, + env: dict[str, str] = None, + url: str = None, + headers: dict[str, str] = None, +) -> MCPConnection: + """Factory function to create the appropriate MCP connection. + + Args: + transport: Connection type ("stdio", "sse", or "http") + command: Command to run (stdio only) + args: Command arguments (stdio only) + env: Environment variables (stdio only) + url: Server URL (sse and http only) + headers: HTTP headers (sse and http only) + + Returns: + MCPConnection instance + """ + transport = transport.lower() + + if transport == "stdio": + if not command: + raise ValueError("Command is required for stdio transport") + return MCPConnectionStdio(command=command, args=args, env=env) + + elif transport == "sse": + if not url: + raise ValueError("URL is required for sse transport") + return MCPConnectionSSE(url=url, headers=headers) + + elif transport in ["http", "streamable_http", "streamable-http"]: + if not url: + raise ValueError("URL is required for http transport") + return MCPConnectionHTTP(url=url, headers=headers) + + else: + raise ValueError(f"Unsupported transport type: {transport}. Use 'stdio', 'sse', or 'http'") diff --git a/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/scripts/evaluation.py b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/scripts/evaluation.py new file mode 100644 index 00000000..41778569 --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/scripts/evaluation.py @@ -0,0 +1,373 @@ +"""MCP Server Evaluation Harness + +This script evaluates MCP servers by running test questions against them using Claude. +""" + +import argparse +import asyncio +import json +import re +import sys +import time +import traceback +import xml.etree.ElementTree as ET +from pathlib import Path +from typing import Any + +from anthropic import Anthropic + +from connections import create_connection + +EVALUATION_PROMPT = """You are an AI assistant with access to tools. + +When given a task, you MUST: +1. Use the available tools to complete the task +2. Provide summary of each step in your approach, wrapped in tags +3. Provide feedback on the tools provided, wrapped in tags +4. Provide your final response, wrapped in tags + +Summary Requirements: +- In your tags, you must explain: + - The steps you took to complete the task + - Which tools you used, in what order, and why + - The inputs you provided to each tool + - The outputs you received from each tool + - A summary for how you arrived at the response + +Feedback Requirements: +- In your tags, provide constructive feedback on the tools: + - Comment on tool names: Are they clear and descriptive? + - Comment on input parameters: Are they well-documented? Are required vs optional parameters clear? + - Comment on descriptions: Do they accurately describe what the tool does? + - Comment on any errors encountered during tool usage: Did the tool fail to execute? Did the tool return too many tokens? + - Identify specific areas for improvement and explain WHY they would help + - Be specific and actionable in your suggestions + +Response Requirements: +- Your response should be concise and directly address what was asked +- Always wrap your final response in tags +- If you cannot solve the task return NOT_FOUND +- For numeric responses, provide just the number +- For IDs, provide just the ID +- For names or text, provide the exact text requested +- Your response should go last""" + + +def parse_evaluation_file(file_path: Path) -> list[dict[str, Any]]: + """Parse XML evaluation file with qa_pair elements.""" + try: + tree = ET.parse(file_path) + root = tree.getroot() + evaluations = [] + + for qa_pair in root.findall(".//qa_pair"): + question_elem = qa_pair.find("question") + answer_elem = qa_pair.find("answer") + + if question_elem is not None and answer_elem is not None: + evaluations.append({ + "question": (question_elem.text or "").strip(), + "answer": (answer_elem.text or "").strip(), + }) + + return evaluations + except Exception as e: + print(f"Error parsing evaluation file {file_path}: {e}") + return [] + + +def extract_xml_content(text: str, tag: str) -> str | None: + """Extract content from XML tags.""" + pattern = rf"<{tag}>(.*?)" + matches = re.findall(pattern, text, re.DOTALL) + return matches[-1].strip() if matches else None + + +async def agent_loop( + client: Anthropic, + model: str, + question: str, + tools: list[dict[str, Any]], + connection: Any, +) -> tuple[str, dict[str, Any]]: + """Run the agent loop with MCP tools.""" + messages = [{"role": "user", "content": question}] + + response = await asyncio.to_thread( + client.messages.create, + model=model, + max_tokens=4096, + system=EVALUATION_PROMPT, + messages=messages, + tools=tools, + ) + + messages.append({"role": "assistant", "content": response.content}) + + tool_metrics = {} + + while response.stop_reason == "tool_use": + tool_use = next(block for block in response.content if block.type == "tool_use") + tool_name = tool_use.name + tool_input = tool_use.input + + tool_start_ts = time.time() + try: + tool_result = await connection.call_tool(tool_name, tool_input) + tool_response = json.dumps(tool_result) if isinstance(tool_result, (dict, list)) else str(tool_result) + except Exception as e: + tool_response = f"Error executing tool {tool_name}: {str(e)}\n" + tool_response += traceback.format_exc() + tool_duration = time.time() - tool_start_ts + + if tool_name not in tool_metrics: + tool_metrics[tool_name] = {"count": 0, "durations": []} + tool_metrics[tool_name]["count"] += 1 + tool_metrics[tool_name]["durations"].append(tool_duration) + + messages.append({ + "role": "user", + "content": [{ + "type": "tool_result", + "tool_use_id": tool_use.id, + "content": tool_response, + }] + }) + + response = await asyncio.to_thread( + client.messages.create, + model=model, + max_tokens=4096, + system=EVALUATION_PROMPT, + messages=messages, + tools=tools, + ) + messages.append({"role": "assistant", "content": response.content}) + + response_text = next( + (block.text for block in response.content if hasattr(block, "text")), + None, + ) + return response_text, tool_metrics + + +async def evaluate_single_task( + client: Anthropic, + model: str, + qa_pair: dict[str, Any], + tools: list[dict[str, Any]], + connection: Any, + task_index: int, +) -> dict[str, Any]: + """Evaluate a single QA pair with the given tools.""" + start_time = time.time() + + print(f"Task {task_index + 1}: Running task with question: {qa_pair['question']}") + response, tool_metrics = await agent_loop(client, model, qa_pair["question"], tools, connection) + + response_value = extract_xml_content(response, "response") + summary = extract_xml_content(response, "summary") + feedback = extract_xml_content(response, "feedback") + + duration_seconds = time.time() - start_time + + return { + "question": qa_pair["question"], + "expected": qa_pair["answer"], + "actual": response_value, + "score": int(response_value == qa_pair["answer"]) if response_value else 0, + "total_duration": duration_seconds, + "tool_calls": tool_metrics, + "num_tool_calls": sum(len(metrics["durations"]) for metrics in tool_metrics.values()), + "summary": summary, + "feedback": feedback, + } + + +REPORT_HEADER = """ +# Evaluation Report + +## Summary + +- **Accuracy**: {correct}/{total} ({accuracy:.1f}%) +- **Average Task Duration**: {average_duration_s:.2f}s +- **Average Tool Calls per Task**: {average_tool_calls:.2f} +- **Total Tool Calls**: {total_tool_calls} + +--- +""" + +TASK_TEMPLATE = """ +### Task {task_num} + +**Question**: {question} +**Ground Truth Answer**: `{expected_answer}` +**Actual Answer**: `{actual_answer}` +**Correct**: {correct_indicator} +**Duration**: {total_duration:.2f}s +**Tool Calls**: {tool_calls} + +**Summary** +{summary} + +**Feedback** +{feedback} + +--- +""" + + +async def run_evaluation( + eval_path: Path, + connection: Any, + model: str = "claude-3-7-sonnet-20250219", +) -> str: + """Run evaluation with MCP server tools.""" + print("๐Ÿš€ Starting Evaluation") + + client = Anthropic() + + tools = await connection.list_tools() + print(f"๐Ÿ“‹ Loaded {len(tools)} tools from MCP server") + + qa_pairs = parse_evaluation_file(eval_path) + print(f"๐Ÿ“‹ Loaded {len(qa_pairs)} evaluation tasks") + + results = [] + for i, qa_pair in enumerate(qa_pairs): + print(f"Processing task {i + 1}/{len(qa_pairs)}") + result = await evaluate_single_task(client, model, qa_pair, tools, connection, i) + results.append(result) + + correct = sum(r["score"] for r in results) + accuracy = (correct / len(results)) * 100 if results else 0 + average_duration_s = sum(r["total_duration"] for r in results) / len(results) if results else 0 + average_tool_calls = sum(r["num_tool_calls"] for r in results) / len(results) if results else 0 + total_tool_calls = sum(r["num_tool_calls"] for r in results) + + report = REPORT_HEADER.format( + correct=correct, + total=len(results), + accuracy=accuracy, + average_duration_s=average_duration_s, + average_tool_calls=average_tool_calls, + total_tool_calls=total_tool_calls, + ) + + report += "".join([ + TASK_TEMPLATE.format( + task_num=i + 1, + question=qa_pair["question"], + expected_answer=qa_pair["answer"], + actual_answer=result["actual"] or "N/A", + correct_indicator="โœ…" if result["score"] else "โŒ", + total_duration=result["total_duration"], + tool_calls=json.dumps(result["tool_calls"], indent=2), + summary=result["summary"] or "N/A", + feedback=result["feedback"] or "N/A", + ) + for i, (qa_pair, result) in enumerate(zip(qa_pairs, results)) + ]) + + return report + + +def parse_headers(header_list: list[str]) -> dict[str, str]: + """Parse header strings in format 'Key: Value' into a dictionary.""" + headers = {} + if not header_list: + return headers + + for header in header_list: + if ":" in header: + key, value = header.split(":", 1) + headers[key.strip()] = value.strip() + else: + print(f"Warning: Ignoring malformed header: {header}") + return headers + + +def parse_env_vars(env_list: list[str]) -> dict[str, str]: + """Parse environment variable strings in format 'KEY=VALUE' into a dictionary.""" + env = {} + if not env_list: + return env + + for env_var in env_list: + if "=" in env_var: + key, value = env_var.split("=", 1) + env[key.strip()] = value.strip() + else: + print(f"Warning: Ignoring malformed environment variable: {env_var}") + return env + + +async def main(): + parser = argparse.ArgumentParser( + description="Evaluate MCP servers using test questions", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Evaluate a local stdio MCP server + python evaluation.py -t stdio -c python -a my_server.py eval.xml + + # Evaluate an SSE MCP server + python evaluation.py -t sse -u https://example.com/mcp -H "Authorization: Bearer token" eval.xml + + # Evaluate an HTTP MCP server with custom model + python evaluation.py -t http -u https://example.com/mcp -m claude-3-5-sonnet-20241022 eval.xml + """, + ) + + parser.add_argument("eval_file", type=Path, help="Path to evaluation XML file") + parser.add_argument("-t", "--transport", choices=["stdio", "sse", "http"], default="stdio", help="Transport type (default: stdio)") + parser.add_argument("-m", "--model", default="claude-3-7-sonnet-20250219", help="Claude model to use (default: claude-3-7-sonnet-20250219)") + + stdio_group = parser.add_argument_group("stdio options") + stdio_group.add_argument("-c", "--command", help="Command to run MCP server (stdio only)") + stdio_group.add_argument("-a", "--args", nargs="+", help="Arguments for the command (stdio only)") + stdio_group.add_argument("-e", "--env", nargs="+", help="Environment variables in KEY=VALUE format (stdio only)") + + remote_group = parser.add_argument_group("sse/http options") + remote_group.add_argument("-u", "--url", help="MCP server URL (sse/http only)") + remote_group.add_argument("-H", "--header", nargs="+", dest="headers", help="HTTP headers in 'Key: Value' format (sse/http only)") + + parser.add_argument("-o", "--output", type=Path, help="Output file for evaluation report (default: stdout)") + + args = parser.parse_args() + + if not args.eval_file.exists(): + print(f"Error: Evaluation file not found: {args.eval_file}") + sys.exit(1) + + headers = parse_headers(args.headers) if args.headers else None + env_vars = parse_env_vars(args.env) if args.env else None + + try: + connection = create_connection( + transport=args.transport, + command=args.command, + args=args.args, + env=env_vars, + url=args.url, + headers=headers, + ) + except ValueError as e: + print(f"Error: {e}") + sys.exit(1) + + print(f"๐Ÿ”— Connecting to MCP server via {args.transport}...") + + async with connection: + print("โœ… Connected successfully") + report = await run_evaluation(args.eval_file, connection, args.model) + + if args.output: + args.output.write_text(report) + print(f"\nโœ… Report saved to {args.output}") + else: + print("\n" + report) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/scripts/example_evaluation.xml b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/scripts/example_evaluation.xml new file mode 100644 index 00000000..41e4459b --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/scripts/example_evaluation.xml @@ -0,0 +1,22 @@ + + + Calculate the compound interest on $10,000 invested at 5% annual interest rate, compounded monthly for 3 years. What is the final amount in dollars (rounded to 2 decimal places)? + 11614.72 + + + A projectile is launched at a 45-degree angle with an initial velocity of 50 m/s. Calculate the total distance (in meters) it has traveled from the launch point after 2 seconds, assuming g=9.8 m/sยฒ. Round to 2 decimal places. + 87.25 + + + A sphere has a volume of 500 cubic meters. Calculate its surface area in square meters. Round to 2 decimal places. + 304.65 + + + Calculate the population standard deviation of this dataset: [12, 15, 18, 22, 25, 30, 35]. Round to 2 decimal places. + 7.61 + + + Calculate the pH of a solution with a hydrogen ion concentration of 3.5 ร— 10^-5 M. Round to 2 decimal places. + 4.46 + + diff --git a/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/scripts/requirements.txt b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/scripts/requirements.txt new file mode 100644 index 00000000..e73e5d1e --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/mcp-builder/scripts/requirements.txt @@ -0,0 +1,2 @@ +anthropic>=0.39.0 +mcp>=1.1.0 diff --git a/plugins/antigravity-bundle-automation-builder/skills/notion-automation/SKILL.md b/plugins/antigravity-bundle-automation-builder/skills/notion-automation/SKILL.md new file mode 100644 index 00000000..93ca24bf --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/notion-automation/SKILL.md @@ -0,0 +1,219 @@ +--- +name: notion-automation +description: "Automate Notion tasks via Rube MCP (Composio): pages, databases, blocks, comments, users. Always search tools first for current schemas." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Notion Automation via Rube MCP + +Automate Notion operations through Composio's Notion toolkit via Rube MCP. + +## Prerequisites + +- Rube MCP must be connected (RUBE_SEARCH_TOOLS available) +- Active Notion connection via `RUBE_MANAGE_CONNECTIONS` with toolkit `notion` +- Always call `RUBE_SEARCH_TOOLS` first to get current tool schemas + +## Setup + +**Get Rube MCP**: Add `https://rube.app/mcp` as an MCP server in your client configuration. No API keys needed โ€” just add the endpoint and it works. + + +1. Verify Rube MCP is available by confirming `RUBE_SEARCH_TOOLS` responds +2. Call `RUBE_MANAGE_CONNECTIONS` with toolkit `notion` +3. If connection is not ACTIVE, follow the returned auth link to complete Notion OAuth +4. Confirm connection status shows ACTIVE before running any workflows + +## Core Workflows + +### 1. Create and Manage Pages + +**When to use**: User wants to create, update, or archive Notion pages + +**Tool sequence**: +1. `NOTION_SEARCH_NOTION_PAGE` - Find parent page or existing page [Prerequisite] +2. `NOTION_CREATE_NOTION_PAGE` - Create a new page under a parent [Optional] +3. `NOTION_RETRIEVE_PAGE` - Get page metadata/properties [Optional] +4. `NOTION_UPDATE_PAGE` - Update page properties, title, icon, cover [Optional] +5. `NOTION_ARCHIVE_NOTION_PAGE` - Soft-delete (archive) a page [Optional] + +**Key parameters**: +- `query`: Search text for SEARCH_NOTION_PAGE +- `parent_id`: Parent page or database ID +- `page_id`: Page ID for retrieval/update/archive +- `properties`: Page property values matching parent schema + +**Pitfalls**: +- RETRIEVE_PAGE returns only metadata/properties, NOT body content; use FETCH_BLOCK_CONTENTS for page body +- ARCHIVE_NOTION_PAGE is a soft-delete (sets archived=true), not permanent deletion +- Broad searches can look incomplete unless has_more/next_cursor is fully paginated + +### 2. Query and Manage Databases + +**When to use**: User wants to query database rows, insert entries, or update records + +**Tool sequence**: +1. `NOTION_SEARCH_NOTION_PAGE` - Find the database by name [Prerequisite] +2. `NOTION_FETCH_DATABASE` - Inspect schema and properties [Prerequisite] +3. `NOTION_QUERY_DATABASE` / `NOTION_QUERY_DATABASE_WITH_FILTER` - Query rows [Required] +4. `NOTION_INSERT_ROW_DATABASE` - Add new entries [Optional] +5. `NOTION_UPDATE_ROW_DATABASE` - Update existing entries [Optional] + +**Key parameters**: +- `database_id`: Database ID (from search or URL) +- `filter`: Filter object matching Notion filter syntax +- `sorts`: Array of sort objects +- `start_cursor`: Pagination cursor from previous response +- `properties`: Property values matching database schema for inserts/updates + +**Pitfalls**: +- 404 object_not_found usually means wrong database_id or the database is not shared with the integration +- Results are paginated; ignoring has_more/next_cursor silently truncates reads +- Schema mismatches or missing required properties cause 400 validation_error +- Formula and read-only fields cannot be set via INSERT_ROW_DATABASE +- Property names in filters must match schema exactly (case-sensitive) + +### 3. Manage Blocks and Page Content + +**When to use**: User wants to read, append, or modify content blocks in a page + +**Tool sequence**: +1. `NOTION_FETCH_BLOCK_CONTENTS` - Read child blocks of a page [Required] +2. `NOTION_ADD_MULTIPLE_PAGE_CONTENT` - Append blocks to a page [Optional] +3. `NOTION_APPEND_TEXT_BLOCKS` - Append text-only blocks [Optional] +4. `NOTION_REPLACE_PAGE_CONTENT` - Replace all page content [Optional] +5. `NOTION_DELETE_BLOCK` - Remove a specific block [Optional] + +**Key parameters**: +- `block_id` / `page_id`: Target page or block ID +- `content_blocks`: Array of block objects (NOT child_blocks) +- `text`: Plain text content for APPEND_TEXT_BLOCKS + +**Pitfalls**: +- Use `content_blocks` parameter, NOT `child_blocks` -- the latter fails validation +- ADD_MULTIPLE_PAGE_CONTENT fails on archived pages; unarchive via UPDATE_PAGE first +- Created blocks are in response.data.results; persist block IDs for later edits +- DELETE_BLOCK is archival (archived=true), not permanent deletion + +### 4. Manage Database Schema + +**When to use**: User wants to create databases or modify their structure + +**Tool sequence**: +1. `NOTION_FETCH_DATABASE` - Inspect current schema [Prerequisite] +2. `NOTION_CREATE_DATABASE` - Create a new database [Optional] +3. `NOTION_UPDATE_SCHEMA_DATABASE` - Modify database properties [Optional] + +**Key parameters**: +- `parent_id`: Parent page ID for new databases +- `title`: Database title +- `properties`: Property definitions with types and options +- `database_id`: Database ID for schema updates + +**Pitfalls**: +- Cannot change property types via UPDATE_SCHEMA; must create new property and migrate data +- Formula, rollup, and relation properties have complex configuration requirements + +### 5. Manage Users and Comments + +**When to use**: User wants to list workspace users or manage comments on pages + +**Tool sequence**: +1. `NOTION_LIST_USERS` - List all workspace users [Optional] +2. `NOTION_GET_ABOUT_ME` - Get current authenticated user [Optional] +3. `NOTION_CREATE_COMMENT` - Add a comment to a page [Optional] +4. `NOTION_FETCH_COMMENTS` - List comments on a page [Optional] + +**Key parameters**: +- `page_id`: Page ID for comments (also called `discussion_id`) +- `rich_text`: Comment content as rich text array + +**Pitfalls**: +- Comments are linked to pages, not individual blocks +- User IDs from LIST_USERS are needed for people-type property filters + +## Common Patterns + +### ID Resolution + +**Page/Database name -> ID**: +``` +1. Call NOTION_SEARCH_NOTION_PAGE with query=name +2. Paginate with has_more/next_cursor until found +3. Extract id from matching result +``` + +**Database schema inspection**: +``` +1. Call NOTION_FETCH_DATABASE with database_id +2. Extract properties object for field names and types +3. Use exact property names in queries and inserts +``` + +### Pagination + +- Set `page_size` for results per page (max 100) +- Check response for `has_more` boolean +- Pass `start_cursor` or `next_cursor` in next request +- Continue until `has_more` is false + +### Notion Filter Syntax + +**Single filter**: +```json +{"property": "Status", "select": {"equals": "Done"}} +``` + +**Compound filter**: +```json +{"and": [ + {"property": "Status", "select": {"equals": "In Progress"}}, + {"property": "Assignee", "people": {"contains": "user-id"}} +]} +``` + +## Known Pitfalls + +**Integration Sharing**: +- Pages and databases must be shared with the Notion integration to be accessible +- Title queries can return 0 when the item is not shared with the integration + +**Property Types**: +- Property names are case-sensitive and must match schema exactly +- Formula, rollup, and created_time fields are read-only +- Select/multi-select values must match existing options unless creating new ones + +**Response Parsing**: +- Response data may be nested under `data_preview` or `data.results` +- Parse defensively with fallbacks for different nesting levels + +## Quick Reference + +| Task | Tool Slug | Key Params | +|------|-----------|------------| +| Search pages/databases | NOTION_SEARCH_NOTION_PAGE | query | +| Create page | NOTION_CREATE_NOTION_PAGE | parent_id, properties | +| Get page metadata | NOTION_RETRIEVE_PAGE | page_id | +| Update page | NOTION_UPDATE_PAGE | page_id, properties | +| Archive page | NOTION_ARCHIVE_NOTION_PAGE | page_id | +| Duplicate page | NOTION_DUPLICATE_PAGE | page_id | +| Get page blocks | NOTION_FETCH_BLOCK_CONTENTS | block_id | +| Append blocks | NOTION_ADD_MULTIPLE_PAGE_CONTENT | page_id, content_blocks | +| Append text | NOTION_APPEND_TEXT_BLOCKS | page_id, text | +| Replace content | NOTION_REPLACE_PAGE_CONTENT | page_id, content_blocks | +| Delete block | NOTION_DELETE_BLOCK | block_id | +| Query database | NOTION_QUERY_DATABASE | database_id, filter, sorts | +| Query with filter | NOTION_QUERY_DATABASE_WITH_FILTER | database_id, filter | +| Insert row | NOTION_INSERT_ROW_DATABASE | database_id, properties | +| Update row | NOTION_UPDATE_ROW_DATABASE | page_id, properties | +| Get database schema | NOTION_FETCH_DATABASE | database_id | +| Create database | NOTION_CREATE_DATABASE | parent_id, title, properties | +| Update schema | NOTION_UPDATE_SCHEMA_DATABASE | database_id, properties | +| List users | NOTION_LIST_USERS | (none) | +| Create comment | NOTION_CREATE_COMMENT | page_id, rich_text | +| List comments | NOTION_FETCH_COMMENTS | page_id | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-automation-builder/skills/slack-automation/SKILL.md b/plugins/antigravity-bundle-automation-builder/skills/slack-automation/SKILL.md new file mode 100644 index 00000000..a022b827 --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/slack-automation/SKILL.md @@ -0,0 +1,192 @@ +--- +name: slack-automation +description: "Automate Slack workspace operations including messaging, search, channel management, and reaction workflows through Composio's Slack toolkit." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Slack Automation via Rube MCP + +Automate Slack workspace operations including messaging, search, channel management, and reaction workflows through Composio's Slack toolkit. + +## Prerequisites + +- Rube MCP must be connected (RUBE_SEARCH_TOOLS available) +- Active Slack connection via `RUBE_MANAGE_CONNECTIONS` with toolkit `slack` +- Always call `RUBE_SEARCH_TOOLS` first to get current tool schemas + +## Setup + +**Get Rube MCP**: Add `https://rube.app/mcp` as an MCP server in your client configuration. No API keys needed โ€” just add the endpoint and it works. + +1. Verify Rube MCP is available by confirming `RUBE_SEARCH_TOOLS` responds +2. Call `RUBE_MANAGE_CONNECTIONS` with toolkit `slack` +3. If connection is not ACTIVE, follow the returned auth link to complete Slack OAuth +4. Confirm connection status shows ACTIVE before running any workflows + +## Core Workflows + +### 1. Send Messages to Channels + +**When to use**: User wants to post a message to a Slack channel or DM + +**Tool sequence**: +1. `SLACK_FIND_CHANNELS` - Resolve channel name to channel ID [Prerequisite] +2. `SLACK_LIST_ALL_CHANNELS` - Fallback if FIND_CHANNELS returns empty/ambiguous results [Fallback] +3. `SLACK_FIND_USERS` - Resolve user for DMs or @mentions [Optional] +4. `SLACK_OPEN_DM` - Open/reuse a DM channel if messaging a user directly [Optional] +5. `SLACK_SEND_MESSAGE` - Post the message with resolved channel ID [Required] +6. `SLACK_UPDATES_A_SLACK_MESSAGE` - Edit the posted message if corrections needed [Optional] + +**Key parameters**: +- `channel`: Channel ID or name (without '#' prefix) +- `markdown_text`: Preferred field for formatted messages (supports headers, bold, italic, code blocks) +- `text`: Raw text fallback (deprecated in favor of markdown_text) +- `thread_ts`: Timestamp of parent message to reply in a thread +- `blocks`: Block Kit layout blocks (deprecated, use markdown_text) + +**Pitfalls**: +- `SLACK_FIND_CHANNELS` requires `query` param; missing it errors with "Invalid request data provided" +- `SLACK_SEND_MESSAGE` requires valid channel plus one of markdown_text/text/blocks/attachments +- Invalid block payloads return error=invalid_blocks (max 50 blocks) +- Replies become top-level posts if `thread_ts` is omitted +- Persist `response.data.channel` and `response.data.message.ts` from SEND_MESSAGE for edit/thread operations + +### 2. Search Messages and Conversations + +**When to use**: User wants to find specific messages across the workspace + +**Tool sequence**: +1. `SLACK_FIND_CHANNELS` - Resolve channel for scoped search with `in:#channel` [Optional] +2. `SLACK_FIND_USERS` - Resolve user for author filter with `from:@user` [Optional] +3. `SLACK_SEARCH_MESSAGES` - Run keyword search across accessible conversations [Required] +4. `SLACK_FETCH_MESSAGE_THREAD_FROM_A_CONVERSATION` - Expand threads for relevant hits [Required] + +**Key parameters**: +- `query`: Search string supporting modifiers (`in:#channel`, `from:@user`, `before:YYYY-MM-DD`, `after:YYYY-MM-DD`, `has:link`, `has:file`) +- `count`: Results per page (max 100), or total with auto_paginate=true +- `sort`: 'score' (relevance) or 'timestamp' (chronological) +- `sort_dir`: 'asc' or 'desc' + +**Pitfalls**: +- Validation fails if `query` is missing/empty +- `ok=true` can still mean no hits (`response.data.messages.total=0`) +- Matches are under `response.data.messages.matches` (sometimes also `response.data_preview.messages.matches`) +- `match.text` may be empty/truncated; key info can appear in `matches[].attachments[]` +- Thread expansion via FETCH_MESSAGE_THREAD can truncate when `response.data.has_more=true`; paginate via `response_metadata.next_cursor` + +### 3. Manage Channels and Users + +**When to use**: User wants to list channels, users, or workspace info + +**Tool sequence**: +1. `SLACK_FETCH_TEAM_INFO` - Validate connectivity and get workspace identity [Required] +2. `SLACK_LIST_ALL_CHANNELS` - Enumerate public channels [Required] +3. `SLACK_LIST_CONVERSATIONS` - Include private channels and DMs [Optional] +4. `SLACK_LIST_ALL_USERS` - List workspace members [Required] +5. `SLACK_RETRIEVE_CONVERSATION_INFORMATION` - Get detailed channel metadata [Optional] +6. `SLACK_LIST_USER_GROUPS_FOR_TEAM_WITH_OPTIONS` - List user groups [Optional] + +**Key parameters**: +- `cursor`: Pagination cursor from `response_metadata.next_cursor` +- `limit`: Results per page (default varies; set explicitly for large workspaces) +- `types`: Channel types filter ('public_channel', 'private_channel', 'im', 'mpim') + +**Pitfalls**: +- Workspace metadata is nested under `response.data.team`, not top-level +- `SLACK_LIST_ALL_CHANNELS` returns public channels only; use `SLACK_LIST_CONVERSATIONS` for private/IM coverage +- `SLACK_LIST_ALL_USERS` can hit HTTP 429 rate limits; honor Retry-After header +- Always paginate via `response_metadata.next_cursor` until empty; de-duplicate by `id` + +### 4. React to and Thread Messages + +**When to use**: User wants to add reactions or manage threaded conversations + +**Tool sequence**: +1. `SLACK_SEARCH_MESSAGES` or `SLACK_FETCH_CONVERSATION_HISTORY` - Find the target message [Prerequisite] +2. `SLACK_ADD_REACTION_TO_AN_ITEM` - Add an emoji reaction [Required] +3. `SLACK_FETCH_ITEM_REACTIONS` - List reactions on a message [Optional] +4. `SLACK_REMOVE_REACTION_FROM_ITEM` - Remove a reaction [Optional] +5. `SLACK_SEND_MESSAGE` - Reply in thread using `thread_ts` [Optional] +6. `SLACK_FETCH_MESSAGE_THREAD_FROM_A_CONVERSATION` - Read full thread [Optional] + +**Key parameters**: +- `channel`: Channel ID where the message lives +- `timestamp` / `ts`: Message timestamp (unique identifier like '1234567890.123456') +- `name`: Emoji name without colons (e.g., 'thumbsup', 'wave::skin-tone-3') +- `thread_ts`: Parent message timestamp for threaded replies + +**Pitfalls**: +- Reactions require exact channel ID + message timestamp pair +- Emoji names use Slack's naming convention without colons +- `SLACK_FETCH_CONVERSATION_HISTORY` only returns main channel timeline, NOT threaded replies +- Use `SLACK_FETCH_MESSAGE_THREAD_FROM_A_CONVERSATION` with parent's `thread_ts` to get thread replies + +### 5. Schedule Messages + +**When to use**: User wants to schedule a message for future delivery + +**Tool sequence**: +1. `SLACK_FIND_CHANNELS` - Resolve channel ID [Prerequisite] +2. `SLACK_SCHEDULE_MESSAGE` - Schedule the message with `post_at` timestamp [Required] + +**Key parameters**: +- `channel`: Resolved channel ID +- `post_at`: Unix timestamp for delivery (up to 120 days in advance) +- `text` / `blocks`: Message content + +**Pitfalls**: +- Scheduling is limited to 120 days in advance +- `post_at` must be a Unix timestamp, not ISO 8601 + +## Common Patterns + +### ID Resolution +Always resolve display names to IDs before operations: +- **Channel name -> Channel ID**: `SLACK_FIND_CHANNELS` with `query` param +- **User name -> User ID**: `SLACK_FIND_USERS` with `search_query` or `email` +- **DM channel**: `SLACK_OPEN_DM` with resolved user IDs + +### Pagination +Most list endpoints use cursor-based pagination: +- Follow `response_metadata.next_cursor` until empty +- Set explicit `limit` values (e.g., 100-200) for reliable paging +- De-duplicate results by `id` across pages + +### Message Formatting +- Prefer `markdown_text` over `text` or `blocks` for formatted messages +- Use `<@USER_ID>` format to mention users (not @username) +- Use `\n` for line breaks in markdown_text + +## Known Pitfalls + +- **Channel resolution**: `SLACK_FIND_CHANNELS` can return empty results if channel is private and bot hasn't been invited +- **Rate limits**: `SLACK_LIST_ALL_USERS` and other list endpoints can hit HTTP 429; honor Retry-After header +- **Nested responses**: Results may be nested under `response.data.results[0].response.data` in wrapped executions +- **Thread vs channel**: `SLACK_FETCH_CONVERSATION_HISTORY` returns main timeline only; use `SLACK_FETCH_MESSAGE_THREAD_FROM_A_CONVERSATION` for thread replies +- **Message editing**: Requires both `channel` and original message `ts`; persist these from SEND_MESSAGE response +- **Search delays**: Recently posted messages may not appear in search results immediately +- **Scope limitations**: Missing OAuth scopes can cause 403 errors; check with `SLACK_GET_APP_PERMISSION_SCOPES` + +## Quick Reference + +| Task | Tool Slug | Key Params | +|------|-----------|------------| +| Find channels | `SLACK_FIND_CHANNELS` | `query` | +| List all channels | `SLACK_LIST_ALL_CHANNELS` | `limit`, `cursor`, `types` | +| Send message | `SLACK_SEND_MESSAGE` | `channel`, `markdown_text` | +| Edit message | `SLACK_UPDATES_A_SLACK_MESSAGE` | `channel`, `ts`, `markdown_text` | +| Search messages | `SLACK_SEARCH_MESSAGES` | `query`, `count`, `sort` | +| Get thread | `SLACK_FETCH_MESSAGE_THREAD_FROM_A_CONVERSATION` | `channel`, `ts` | +| Add reaction | `SLACK_ADD_REACTION_TO_AN_ITEM` | `channel`, `name`, `timestamp` | +| Find users | `SLACK_FIND_USERS` | `search_query` or `email` | +| List users | `SLACK_LIST_ALL_USERS` | `limit`, `cursor` | +| Open DM | `SLACK_OPEN_DM` | user IDs | +| Schedule message | `SLACK_SCHEDULE_MESSAGE` | `channel`, `post_at`, `text` | +| Get channel info | `SLACK_RETRIEVE_CONVERSATION_INFORMATION` | channel ID | +| Channel history | `SLACK_FETCH_CONVERSATION_HISTORY` | `channel`, `oldest`, `latest` | +| Workspace info | `SLACK_FETCH_TEAM_INFO` | (none) | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-automation-builder/skills/workflow-automation/SKILL.md b/plugins/antigravity-bundle-automation-builder/skills/workflow-automation/SKILL.md new file mode 100644 index 00000000..8514d926 --- /dev/null +++ b/plugins/antigravity-bundle-automation-builder/skills/workflow-automation/SKILL.md @@ -0,0 +1,74 @@ +--- +name: workflow-automation +description: "You are a workflow automation architect who has seen both the promise and the pain of these platforms. You've migrated teams from brittle cron jobs to durable execution and watched their on-call burden drop by 80%." +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# Workflow Automation + +You are a workflow automation architect who has seen both the promise and +the pain of these platforms. You've migrated teams from brittle cron jobs +to durable execution and watched their on-call burden drop by 80%. + +Your core insight: Different platforms make different tradeoffs. n8n is +accessible but sacrifices performance. Temporal is correct but complex. +Inngest balances developer experience with reliability. DBOS uses your +existing PostgreSQL for durable execution with minimal infrastructure +overhead. There's no "best" - only "best for your situation." + +You push for durable execution + +## Capabilities + +- workflow-automation +- workflow-orchestration +- durable-execution +- event-driven-workflows +- step-functions +- job-queues +- background-jobs +- scheduled-tasks + +## Patterns + +### Sequential Workflow Pattern + +Steps execute in order, each output becomes next input + +### Parallel Workflow Pattern + +Independent steps run simultaneously, aggregate results + +### Orchestrator-Worker Pattern + +Central coordinator dispatches work to specialized workers + +## Anti-Patterns + +### โŒ No Durable Execution for Payments + +### โŒ Monolithic Workflows + +### โŒ No Observability + +## โš ๏ธ Sharp Edges + +| Issue | Severity | Solution | +|-------|----------|----------| +| Issue | critical | # ALWAYS use idempotency keys for external calls: | +| Issue | high | # Break long workflows into checkpointed steps: | +| Issue | high | # ALWAYS set timeouts on activities: | +| Issue | critical | # WRONG - side effects in workflow code: | +| Issue | medium | # ALWAYS use exponential backoff: | +| Issue | high | # WRONG - large data in workflow: | +| Issue | high | # Inngest onFailure handler: | +| Issue | medium | # Every production n8n workflow needs: | + +## Related Skills + +Works well with: `multi-agent-orchestration`, `agent-tool-builder`, `backend`, `devops`, `dbos-*` + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-azure-ai-cloud/.codex-plugin/plugin.json b/plugins/antigravity-bundle-azure-ai-cloud/.codex-plugin/plugin.json new file mode 100644 index 00000000..d63c0de9 --- /dev/null +++ b/plugins/antigravity-bundle-azure-ai-cloud/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-azure-ai-cloud", + "version": "8.10.0", + "description": "Install the \"Azure AI & Cloud\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "azure-ai-cloud", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Azure AI & Cloud", + "shortDescription": "Specialized Packs ยท 6 curated skills", + "longDescription": "For building on Azure across cloud, AI, and platform services. Covers Azd Deployment, Azure Functions, and 4 more skills.", + "developerName": "sickn33 and contributors", + "category": "Specialized Packs", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-azure-ai-cloud/skills/azd-deployment/SKILL.md b/plugins/antigravity-bundle-azure-ai-cloud/skills/azd-deployment/SKILL.md new file mode 100644 index 00000000..b713f16b --- /dev/null +++ b/plugins/antigravity-bundle-azure-ai-cloud/skills/azd-deployment/SKILL.md @@ -0,0 +1,302 @@ +--- +name: azd-deployment +description: "Deploy containerized frontend + backend applications to Azure Container Apps with remote builds, managed identity, and idempotent infrastructure." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Azure Developer CLI (azd) Container Apps Deployment + +Deploy containerized frontend + backend applications to Azure Container Apps with remote builds, managed identity, and idempotent infrastructure. + +## Quick Start + +```bash +# Initialize and deploy +azd auth login +azd init # Creates azure.yaml and .azure/ folder +azd env new # Create environment (dev, staging, prod) +azd up # Provision infra + build + deploy +``` + +## Core File Structure + +``` +project/ +โ”œโ”€โ”€ azure.yaml # azd service definitions + hooks +โ”œโ”€โ”€ infra/ +โ”‚ โ”œโ”€โ”€ main.bicep # Root infrastructure module +โ”‚ โ”œโ”€โ”€ main.parameters.json # Parameter injection from env vars +โ”‚ โ””โ”€โ”€ modules/ +โ”‚ โ”œโ”€โ”€ container-apps-environment.bicep +โ”‚ โ””โ”€โ”€ container-app.bicep +โ”œโ”€โ”€ .azure/ +โ”‚ โ”œโ”€โ”€ config.json # Default environment pointer +โ”‚ โ””โ”€โ”€ / +โ”‚ โ”œโ”€โ”€ .env # Environment-specific values (azd-managed) +โ”‚ โ””โ”€โ”€ config.json # Environment metadata +โ””โ”€โ”€ src/ + โ”œโ”€โ”€ frontend/Dockerfile + โ””โ”€โ”€ backend/Dockerfile +``` + +## azure.yaml Configuration + +### Minimal Configuration + +```yaml +name: azd-deployment +services: + backend: + project: ./src/backend + language: python + host: containerapp + docker: + path: ./Dockerfile + remoteBuild: true +``` + +### Full Configuration with Hooks + +```yaml +name: azd-deployment +metadata: + template: my-project@1.0.0 + +infra: + provider: bicep + path: ./infra + +azure: + location: eastus2 + +services: + frontend: + project: ./src/frontend + language: ts + host: containerapp + docker: + path: ./Dockerfile + context: . + remoteBuild: true + + backend: + project: ./src/backend + language: python + host: containerapp + docker: + path: ./Dockerfile + context: . + remoteBuild: true + +hooks: + preprovision: + shell: sh + run: | + echo "Before provisioning..." + + postprovision: + shell: sh + run: | + echo "After provisioning - set up RBAC, etc." + + postdeploy: + shell: sh + run: | + echo "Frontend: ${SERVICE_FRONTEND_URI}" + echo "Backend: ${SERVICE_BACKEND_URI}" +``` + +### Key azure.yaml Options + +| Option | Description | +|--------|-------------| +| `remoteBuild: true` | Build images in Azure Container Registry (recommended) | +| `context: .` | Docker build context relative to project path | +| `host: containerapp` | Deploy to Azure Container Apps | +| `infra.provider: bicep` | Use Bicep for infrastructure | + +## Environment Variables Flow + +### Three-Level Configuration + +1. **Local `.env`** - For local development only +2. **`.azure//.env`** - azd-managed, auto-populated from Bicep outputs +3. **`main.parameters.json`** - Maps env vars to Bicep parameters + +### Parameter Injection Pattern + +```json +// infra/main.parameters.json +{ + "parameters": { + "environmentName": { "value": "${AZURE_ENV_NAME}" }, + "location": { "value": "${AZURE_LOCATION=eastus2}" }, + "azureOpenAiEndpoint": { "value": "${AZURE_OPENAI_ENDPOINT}" } + } +} +``` + +Syntax: `${VAR_NAME}` or `${VAR_NAME=default_value}` + +### Setting Environment Variables + +```bash +# Set for current environment +azd env set AZURE_OPENAI_ENDPOINT "https://my-openai.openai.azure.com" +azd env set AZURE_SEARCH_ENDPOINT "https://my-search.search.windows.net" + +# Set during init +azd env new prod +azd env set AZURE_OPENAI_ENDPOINT "..." +``` + +### Bicep Output โ†’ Environment Variable + +```bicep +// In main.bicep - outputs auto-populate .azure//.env +output SERVICE_FRONTEND_URI string = frontend.outputs.uri +output SERVICE_BACKEND_URI string = backend.outputs.uri +output BACKEND_PRINCIPAL_ID string = backend.outputs.principalId +``` + +## Idempotent Deployments + +### Why azd up is Idempotent + +1. **Bicep is declarative** - Resources reconcile to desired state +2. **Remote builds tag uniquely** - Image tags include deployment timestamp +3. **ACR reuses layers** - Only changed layers upload + +### Preserving Manual Changes + +Custom domains added via Portal can be lost on redeploy. Preserve with hooks: + +```yaml +hooks: + preprovision: + shell: sh + run: | + # Save custom domains before provision + if az containerapp show --name "$FRONTEND_NAME" -g "$RG" &>/dev/null; then + az containerapp show --name "$FRONTEND_NAME" -g "$RG" \ + --query "properties.configuration.ingress.customDomains" \ + -o json > /tmp/domains.json + fi + + postprovision: + shell: sh + run: | + # Verify/restore custom domains + if [ -f /tmp/domains.json ]; then + echo "Saved domains: $(cat /tmp/domains.json)" + fi +``` + +### Handling Existing Resources + +```bicep +// Reference existing ACR (don't recreate) +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = { + name: containerRegistryName +} + +// Set customDomains to null to preserve Portal-added domains +customDomains: empty(customDomainsParam) ? null : customDomainsParam +``` + +## Container App Service Discovery + +Internal HTTP routing between Container Apps in same environment: + +```bicep +// Backend reference in frontend env vars +env: [ + { + name: 'BACKEND_URL' + value: 'http://ca-backend-${resourceToken}' // Internal DNS + } +] +``` + +Frontend nginx proxies to internal URL: +```nginx +location /api { + proxy_pass $BACKEND_URL; +} +``` + +## Managed Identity & RBAC + +### Enable System-Assigned Identity + +```bicep +resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { + identity: { + type: 'SystemAssigned' + } +} + +output principalId string = containerApp.identity.principalId +``` + +### Post-Provision RBAC Assignment + +```yaml +hooks: + postprovision: + shell: sh + run: | + PRINCIPAL_ID="${BACKEND_PRINCIPAL_ID}" + + # Azure OpenAI access + az role assignment create \ + --assignee-object-id "$PRINCIPAL_ID" \ + --assignee-principal-type ServicePrincipal \ + --role "Cognitive Services OpenAI User" \ + --scope "$OPENAI_RESOURCE_ID" 2>/dev/null || true + + # Azure AI Search access + az role assignment create \ + --assignee-object-id "$PRINCIPAL_ID" \ + --role "Search Index Data Reader" \ + --scope "$SEARCH_RESOURCE_ID" 2>/dev/null || true +``` + +## Common Commands + +```bash +# Environment management +azd env list # List environments +azd env select # Switch environment +azd env get-values # Show all env vars +azd env set KEY value # Set variable + +# Deployment +azd up # Full provision + deploy +azd provision # Infrastructure only +azd deploy # Code deployment only +azd deploy --service backend # Deploy single service + +# Debugging +azd show # Show project status +az containerapp logs show -n -g --follow # Stream logs +``` + +## Reference Files + +- **Bicep patterns**: See references/bicep-patterns.md for Container Apps modules +- **Troubleshooting**: See references/troubleshooting.md for common issues +- **azure.yaml schema**: See references/azure-yaml-schema.md for full options + +## Critical Reminders + +1. **Always use `remoteBuild: true`** - Local builds fail on M1/ARM Macs deploying to AMD64 +2. **Bicep outputs auto-populate .azure//.env** - Don't manually edit +3. **Use `azd env set` for secrets** - Not main.parameters.json defaults +4. **Service tags (`azd-service-name`)** - Required for azd to find Container Apps +5. **`|| true` in hooks** - Prevent RBAC "already exists" errors from failing deploy + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-azure-ai-cloud/skills/azure-ai-openai-dotnet/SKILL.md b/plugins/antigravity-bundle-azure-ai-cloud/skills/azure-ai-openai-dotnet/SKILL.md new file mode 100644 index 00000000..0d12fc5d --- /dev/null +++ b/plugins/antigravity-bundle-azure-ai-cloud/skills/azure-ai-openai-dotnet/SKILL.md @@ -0,0 +1,459 @@ +--- +name: azure-ai-openai-dotnet +description: Azure OpenAI SDK for .NET. Client library for Azure OpenAI and OpenAI services. Use for chat completions, embeddings, image generation, audio transcription, and assistants. +risk: unknown +source: community +date_added: '2026-02-27' +--- + +# Azure.AI.OpenAI (.NET) + +Client library for Azure OpenAI Service providing access to OpenAI models including GPT-4, GPT-4o, embeddings, DALL-E, and Whisper. + +## Installation + +```bash +dotnet add package Azure.AI.OpenAI + +# For OpenAI (non-Azure) compatibility +dotnet add package OpenAI +``` + +**Current Version**: 2.1.0 (stable) + +## Environment Variables + +```bash +AZURE_OPENAI_ENDPOINT=https://.openai.azure.com +AZURE_OPENAI_API_KEY= # For key-based auth +AZURE_OPENAI_DEPLOYMENT_NAME=gpt-4o-mini # Your deployment name +``` + +## Client Hierarchy + +``` +AzureOpenAIClient (top-level) +โ”œโ”€โ”€ GetChatClient(deploymentName) โ†’ ChatClient +โ”œโ”€โ”€ GetEmbeddingClient(deploymentName) โ†’ EmbeddingClient +โ”œโ”€โ”€ GetImageClient(deploymentName) โ†’ ImageClient +โ”œโ”€โ”€ GetAudioClient(deploymentName) โ†’ AudioClient +โ””โ”€โ”€ GetAssistantClient() โ†’ AssistantClient +``` + +## Authentication + +### API Key Authentication + +```csharp +using Azure; +using Azure.AI.OpenAI; + +AzureOpenAIClient client = new( + new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!), + new AzureKeyCredential(Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY")!)); +``` + +### Microsoft Entra ID (Recommended for Production) + +```csharp +using Azure.Identity; +using Azure.AI.OpenAI; + +AzureOpenAIClient client = new( + new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!), + new DefaultAzureCredential()); +``` + +### Using OpenAI SDK Directly with Azure + +```csharp +using Azure.Identity; +using OpenAI; +using OpenAI.Chat; +using System.ClientModel.Primitives; + +#pragma warning disable OPENAI001 + +BearerTokenPolicy tokenPolicy = new( + new DefaultAzureCredential(), + "https://cognitiveservices.azure.com/.default"); + +ChatClient client = new( + model: "gpt-4o-mini", + authenticationPolicy: tokenPolicy, + options: new OpenAIClientOptions() + { + Endpoint = new Uri("https://YOUR-RESOURCE.openai.azure.com/openai/v1") + }); +``` + +## Chat Completions + +### Basic Chat + +```csharp +using Azure.AI.OpenAI; +using OpenAI.Chat; + +AzureOpenAIClient azureClient = new( + new Uri(endpoint), + new DefaultAzureCredential()); + +ChatClient chatClient = azureClient.GetChatClient("gpt-4o-mini"); + +ChatCompletion completion = chatClient.CompleteChat( +[ + new SystemChatMessage("You are a helpful assistant."), + new UserChatMessage("What is Azure OpenAI?") +]); + +Console.WriteLine(completion.Content[0].Text); +``` + +### Async Chat + +```csharp +ChatCompletion completion = await chatClient.CompleteChatAsync( +[ + new SystemChatMessage("You are a helpful assistant."), + new UserChatMessage("Explain cloud computing in simple terms.") +]); + +Console.WriteLine($"Response: {completion.Content[0].Text}"); +Console.WriteLine($"Tokens used: {completion.Usage.TotalTokenCount}"); +``` + +### Streaming Chat + +```csharp +await foreach (StreamingChatCompletionUpdate update + in chatClient.CompleteChatStreamingAsync(messages)) +{ + if (update.ContentUpdate.Count > 0) + { + Console.Write(update.ContentUpdate[0].Text); + } +} +``` + +### Chat with Options + +```csharp +ChatCompletionOptions options = new() +{ + MaxOutputTokenCount = 1000, + Temperature = 0.7f, + TopP = 0.95f, + FrequencyPenalty = 0, + PresencePenalty = 0 +}; + +ChatCompletion completion = await chatClient.CompleteChatAsync(messages, options); +``` + +### Multi-turn Conversation + +```csharp +List messages = new() +{ + new SystemChatMessage("You are a helpful assistant."), + new UserChatMessage("Hi, can you help me?"), + new AssistantChatMessage("Of course! What do you need help with?"), + new UserChatMessage("What's the capital of France?") +}; + +ChatCompletion completion = await chatClient.CompleteChatAsync(messages); +messages.Add(new AssistantChatMessage(completion.Content[0].Text)); +``` + +## Structured Outputs (JSON Schema) + +```csharp +using System.Text.Json; + +ChatCompletionOptions options = new() +{ + ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat( + jsonSchemaFormatName: "math_reasoning", + jsonSchema: BinaryData.FromBytes(""" + { + "type": "object", + "properties": { + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "explanation": { "type": "string" }, + "output": { "type": "string" } + }, + "required": ["explanation", "output"], + "additionalProperties": false + } + }, + "final_answer": { "type": "string" } + }, + "required": ["steps", "final_answer"], + "additionalProperties": false + } + """u8.ToArray()), + jsonSchemaIsStrict: true) +}; + +ChatCompletion completion = await chatClient.CompleteChatAsync( + [new UserChatMessage("How can I solve 8x + 7 = -23?")], + options); + +using JsonDocument json = JsonDocument.Parse(completion.Content[0].Text); +Console.WriteLine($"Answer: {json.RootElement.GetProperty("final_answer")}"); +``` + +## Reasoning Models (o1, o4-mini) + +```csharp +ChatCompletionOptions options = new() +{ + ReasoningEffortLevel = ChatReasoningEffortLevel.Low, + MaxOutputTokenCount = 100000 +}; + +ChatCompletion completion = await chatClient.CompleteChatAsync( +[ + new DeveloperChatMessage("You are a helpful assistant"), + new UserChatMessage("Explain the theory of relativity") +], options); +``` + +## Azure AI Search Integration (RAG) + +```csharp +using Azure.AI.OpenAI.Chat; + +#pragma warning disable AOAI001 + +ChatCompletionOptions options = new(); +options.AddDataSource(new AzureSearchChatDataSource() +{ + Endpoint = new Uri(searchEndpoint), + IndexName = searchIndex, + Authentication = DataSourceAuthentication.FromApiKey(searchKey) +}); + +ChatCompletion completion = await chatClient.CompleteChatAsync( + [new UserChatMessage("What health plans are available?")], + options); + +ChatMessageContext context = completion.GetMessageContext(); +if (context?.Intent is not null) +{ + Console.WriteLine($"Intent: {context.Intent}"); +} +foreach (ChatCitation citation in context?.Citations ?? []) +{ + Console.WriteLine($"Citation: {citation.Content}"); +} +``` + +## Embeddings + +```csharp +using OpenAI.Embeddings; + +EmbeddingClient embeddingClient = azureClient.GetEmbeddingClient("text-embedding-ada-002"); + +OpenAIEmbedding embedding = await embeddingClient.GenerateEmbeddingAsync("Hello, world!"); +ReadOnlyMemory vector = embedding.ToFloats(); + +Console.WriteLine($"Embedding dimensions: {vector.Length}"); +``` + +### Batch Embeddings + +```csharp +List inputs = new() +{ + "First document text", + "Second document text", + "Third document text" +}; + +OpenAIEmbeddingCollection embeddings = await embeddingClient.GenerateEmbeddingsAsync(inputs); + +foreach (OpenAIEmbedding emb in embeddings) +{ + Console.WriteLine($"Index {emb.Index}: {emb.ToFloats().Length} dimensions"); +} +``` + +## Image Generation (DALL-E) + +```csharp +using OpenAI.Images; + +ImageClient imageClient = azureClient.GetImageClient("dall-e-3"); + +GeneratedImage image = await imageClient.GenerateImageAsync( + "A futuristic city skyline at sunset", + new ImageGenerationOptions + { + Size = GeneratedImageSize.W1024xH1024, + Quality = GeneratedImageQuality.High, + Style = GeneratedImageStyle.Vivid + }); + +Console.WriteLine($"Image URL: {image.ImageUri}"); +``` + +## Audio (Whisper) + +### Transcription + +```csharp +using OpenAI.Audio; + +AudioClient audioClient = azureClient.GetAudioClient("whisper"); + +AudioTranscription transcription = await audioClient.TranscribeAudioAsync( + "audio.mp3", + new AudioTranscriptionOptions + { + ResponseFormat = AudioTranscriptionFormat.Verbose, + Language = "en" + }); + +Console.WriteLine(transcription.Text); +``` + +### Text-to-Speech + +```csharp +BinaryData speech = await audioClient.GenerateSpeechAsync( + "Hello, welcome to Azure OpenAI!", + GeneratedSpeechVoice.Alloy, + new SpeechGenerationOptions + { + SpeedRatio = 1.0f, + ResponseFormat = GeneratedSpeechFormat.Mp3 + }); + +await File.WriteAllBytesAsync("output.mp3", speech.ToArray()); +``` + +## Function Calling (Tools) + +```csharp +ChatTool getCurrentWeatherTool = ChatTool.CreateFunctionTool( + functionName: "get_current_weather", + functionDescription: "Get the current weather in a given location", + functionParameters: BinaryData.FromString(""" + { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location"] + } + """)); + +ChatCompletionOptions options = new() +{ + Tools = { getCurrentWeatherTool } +}; + +ChatCompletion completion = await chatClient.CompleteChatAsync( + [new UserChatMessage("What's the weather in Seattle?")], + options); + +if (completion.FinishReason == ChatFinishReason.ToolCalls) +{ + foreach (ChatToolCall toolCall in completion.ToolCalls) + { + Console.WriteLine($"Function: {toolCall.FunctionName}"); + Console.WriteLine($"Arguments: {toolCall.FunctionArguments}"); + } +} +``` + +## Key Types Reference + +| Type | Purpose | +|------|---------| +| `AzureOpenAIClient` | Top-level client for Azure OpenAI | +| `ChatClient` | Chat completions | +| `EmbeddingClient` | Text embeddings | +| `ImageClient` | Image generation (DALL-E) | +| `AudioClient` | Audio transcription/TTS | +| `ChatCompletion` | Chat response | +| `ChatCompletionOptions` | Request configuration | +| `StreamingChatCompletionUpdate` | Streaming response chunk | +| `ChatMessage` | Base message type | +| `SystemChatMessage` | System prompt | +| `UserChatMessage` | User input | +| `AssistantChatMessage` | Assistant response | +| `DeveloperChatMessage` | Developer message (reasoning models) | +| `ChatTool` | Function/tool definition | +| `ChatToolCall` | Tool invocation request | + +## Best Practices + +1. **Use Entra ID in production** โ€” Avoid API keys; use `DefaultAzureCredential` +2. **Reuse client instances** โ€” Create once, share across requests +3. **Handle rate limits** โ€” Implement exponential backoff for 429 errors +4. **Stream for long responses** โ€” Use `CompleteChatStreamingAsync` for better UX +5. **Set appropriate timeouts** โ€” Long completions may need extended timeouts +6. **Use structured outputs** โ€” JSON schema ensures consistent response format +7. **Monitor token usage** โ€” Track `completion.Usage` for cost management +8. **Validate tool calls** โ€” Always validate function arguments before execution + +## Error Handling + +```csharp +using Azure; + +try +{ + ChatCompletion completion = await chatClient.CompleteChatAsync(messages); +} +catch (RequestFailedException ex) when (ex.Status == 429) +{ + Console.WriteLine("Rate limited. Retry after delay."); + await Task.Delay(TimeSpan.FromSeconds(10)); +} +catch (RequestFailedException ex) when (ex.Status == 400) +{ + Console.WriteLine($"Bad request: {ex.Message}"); +} +catch (RequestFailedException ex) +{ + Console.WriteLine($"Azure OpenAI error: {ex.Status} - {ex.Message}"); +} +``` + +## Related SDKs + +| SDK | Purpose | Install | +|-----|---------|---------| +| `Azure.AI.OpenAI` | Azure OpenAI client (this SDK) | `dotnet add package Azure.AI.OpenAI` | +| `OpenAI` | OpenAI compatibility | `dotnet add package OpenAI` | +| `Azure.Identity` | Authentication | `dotnet add package Azure.Identity` | +| `Azure.Search.Documents` | AI Search for RAG | `dotnet add package Azure.Search.Documents` | + +## Reference Links + +| Resource | URL | +|----------|-----| +| NuGet Package | https://www.nuget.org/packages/Azure.AI.OpenAI | +| API Reference | https://learn.microsoft.com/dotnet/api/azure.ai.openai | +| Migration Guide (1.0โ†’2.0) | https://learn.microsoft.com/azure/ai-services/openai/how-to/dotnet-migration | +| Quickstart | https://learn.microsoft.com/azure/ai-services/openai/quickstart | +| GitHub Source | https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/openai/Azure.AI.OpenAI | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-azure-ai-cloud/skills/azure-functions/SKILL.md b/plugins/antigravity-bundle-azure-ai-cloud/skills/azure-functions/SKILL.md new file mode 100644 index 00000000..faafec6e --- /dev/null +++ b/plugins/antigravity-bundle-azure-ai-cloud/skills/azure-functions/SKILL.md @@ -0,0 +1,47 @@ +--- +name: azure-functions +description: "Modern .NET execution model with process isolation" +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# Azure Functions + +## Patterns + +### Isolated Worker Model (.NET) + +Modern .NET execution model with process isolation + +### Node.js v4 Programming Model + +Modern code-centric approach for TypeScript/JavaScript + +### Python v2 Programming Model + +Decorator-based approach for Python functions + +## Anti-Patterns + +### โŒ Blocking Async Calls + +### โŒ New HttpClient Per Request + +### โŒ In-Process Model for New Projects + +## โš ๏ธ Sharp Edges + +| Issue | Severity | Solution | +|-------|----------|----------| +| Issue | high | ## Use async pattern with Durable Functions | +| Issue | high | ## Use IHttpClientFactory (Recommended) | +| Issue | high | ## Always use async/await | +| Issue | medium | ## Configure maximum timeout (Consumption) | +| Issue | high | ## Use isolated worker for new projects | +| Issue | medium | ## Configure Application Insights properly | +| Issue | medium | ## Check extension bundle (most common) | +| Issue | medium | ## Add warmup trigger to initialize your code | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-azure-ai-cloud/skills/azure-identity-py/SKILL.md b/plugins/antigravity-bundle-azure-ai-cloud/skills/azure-identity-py/SKILL.md new file mode 100644 index 00000000..9a87217e --- /dev/null +++ b/plugins/antigravity-bundle-azure-ai-cloud/skills/azure-identity-py/SKILL.md @@ -0,0 +1,195 @@ +--- +name: azure-identity-py +description: Azure Identity SDK for Python authentication. Use for DefaultAzureCredential, managed identity, service principals, and token caching. +risk: unknown +source: community +date_added: '2026-02-27' +--- + +# Azure Identity SDK for Python + +Authentication library for Azure SDK clients using Microsoft Entra ID (formerly Azure AD). + +## Installation + +```bash +pip install azure-identity +``` + +## Environment Variables + +```bash +# Service Principal (for production/CI) +AZURE_TENANT_ID= +AZURE_CLIENT_ID= +AZURE_CLIENT_SECRET= + +# User-assigned Managed Identity (optional) +AZURE_CLIENT_ID= +``` + +## DefaultAzureCredential + +The recommended credential for most scenarios. Tries multiple authentication methods in order: + +```python +from azure.identity import DefaultAzureCredential +from azure.storage.blob import BlobServiceClient + +# Works in local dev AND production without code changes +credential = DefaultAzureCredential() + +client = BlobServiceClient( + account_url="https://.blob.core.windows.net", + credential=credential +) +``` + +### Credential Chain Order + +| Order | Credential | Environment | +|-------|-----------|-------------| +| 1 | EnvironmentCredential | CI/CD, containers | +| 2 | WorkloadIdentityCredential | Kubernetes | +| 3 | ManagedIdentityCredential | Azure VMs, App Service, Functions | +| 4 | SharedTokenCacheCredential | Windows only | +| 5 | VisualStudioCodeCredential | VS Code with Azure extension | +| 6 | AzureCliCredential | `az login` | +| 7 | AzurePowerShellCredential | `Connect-AzAccount` | +| 8 | AzureDeveloperCliCredential | `azd auth login` | + +### Customizing DefaultAzureCredential + +```python +# Exclude credentials you don't need +credential = DefaultAzureCredential( + exclude_environment_credential=True, + exclude_shared_token_cache_credential=True, + managed_identity_client_id="" # For user-assigned MI +) + +# Enable interactive browser (disabled by default) +credential = DefaultAzureCredential( + exclude_interactive_browser_credential=False +) +``` + +## Specific Credential Types + +### ManagedIdentityCredential + +For Azure-hosted resources (VMs, App Service, Functions, AKS): + +```python +from azure.identity import ManagedIdentityCredential + +# System-assigned managed identity +credential = ManagedIdentityCredential() + +# User-assigned managed identity +credential = ManagedIdentityCredential( + client_id="" +) +``` + +### ClientSecretCredential + +For service principal with secret: + +```python +from azure.identity import ClientSecretCredential + +credential = ClientSecretCredential( + tenant_id=os.environ["AZURE_TENANT_ID"], + client_id=os.environ["AZURE_CLIENT_ID"], + client_secret=os.environ["AZURE_CLIENT_SECRET"] +) +``` + +### AzureCliCredential + +Uses the account from `az login`: + +```python +from azure.identity import AzureCliCredential + +credential = AzureCliCredential() +``` + +### ChainedTokenCredential + +Custom credential chain: + +```python +from azure.identity import ( + ChainedTokenCredential, + ManagedIdentityCredential, + AzureCliCredential +) + +# Try managed identity first, fall back to CLI +credential = ChainedTokenCredential( + ManagedIdentityCredential(client_id=""), + AzureCliCredential() +) +``` + +## Credential Types Table + +| Credential | Use Case | Auth Method | +|------------|----------|-------------| +| `DefaultAzureCredential` | Most scenarios | Auto-detect | +| `ManagedIdentityCredential` | Azure-hosted apps | Managed Identity | +| `ClientSecretCredential` | Service principal | Client secret | +| `ClientCertificateCredential` | Service principal | Certificate | +| `AzureCliCredential` | Local development | Azure CLI | +| `AzureDeveloperCliCredential` | Local development | Azure Developer CLI | +| `InteractiveBrowserCredential` | User sign-in | Browser OAuth | +| `DeviceCodeCredential` | Headless/SSH | Device code flow | + +## Getting Tokens Directly + +```python +from azure.identity import DefaultAzureCredential + +credential = DefaultAzureCredential() + +# Get token for a specific scope +token = credential.get_token("https://management.azure.com/.default") +print(f"Token expires: {token.expires_on}") + +# For Azure Database for PostgreSQL +token = credential.get_token("https://ossrdbms-aad.database.windows.net/.default") +``` + +## Async Client + +```python +from azure.identity.aio import DefaultAzureCredential +from azure.storage.blob.aio import BlobServiceClient + +async def main(): + credential = DefaultAzureCredential() + + async with BlobServiceClient( + account_url="https://.blob.core.windows.net", + credential=credential + ) as client: + # ... async operations + pass + + await credential.close() +``` + +## Best Practices + +1. **Use DefaultAzureCredential** for code that runs locally and in Azure +2. **Never hardcode credentials** โ€” use environment variables or managed identity +3. **Prefer managed identity** in production Azure deployments +4. **Use ChainedTokenCredential** when you need a custom credential order +5. **Close async credentials** explicitly or use context managers +6. **Set AZURE_CLIENT_ID** for user-assigned managed identities +7. **Exclude unused credentials** to speed up authentication + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-azure-ai-cloud/skills/azure-monitor-opentelemetry-ts/SKILL.md b/plugins/antigravity-bundle-azure-ai-cloud/skills/azure-monitor-opentelemetry-ts/SKILL.md new file mode 100644 index 00000000..2d31ec53 --- /dev/null +++ b/plugins/antigravity-bundle-azure-ai-cloud/skills/azure-monitor-opentelemetry-ts/SKILL.md @@ -0,0 +1,324 @@ +--- +name: azure-monitor-opentelemetry-ts +description: "Auto-instrument Node.js applications with distributed tracing, metrics, and logs." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Azure Monitor OpenTelemetry SDK for TypeScript + +Auto-instrument Node.js applications with distributed tracing, metrics, and logs. + +## Installation + +```bash +# Distro (recommended - auto-instrumentation) +npm install @azure/monitor-opentelemetry + +# Low-level exporters (custom OpenTelemetry setup) +npm install @azure/monitor-opentelemetry-exporter + +# Custom logs ingestion +npm install @azure/monitor-ingestion +``` + +## Environment Variables + +```bash +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=...;IngestionEndpoint=... +``` + +## Quick Start (Auto-Instrumentation) + +**IMPORTANT:** Call `useAzureMonitor()` BEFORE importing other modules. + +```typescript +import { useAzureMonitor } from "@azure/monitor-opentelemetry"; + +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); + +// Now import your application +import express from "express"; +const app = express(); +``` + +## ESM Support (Node.js 18.19+) + +```bash +node --import @azure/monitor-opentelemetry/loader ./dist/index.js +``` + +**package.json:** +```json +{ + "scripts": { + "start": "node --import @azure/monitor-opentelemetry/loader ./dist/index.js" + } +} +``` + +## Full Configuration + +```typescript +import { useAzureMonitor, AzureMonitorOpenTelemetryOptions } from "@azure/monitor-opentelemetry"; +import { resourceFromAttributes } from "@opentelemetry/resources"; + +const options: AzureMonitorOpenTelemetryOptions = { + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING, + storageDirectory: "/path/to/offline/storage", + disableOfflineStorage: false + }, + + // Sampling + samplingRatio: 1.0, // 0-1, percentage of traces + + // Features + enableLiveMetrics: true, + enableStandardMetrics: true, + enablePerformanceCounters: true, + + // Instrumentation libraries + instrumentationOptions: { + azureSdk: { enabled: true }, + http: { enabled: true }, + mongoDb: { enabled: true }, + mySql: { enabled: true }, + postgreSql: { enabled: true }, + redis: { enabled: true }, + bunyan: { enabled: false }, + winston: { enabled: false } + }, + + // Custom resource + resource: resourceFromAttributes({ "service.name": "my-service" }) +}; + +useAzureMonitor(options); +``` + +## Custom Traces + +```typescript +import { trace } from "@opentelemetry/api"; + +const tracer = trace.getTracer("my-tracer"); + +const span = tracer.startSpan("doWork"); +try { + span.setAttribute("component", "worker"); + span.setAttribute("operation.id", "42"); + span.addEvent("processing started"); + + // Your work here + +} catch (error) { + span.recordException(error as Error); + span.setStatus({ code: 2, message: (error as Error).message }); +} finally { + span.end(); +} +``` + +## Custom Metrics + +```typescript +import { metrics } from "@opentelemetry/api"; + +const meter = metrics.getMeter("my-meter"); + +// Counter +const counter = meter.createCounter("requests_total"); +counter.add(1, { route: "/api/users", method: "GET" }); + +// Histogram +const histogram = meter.createHistogram("request_duration_ms"); +histogram.record(150, { route: "/api/users" }); + +// Observable Gauge +const gauge = meter.createObservableGauge("active_connections"); +gauge.addCallback((result) => { + result.observe(getActiveConnections(), { pool: "main" }); +}); +``` + +## Manual Exporter Setup + +### Trace Exporter + +```typescript +import { AzureMonitorTraceExporter } from "@azure/monitor-opentelemetry-exporter"; +import { NodeTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-node"; + +const exporter = new AzureMonitorTraceExporter({ + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING +}); + +const provider = new NodeTracerProvider({ + spanProcessors: [new BatchSpanProcessor(exporter)] +}); + +provider.register(); +``` + +### Metric Exporter + +```typescript +import { AzureMonitorMetricExporter } from "@azure/monitor-opentelemetry-exporter"; +import { PeriodicExportingMetricReader, MeterProvider } from "@opentelemetry/sdk-metrics"; +import { metrics } from "@opentelemetry/api"; + +const exporter = new AzureMonitorMetricExporter({ + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING +}); + +const meterProvider = new MeterProvider({ + readers: [new PeriodicExportingMetricReader({ exporter })] +}); + +metrics.setGlobalMeterProvider(meterProvider); +``` + +### Log Exporter + +```typescript +import { AzureMonitorLogExporter } from "@azure/monitor-opentelemetry-exporter"; +import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs"; +import { logs } from "@opentelemetry/api-logs"; + +const exporter = new AzureMonitorLogExporter({ + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING +}); + +const loggerProvider = new LoggerProvider(); +loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(exporter)); + +logs.setGlobalLoggerProvider(loggerProvider); +``` + +## Custom Logs Ingestion + +```typescript +import { DefaultAzureCredential } from "@azure/identity"; +import { LogsIngestionClient, isAggregateLogsUploadError } from "@azure/monitor-ingestion"; + +const endpoint = "https://.ingest.monitor.azure.com"; +const ruleId = ""; +const streamName = "Custom-MyTable_CL"; + +const client = new LogsIngestionClient(endpoint, new DefaultAzureCredential()); + +const logs = [ + { + Time: new Date().toISOString(), + Computer: "Server1", + Message: "Application started", + Level: "Information" + } +]; + +try { + await client.upload(ruleId, streamName, logs); +} catch (error) { + if (isAggregateLogsUploadError(error)) { + for (const uploadError of error.errors) { + console.error("Failed logs:", uploadError.failedLogs); + } + } +} +``` + +## Custom Span Processor + +```typescript +import { SpanProcessor, ReadableSpan } from "@opentelemetry/sdk-trace-base"; +import { Span, Context, SpanKind, TraceFlags } from "@opentelemetry/api"; +import { useAzureMonitor } from "@azure/monitor-opentelemetry"; + +class FilteringSpanProcessor implements SpanProcessor { + forceFlush(): Promise { return Promise.resolve(); } + shutdown(): Promise { return Promise.resolve(); } + onStart(span: Span, context: Context): void {} + + onEnd(span: ReadableSpan): void { + // Add custom attributes + span.attributes["CustomDimension"] = "value"; + + // Filter out internal spans + if (span.kind === SpanKind.INTERNAL) { + span.spanContext().traceFlags = TraceFlags.NONE; + } + } +} + +useAzureMonitor({ + spanProcessors: [new FilteringSpanProcessor()] +}); +``` + +## Sampling + +```typescript +import { ApplicationInsightsSampler } from "@azure/monitor-opentelemetry-exporter"; +import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; + +// Sample 75% of traces +const sampler = new ApplicationInsightsSampler(0.75); + +const provider = new NodeTracerProvider({ sampler }); +``` + +## Shutdown + +```typescript +import { useAzureMonitor, shutdownAzureMonitor } from "@azure/monitor-opentelemetry"; + +useAzureMonitor(); + +// On application shutdown +process.on("SIGTERM", async () => { + await shutdownAzureMonitor(); + process.exit(0); +}); +``` + +## Key Types + +```typescript +import { + useAzureMonitor, + shutdownAzureMonitor, + AzureMonitorOpenTelemetryOptions, + InstrumentationOptions +} from "@azure/monitor-opentelemetry"; + +import { + AzureMonitorTraceExporter, + AzureMonitorMetricExporter, + AzureMonitorLogExporter, + ApplicationInsightsSampler, + AzureMonitorExporterOptions +} from "@azure/monitor-opentelemetry-exporter"; + +import { + LogsIngestionClient, + isAggregateLogsUploadError +} from "@azure/monitor-ingestion"; +``` + +## Best Practices + +1. **Call useAzureMonitor() first** - Before importing other modules +2. **Use ESM loader for ESM projects** - `--import @azure/monitor-opentelemetry/loader` +3. **Enable offline storage** - For reliable telemetry in disconnected scenarios +4. **Set sampling ratio** - For high-traffic applications +5. **Add custom dimensions** - Use span processors for enrichment +6. **Graceful shutdown** - Call `shutdownAzureMonitor()` to flush telemetry + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-azure-ai-cloud/skills/azure-search-documents-py/SKILL.md b/plugins/antigravity-bundle-azure-ai-cloud/skills/azure-search-documents-py/SKILL.md new file mode 100644 index 00000000..7bee4219 --- /dev/null +++ b/plugins/antigravity-bundle-azure-ai-cloud/skills/azure-search-documents-py/SKILL.md @@ -0,0 +1,531 @@ +--- +name: azure-search-documents-py +description: Azure AI Search SDK for Python. Use for vector search, hybrid search, semantic ranking, indexing, and skillsets. +risk: unknown +source: community +date_added: '2026-02-27' +--- + +# Azure AI Search SDK for Python + +Full-text, vector, and hybrid search with AI enrichment capabilities. + +## Installation + +```bash +pip install azure-search-documents +``` + +## Environment Variables + +```bash +AZURE_SEARCH_ENDPOINT=https://.search.windows.net +AZURE_SEARCH_API_KEY= +AZURE_SEARCH_INDEX_NAME= +``` + +## Authentication + +### API Key + +```python +from azure.search.documents import SearchClient +from azure.core.credentials import AzureKeyCredential + +client = SearchClient( + endpoint=os.environ["AZURE_SEARCH_ENDPOINT"], + index_name=os.environ["AZURE_SEARCH_INDEX_NAME"], + credential=AzureKeyCredential(os.environ["AZURE_SEARCH_API_KEY"]) +) +``` + +### Entra ID (Recommended) + +```python +from azure.search.documents import SearchClient +from azure.identity import DefaultAzureCredential + +client = SearchClient( + endpoint=os.environ["AZURE_SEARCH_ENDPOINT"], + index_name=os.environ["AZURE_SEARCH_INDEX_NAME"], + credential=DefaultAzureCredential() +) +``` + +## Client Types + +| Client | Purpose | +|--------|---------| +| `SearchClient` | Search and document operations | +| `SearchIndexClient` | Index management, synonym maps | +| `SearchIndexerClient` | Indexers, data sources, skillsets | + +## Create Index with Vector Field + +```python +from azure.search.documents.indexes import SearchIndexClient +from azure.search.documents.indexes.models import ( + SearchIndex, + SearchField, + SearchFieldDataType, + VectorSearch, + HnswAlgorithmConfiguration, + VectorSearchProfile, + SearchableField, + SimpleField +) + +index_client = SearchIndexClient(endpoint, AzureKeyCredential(key)) + +fields = [ + SimpleField(name="id", type=SearchFieldDataType.String, key=True), + SearchableField(name="title", type=SearchFieldDataType.String), + SearchableField(name="content", type=SearchFieldDataType.String), + SearchField( + name="content_vector", + type=SearchFieldDataType.Collection(SearchFieldDataType.Single), + searchable=True, + vector_search_dimensions=1536, + vector_search_profile_name="my-vector-profile" + ) +] + +vector_search = VectorSearch( + algorithms=[ + HnswAlgorithmConfiguration(name="my-hnsw") + ], + profiles=[ + VectorSearchProfile( + name="my-vector-profile", + algorithm_configuration_name="my-hnsw" + ) + ] +) + +index = SearchIndex( + name="my-index", + fields=fields, + vector_search=vector_search +) + +index_client.create_or_update_index(index) +``` + +## Upload Documents + +```python +from azure.search.documents import SearchClient + +client = SearchClient(endpoint, "my-index", AzureKeyCredential(key)) + +documents = [ + { + "id": "1", + "title": "Azure AI Search", + "content": "Full-text and vector search service", + "content_vector": [0.1, 0.2, ...] # 1536 dimensions + } +] + +result = client.upload_documents(documents) +print(f"Uploaded {len(result)} documents") +``` + +## Keyword Search + +```python +results = client.search( + search_text="azure search", + select=["id", "title", "content"], + top=10 +) + +for result in results: + print(f"{result['title']}: {result['@search.score']}") +``` + +## Vector Search + +```python +from azure.search.documents.models import VectorizedQuery + +# Your query embedding (1536 dimensions) +query_vector = get_embedding("semantic search capabilities") + +vector_query = VectorizedQuery( + vector=query_vector, + k_nearest_neighbors=10, + fields="content_vector" +) + +results = client.search( + vector_queries=[vector_query], + select=["id", "title", "content"] +) + +for result in results: + print(f"{result['title']}: {result['@search.score']}") +``` + +## Hybrid Search (Vector + Keyword) + +```python +from azure.search.documents.models import VectorizedQuery + +vector_query = VectorizedQuery( + vector=query_vector, + k_nearest_neighbors=10, + fields="content_vector" +) + +results = client.search( + search_text="azure search", + vector_queries=[vector_query], + select=["id", "title", "content"], + top=10 +) +``` + +## Semantic Ranking + +```python +from azure.search.documents.models import QueryType + +results = client.search( + search_text="what is azure search", + query_type=QueryType.SEMANTIC, + semantic_configuration_name="my-semantic-config", + select=["id", "title", "content"], + top=10 +) + +for result in results: + print(f"{result['title']}") + if result.get("@search.captions"): + print(f" Caption: {result['@search.captions'][0].text}") +``` + +## Filters + +```python +results = client.search( + search_text="*", + filter="category eq 'Technology' and rating gt 4", + order_by=["rating desc"], + select=["id", "title", "category", "rating"] +) +``` + +## Facets + +```python +results = client.search( + search_text="*", + facets=["category,count:10", "rating"], + top=0 # Only get facets, no documents +) + +for facet_name, facet_values in results.get_facets().items(): + print(f"{facet_name}:") + for facet in facet_values: + print(f" {facet['value']}: {facet['count']}") +``` + +## Autocomplete & Suggest + +```python +# Autocomplete +results = client.autocomplete( + search_text="sea", + suggester_name="my-suggester", + mode="twoTerms" +) + +# Suggest +results = client.suggest( + search_text="sea", + suggester_name="my-suggester", + select=["title"] +) +``` + +## Indexer with Skillset + +```python +from azure.search.documents.indexes import SearchIndexerClient +from azure.search.documents.indexes.models import ( + SearchIndexer, + SearchIndexerDataSourceConnection, + SearchIndexerSkillset, + EntityRecognitionSkill, + InputFieldMappingEntry, + OutputFieldMappingEntry +) + +indexer_client = SearchIndexerClient(endpoint, AzureKeyCredential(key)) + +# Create data source +data_source = SearchIndexerDataSourceConnection( + name="my-datasource", + type="azureblob", + connection_string=connection_string, + container={"name": "documents"} +) +indexer_client.create_or_update_data_source_connection(data_source) + +# Create skillset +skillset = SearchIndexerSkillset( + name="my-skillset", + skills=[ + EntityRecognitionSkill( + inputs=[InputFieldMappingEntry(name="text", source="/document/content")], + outputs=[OutputFieldMappingEntry(name="organizations", target_name="organizations")] + ) + ] +) +indexer_client.create_or_update_skillset(skillset) + +# Create indexer +indexer = SearchIndexer( + name="my-indexer", + data_source_name="my-datasource", + target_index_name="my-index", + skillset_name="my-skillset" +) +indexer_client.create_or_update_indexer(indexer) +``` + +## Best Practices + +1. **Use hybrid search** for best relevance combining vector and keyword +2. **Enable semantic ranking** for natural language queries +3. **Index in batches** of 100-1000 documents for efficiency +4. **Use filters** to narrow results before ranking +5. **Configure vector dimensions** to match your embedding model +6. **Use HNSW algorithm** for large-scale vector search +7. **Create suggesters** at index creation time (cannot add later) + +## Reference Files + +| File | Contents | +|------|----------| +| references/vector-search.md | HNSW configuration, integrated vectorization, multi-vector queries | +| references/semantic-ranking.md | Semantic configuration, captions, answers, hybrid patterns | +| scripts/setup_vector_index.py | CLI script to create vector-enabled search index | + + +--- + +## Additional Azure AI Search Patterns + +# Azure AI Search Python SDK + +Write clean, idiomatic Python code for Azure AI Search using `azure-search-documents`. + +## Installation + +```bash +pip install azure-search-documents azure-identity +``` + +## Environment Variables + +```bash +AZURE_SEARCH_ENDPOINT=https://.search.windows.net +AZURE_SEARCH_INDEX_NAME= +# For API key auth (not recommended for production) +AZURE_SEARCH_API_KEY= +``` + +## Authentication + +**DefaultAzureCredential (preferred)**: +```python +from azure.identity import DefaultAzureCredential +from azure.search.documents import SearchClient + +credential = DefaultAzureCredential() +client = SearchClient(endpoint, index_name, credential) +``` + +**API Key**: +```python +from azure.core.credentials import AzureKeyCredential +from azure.search.documents import SearchClient + +client = SearchClient(endpoint, index_name, AzureKeyCredential(api_key)) +``` + +## Client Selection + +| Client | Purpose | +|--------|---------| +| `SearchClient` | Query indexes, upload/update/delete documents | +| `SearchIndexClient` | Create/manage indexes, knowledge sources, knowledge bases | +| `SearchIndexerClient` | Manage indexers, skillsets, data sources | +| `KnowledgeBaseRetrievalClient` | Agentic retrieval with LLM-powered Q&A | + +## Index Creation Pattern + +```python +from azure.search.documents.indexes import SearchIndexClient +from azure.search.documents.indexes.models import ( + SearchIndex, SearchField, VectorSearch, VectorSearchProfile, + HnswAlgorithmConfiguration, AzureOpenAIVectorizer, + AzureOpenAIVectorizerParameters, SemanticSearch, + SemanticConfiguration, SemanticPrioritizedFields, SemanticField +) + +index = SearchIndex( + name=index_name, + fields=[ + SearchField(name="id", type="Edm.String", key=True), + SearchField(name="content", type="Edm.String", searchable=True), + SearchField(name="embedding", type="Collection(Edm.Single)", + vector_search_dimensions=3072, + vector_search_profile_name="vector-profile"), + ], + vector_search=VectorSearch( + profiles=[VectorSearchProfile( + name="vector-profile", + algorithm_configuration_name="hnsw-algo", + vectorizer_name="openai-vectorizer" + )], + algorithms=[HnswAlgorithmConfiguration(name="hnsw-algo")], + vectorizers=[AzureOpenAIVectorizer( + vectorizer_name="openai-vectorizer", + parameters=AzureOpenAIVectorizerParameters( + resource_url=aoai_endpoint, + deployment_name=embedding_deployment, + model_name=embedding_model + ) + )] + ), + semantic_search=SemanticSearch( + default_configuration_name="semantic-config", + configurations=[SemanticConfiguration( + name="semantic-config", + prioritized_fields=SemanticPrioritizedFields( + content_fields=[SemanticField(field_name="content")] + ) + )] + ) +) + +index_client = SearchIndexClient(endpoint, credential) +index_client.create_or_update_index(index) +``` + +## Document Operations + +```python +from azure.search.documents import SearchIndexingBufferedSender + +# Batch upload with automatic batching +with SearchIndexingBufferedSender(endpoint, index_name, credential) as sender: + sender.upload_documents(documents) + +# Direct operations via SearchClient +search_client = SearchClient(endpoint, index_name, credential) +search_client.upload_documents(documents) # Add new +search_client.merge_documents(documents) # Update existing +search_client.merge_or_upload_documents(documents) # Upsert +search_client.delete_documents(documents) # Remove +``` + +## Search Patterns + +```python +# Basic search +results = search_client.search(search_text="query") + +# Vector search +from azure.search.documents.models import VectorizedQuery + +results = search_client.search( + search_text=None, + vector_queries=[VectorizedQuery( + vector=embedding, + k_nearest_neighbors=5, + fields="embedding" + )] +) + +# Hybrid search (vector + keyword) +results = search_client.search( + search_text="query", + vector_queries=[VectorizedQuery(vector=embedding, k_nearest_neighbors=5, fields="embedding")], + query_type="semantic", + semantic_configuration_name="semantic-config" +) + +# With filters +results = search_client.search( + search_text="query", + filter="category eq 'technology'", + select=["id", "title", "content"], + top=10 +) +``` + +## Agentic Retrieval (Knowledge Bases) + +For LLM-powered Q&A with answer synthesis, see references/agentic-retrieval.md. + +Key concepts: +- **Knowledge Source**: Points to a search index +- **Knowledge Base**: Wraps knowledge sources + LLM for query planning and synthesis +- **Output modes**: `EXTRACTIVE_DATA` (raw chunks) or `ANSWER_SYNTHESIS` (LLM-generated answers) + +## Async Pattern + +```python +from azure.search.documents.aio import SearchClient + +async with SearchClient(endpoint, index_name, credential) as client: + results = await client.search(search_text="query") + async for result in results: + print(result["title"]) +``` + +## Best Practices + +1. **Use environment variables** for endpoints, keys, and deployment names +2. **Prefer `DefaultAzureCredential`** over API keys for production +3. **Use `SearchIndexingBufferedSender`** for batch uploads (handles batching/retries) +4. **Always define semantic configuration** for agentic retrieval indexes +5. **Use `create_or_update_index`** for idempotent index creation +6. **Close clients** with context managers or explicit `close()` + +## Field Types Reference + +| EDM Type | Python | Notes | +|----------|--------|-------| +| `Edm.String` | str | Searchable text | +| `Edm.Int32` | int | Integer | +| `Edm.Int64` | int | Long integer | +| `Edm.Double` | float | Floating point | +| `Edm.Boolean` | bool | True/False | +| `Edm.DateTimeOffset` | datetime | ISO 8601 | +| `Collection(Edm.Single)` | List[float] | Vector embeddings | +| `Collection(Edm.String)` | List[str] | String arrays | + +## Error Handling + +```python +from azure.core.exceptions import ( + HttpResponseError, + ResourceNotFoundError, + ResourceExistsError +) + +try: + result = search_client.get_document(key="123") +except ResourceNotFoundError: + print("Document not found") +except HttpResponseError as e: + print(f"Search error: {e.message}") +``` + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-business-analyst/.codex-plugin/plugin.json b/plugins/antigravity-bundle-business-analyst/.codex-plugin/plugin.json new file mode 100644 index 00000000..77b4f7c1 --- /dev/null +++ b/plugins/antigravity-bundle-business-analyst/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-business-analyst", + "version": "8.10.0", + "description": "Install the \"Business Analyst\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "business-analyst", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Business Analyst", + "shortDescription": "Product & Business ยท 5 curated skills", + "longDescription": "For data-driven decision making. Covers Business Analyst, Startup Metrics Framework, and 3 more skills.", + "developerName": "sickn33 and contributors", + "category": "Product & Business", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-business-analyst/skills/business-analyst/SKILL.md b/plugins/antigravity-bundle-business-analyst/skills/business-analyst/SKILL.md new file mode 100644 index 00000000..0caf5c9c --- /dev/null +++ b/plugins/antigravity-bundle-business-analyst/skills/business-analyst/SKILL.md @@ -0,0 +1,180 @@ +--- +name: business-analyst +description: Master modern business analysis with AI-powered analytics, real-time dashboards, and data-driven insights. Build comprehensive KPI frameworks, predictive models, and strategic recommendations. +risk: unknown +source: community +date_added: '2026-02-27' +--- + +## Use this skill when + +- Working on business analyst tasks or workflows +- Needing guidance, best practices, or checklists for business analyst + +## Do not use this skill when + +- The task is unrelated to business analyst +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +You are an expert business analyst specializing in data-driven decision making through advanced analytics, modern BI tools, and strategic business intelligence. + +## Purpose + +Expert business analyst focused on transforming complex business data into actionable insights and strategic recommendations. Masters modern analytics platforms, predictive modeling, and data storytelling to drive business growth and optimize operational efficiency. Combines technical proficiency with business acumen to deliver comprehensive analysis that influences executive decision-making. + +## Capabilities + +### Modern Analytics Platforms and Tools + +- Advanced dashboard creation with Tableau, Power BI, Looker, and Qlik Sense +- Cloud-native analytics with Snowflake, BigQuery, and Databricks +- Real-time analytics and streaming data visualization +- Self-service BI implementation and user adoption strategies +- Custom analytics solutions with Python, R, and SQL +- Mobile-responsive dashboard design and optimization +- Automated report generation and distribution systems + +### AI-Powered Business Intelligence + +- Machine learning for predictive analytics and forecasting +- Natural language processing for sentiment and text analysis +- AI-driven anomaly detection and alerting systems +- Automated insight generation and narrative reporting +- Predictive modeling for customer behavior and market trends +- Computer vision for image and video analytics +- Recommendation engines for business optimization + +### Strategic KPI Framework Development + +- Comprehensive KPI strategy design and implementation +- North Star metrics identification and tracking +- OKR (Objectives and Key Results) framework development +- Balanced scorecard implementation and management +- Performance measurement system design +- Metric hierarchy and dependency mapping +- KPI benchmarking against industry standards + +### Financial Analysis and Modeling + +- Advanced revenue modeling and forecasting techniques +- Customer lifetime value (CLV) and acquisition cost (CAC) optimization +- Cohort analysis and retention modeling +- Unit economics analysis and profitability modeling +- Scenario planning and sensitivity analysis +- Financial planning and analysis (FP&A) automation +- Investment analysis and ROI calculations + +### Customer and Market Analytics + +- Customer segmentation and persona development +- Churn prediction and prevention strategies +- Market sizing and total addressable market (TAM) analysis +- Competitive intelligence and market positioning +- Product-market fit analysis and validation +- Customer journey mapping and funnel optimization +- Voice of customer (VoC) analysis and insights + +### Data Visualization and Storytelling + +- Advanced data visualization techniques and best practices +- Interactive dashboard design and user experience optimization +- Executive presentation design and narrative development +- Data storytelling frameworks and methodologies +- Visual analytics for pattern recognition and insight discovery +- Color theory and design principles for business audiences +- Accessibility standards for inclusive data visualization + +### Statistical Analysis and Research + +- Advanced statistical analysis and hypothesis testing +- A/B testing design, execution, and analysis +- Survey design and market research methodologies +- Experimental design and causal inference +- Time series analysis and forecasting +- Multivariate analysis and dimensionality reduction +- Statistical modeling for business applications + +### Data Management and Quality + +- Data governance frameworks and implementation +- Data quality assessment and improvement strategies +- Master data management and data integration +- Data warehouse design and dimensional modeling +- ETL/ELT process design and optimization +- Data lineage and impact analysis +- Privacy and compliance considerations (GDPR, CCPA) + +### Business Process Optimization + +- Process mining and workflow analysis +- Operational efficiency measurement and improvement +- Supply chain analytics and optimization +- Resource allocation and capacity planning +- Performance monitoring and alerting systems +- Automation opportunity identification and assessment +- Change management for analytics initiatives + +### Industry-Specific Analytics + +- E-commerce and retail analytics (conversion, merchandising) +- SaaS metrics and subscription business analysis +- Healthcare analytics and population health insights +- Financial services risk and compliance analytics +- Manufacturing and IoT sensor data analysis +- Marketing attribution and campaign effectiveness +- Human resources analytics and workforce planning + +## Behavioral Traits + +- Focuses on business impact and actionable recommendations +- Translates complex technical concepts for non-technical stakeholders +- Maintains objectivity while providing strategic guidance +- Validates assumptions through data-driven testing +- Communicates insights through compelling visual narratives +- Balances detail with executive-level summarization +- Considers ethical implications of data use and analysis +- Stays current with industry trends and best practices +- Collaborates effectively across functional teams +- Questions data quality and methodology rigorously + +## Knowledge Base + +- Modern BI and analytics platform ecosystems +- Statistical analysis and machine learning techniques +- Data visualization theory and design principles +- Financial modeling and business valuation methods +- Industry benchmarks and performance standards +- Data governance and quality management practices +- Cloud analytics platforms and data warehousing +- Agile analytics and continuous improvement methodologies +- Privacy regulations and ethical data use guidelines +- Business strategy frameworks and analytical approaches + +## Response Approach + +1. **Define business objectives** and success criteria clearly +2. **Assess data availability** and quality for analysis +3. **Design analytical framework** with appropriate methodologies +4. **Execute comprehensive analysis** with statistical rigor +5. **Create compelling visualizations** that tell the data story +6. **Develop actionable recommendations** with implementation guidance +7. **Present insights effectively** to target audiences +8. **Plan for ongoing monitoring** and continuous improvement + +## Example Interactions + +- "Analyze our customer churn patterns and create a predictive model to identify at-risk customers" +- "Build a comprehensive revenue dashboard with drill-down capabilities and automated alerts" +- "Design an A/B testing framework for our product feature releases" +- "Create a market sizing analysis for our new product line with TAM/SAM/SOM breakdown" +- "Develop a cohort-based LTV model and optimize our customer acquisition strategy" +- "Build an executive dashboard showing key business metrics with trend analysis" +- "Analyze our sales funnel performance and identify optimization opportunities" +- "Create a competitive intelligence framework with automated data collection" diff --git a/plugins/antigravity-bundle-business-analyst/skills/kpi-dashboard-design/SKILL.md b/plugins/antigravity-bundle-business-analyst/skills/kpi-dashboard-design/SKILL.md new file mode 100644 index 00000000..ee13bc6e --- /dev/null +++ b/plugins/antigravity-bundle-business-analyst/skills/kpi-dashboard-design/SKILL.md @@ -0,0 +1,443 @@ +--- +name: kpi-dashboard-design +description: "Comprehensive patterns for designing effective Key Performance Indicator (KPI) dashboards that drive business decisions." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# KPI Dashboard Design + +Comprehensive patterns for designing effective Key Performance Indicator (KPI) dashboards that drive business decisions. + +## Do not use this skill when + +- The task is unrelated to kpi dashboard design +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Use this skill when + +- Designing executive dashboards +- Selecting meaningful KPIs +- Building real-time monitoring displays +- Creating department-specific metrics views +- Improving existing dashboard layouts +- Establishing metric governance + +## Core Concepts + +### 1. KPI Framework + +| Level | Focus | Update Frequency | Audience | +| --------------- | ---------------- | ----------------- | ---------- | +| **Strategic** | Long-term goals | Monthly/Quarterly | Executives | +| **Tactical** | Department goals | Weekly/Monthly | Managers | +| **Operational** | Day-to-day | Real-time/Daily | Teams | + +### 2. SMART KPIs + +``` +Specific: Clear definition +Measurable: Quantifiable +Achievable: Realistic targets +Relevant: Aligned to goals +Time-bound: Defined period +``` + +### 3. Dashboard Hierarchy + +``` +โ”œโ”€โ”€ Executive Summary (1 page) +โ”‚ โ”œโ”€โ”€ 4-6 headline KPIs +โ”‚ โ”œโ”€โ”€ Trend indicators +โ”‚ โ””โ”€โ”€ Key alerts +โ”œโ”€โ”€ Department Views +โ”‚ โ”œโ”€โ”€ Sales Dashboard +โ”‚ โ”œโ”€โ”€ Marketing Dashboard +โ”‚ โ”œโ”€โ”€ Operations Dashboard +โ”‚ โ””โ”€โ”€ Finance Dashboard +โ””โ”€โ”€ Detailed Drilldowns + โ”œโ”€โ”€ Individual metrics + โ””โ”€โ”€ Root cause analysis +``` + +## Common KPIs by Department + +### Sales KPIs + +```yaml +Revenue Metrics: + - Monthly Recurring Revenue (MRR) + - Annual Recurring Revenue (ARR) + - Average Revenue Per User (ARPU) + - Revenue Growth Rate + +Pipeline Metrics: + - Sales Pipeline Value + - Win Rate + - Average Deal Size + - Sales Cycle Length + +Activity Metrics: + - Calls/Emails per Rep + - Demos Scheduled + - Proposals Sent + - Close Rate +``` + +### Marketing KPIs + +```yaml +Acquisition: + - Cost Per Acquisition (CPA) + - Customer Acquisition Cost (CAC) + - Lead Volume + - Marketing Qualified Leads (MQL) + +Engagement: + - Website Traffic + - Conversion Rate + - Email Open/Click Rate + - Social Engagement + +ROI: + - Marketing ROI + - Campaign Performance + - Channel Attribution + - CAC Payback Period +``` + +### Product KPIs + +```yaml +Usage: + - Daily/Monthly Active Users (DAU/MAU) + - Session Duration + - Feature Adoption Rate + - Stickiness (DAU/MAU) + +Quality: + - Net Promoter Score (NPS) + - Customer Satisfaction (CSAT) + - Bug/Issue Count + - Time to Resolution + +Growth: + - User Growth Rate + - Activation Rate + - Retention Rate + - Churn Rate +``` + +### Finance KPIs + +```yaml +Profitability: + - Gross Margin + - Net Profit Margin + - EBITDA + - Operating Margin + +Liquidity: + - Current Ratio + - Quick Ratio + - Cash Flow + - Working Capital + +Efficiency: + - Revenue per Employee + - Operating Expense Ratio + - Days Sales Outstanding + - Inventory Turnover +``` + +## Dashboard Layout Patterns + +### Pattern 1: Executive Summary + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ EXECUTIVE DASHBOARD [Date Range โ–ผ] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ REVENUE โ”‚ PROFIT โ”‚ CUSTOMERS โ”‚ NPS SCORE โ”‚ +โ”‚ $2.4M โ”‚ $450K โ”‚ 12,450 โ”‚ 72 โ”‚ +โ”‚ โ–ฒ 12% โ”‚ โ–ฒ 8% โ”‚ โ–ฒ 15% โ”‚ โ–ฒ 5pts โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ Revenue Trend โ”‚ Revenue by Product โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ /\ /\ โ”‚ โ”‚ โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 45% โ”‚ โ”‚ +โ”‚ โ”‚ / \ / \ /\ โ”‚ โ”‚ โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 32% โ”‚ โ”‚ +โ”‚ โ”‚ / \/ \ / \ โ”‚ โ”‚ โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆ 18% โ”‚ โ”‚ +โ”‚ โ”‚ / \/ \ โ”‚ โ”‚ โ”‚ โ–ˆโ–ˆ 5% โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ๐Ÿ”ด Alert: Churn rate exceeded threshold (>5%) โ”‚ +โ”‚ ๐ŸŸก Warning: Support ticket volume 20% above average โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Pattern 2: SaaS Metrics Dashboard + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ SAAS METRICS Jan 2024 [Monthly โ–ผ] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ MRR GROWTH โ”‚ +โ”‚ โ”‚ MRR โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ $125,000 โ”‚ โ”‚ โ”‚ /โ”€โ”€ โ”‚ โ”‚ +โ”‚ โ”‚ โ–ฒ 8% โ”‚ โ”‚ โ”‚ /โ”€โ”€โ”€โ”€/ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ /โ”€โ”€โ”€โ”€/ โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ /โ”€โ”€โ”€โ”€/ โ”‚ โ”‚ +โ”‚ โ”‚ ARR โ”‚ โ”‚ โ”‚ /โ”€โ”€โ”€โ”€/ โ”‚ โ”‚ +โ”‚ โ”‚ $1,500,000 โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ–ฒ 15% โ”‚ โ”‚ J F M A M J J A S O N D โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ UNIT ECONOMICS โ”‚ COHORT RETENTION โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ CAC: $450 โ”‚ Month 1: โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 100% โ”‚ +โ”‚ LTV: $2,700 โ”‚ Month 3: โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 85% โ”‚ +โ”‚ LTV/CAC: 6.0x โ”‚ Month 6: โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 80% โ”‚ +โ”‚ โ”‚ Month 12: โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 72% โ”‚ +โ”‚ Payback: 4 months โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ CHURN ANALYSIS โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Gross โ”‚ Net โ”‚ Logo โ”‚ Expansion โ”‚ โ”‚ +โ”‚ โ”‚ 4.2% โ”‚ 1.8% โ”‚ 3.1% โ”‚ 2.4% โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Pattern 3: Real-time Operations + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ OPERATIONS CENTER Live โ— Last: 10:42:15 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ SYSTEM HEALTH โ”‚ SERVICE STATUS โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ CPU MEM DISK โ”‚ โ”‚ โ— API Gateway Healthy โ”‚ +โ”‚ โ”‚ 45% 72% 58% โ”‚ โ”‚ โ— User Service Healthy โ”‚ +โ”‚ โ”‚ โ–ˆโ–ˆโ–ˆ โ–ˆโ–ˆโ–ˆโ–ˆ โ–ˆโ–ˆโ–ˆ โ”‚ โ”‚ โ— Payment Service Degraded โ”‚ +โ”‚ โ”‚ โ–ˆโ–ˆโ–ˆ โ–ˆโ–ˆโ–ˆโ–ˆ โ–ˆโ–ˆโ–ˆ โ”‚ โ”‚ โ— Database Healthy โ”‚ +โ”‚ โ”‚ โ–ˆโ–ˆโ–ˆ โ–ˆโ–ˆโ–ˆโ–ˆ โ–ˆโ–ˆโ–ˆ โ”‚ โ”‚ โ— Cache Healthy โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ REQUEST THROUGHPUT โ”‚ ERROR RATE โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ–โ–‚โ–ƒโ–„โ–…โ–†โ–‡โ–ˆโ–‡โ–†โ–…โ–„โ–ƒโ–‚โ–โ–‚โ–ƒโ–„โ–… โ”‚ โ”‚ โ”‚ โ–โ–โ–โ–โ–โ–‚โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–โ– โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ Current: 12,450 req/s โ”‚ Current: 0.02% โ”‚ +โ”‚ Peak: 18,200 req/s โ”‚ Threshold: 1.0% โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ RECENT ALERTS โ”‚ +โ”‚ 10:40 ๐ŸŸก High latency on payment-service (p99 > 500ms) โ”‚ +โ”‚ 10:35 ๐ŸŸข Resolved: Database connection pool recovered โ”‚ +โ”‚ 10:22 ๐Ÿ”ด Payment service circuit breaker tripped โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## Implementation Patterns + +### SQL for KPI Calculations + +```sql +-- Monthly Recurring Revenue (MRR) +WITH mrr_calculation AS ( + SELECT + DATE_TRUNC('month', billing_date) AS month, + SUM( + CASE subscription_interval + WHEN 'monthly' THEN amount + WHEN 'yearly' THEN amount / 12 + WHEN 'quarterly' THEN amount / 3 + END + ) AS mrr + FROM subscriptions + WHERE status = 'active' + GROUP BY DATE_TRUNC('month', billing_date) +) +SELECT + month, + mrr, + LAG(mrr) OVER (ORDER BY month) AS prev_mrr, + (mrr - LAG(mrr) OVER (ORDER BY month)) / LAG(mrr) OVER (ORDER BY month) * 100 AS growth_pct +FROM mrr_calculation; + +-- Cohort Retention +WITH cohorts AS ( + SELECT + user_id, + DATE_TRUNC('month', created_at) AS cohort_month + FROM users +), +activity AS ( + SELECT + user_id, + DATE_TRUNC('month', event_date) AS activity_month + FROM user_events + WHERE event_type = 'active_session' +) +SELECT + c.cohort_month, + EXTRACT(MONTH FROM age(a.activity_month, c.cohort_month)) AS months_since_signup, + COUNT(DISTINCT a.user_id) AS active_users, + COUNT(DISTINCT a.user_id)::FLOAT / COUNT(DISTINCT c.user_id) * 100 AS retention_rate +FROM cohorts c +LEFT JOIN activity a ON c.user_id = a.user_id + AND a.activity_month >= c.cohort_month +GROUP BY c.cohort_month, EXTRACT(MONTH FROM age(a.activity_month, c.cohort_month)) +ORDER BY c.cohort_month, months_since_signup; + +-- Customer Acquisition Cost (CAC) +SELECT + DATE_TRUNC('month', acquired_date) AS month, + SUM(marketing_spend) / NULLIF(COUNT(new_customers), 0) AS cac, + SUM(marketing_spend) AS total_spend, + COUNT(new_customers) AS customers_acquired +FROM ( + SELECT + DATE_TRUNC('month', u.created_at) AS acquired_date, + u.id AS new_customers, + m.spend AS marketing_spend + FROM users u + JOIN marketing_spend m ON DATE_TRUNC('month', u.created_at) = m.month + WHERE u.source = 'marketing' +) acquisition +GROUP BY DATE_TRUNC('month', acquired_date); +``` + +### Python Dashboard Code (Streamlit) + +```python +import streamlit as st +import pandas as pd +import plotly.express as px +import plotly.graph_objects as go + +st.set_page_config(page_title="KPI Dashboard", layout="wide") + +# Header with date filter +col1, col2 = st.columns([3, 1]) +with col1: + st.title("Executive Dashboard") +with col2: + date_range = st.selectbox( + "Period", + ["Last 7 Days", "Last 30 Days", "Last Quarter", "YTD"] + ) + +# KPI Cards +def metric_card(label, value, delta, prefix="", suffix=""): + delta_color = "green" if delta >= 0 else "red" + delta_arrow = "โ–ฒ" if delta >= 0 else "โ–ผ" + st.metric( + label=label, + value=f"{prefix}{value:,.0f}{suffix}", + delta=f"{delta_arrow} {abs(delta):.1f}%" + ) + +col1, col2, col3, col4 = st.columns(4) +with col1: + metric_card("Revenue", 2400000, 12.5, prefix="$") +with col2: + metric_card("Customers", 12450, 15.2) +with col3: + metric_card("NPS Score", 72, 5.0) +with col4: + metric_card("Churn Rate", 4.2, -0.8, suffix="%") + +# Charts +col1, col2 = st.columns(2) + +with col1: + st.subheader("Revenue Trend") + revenue_data = pd.DataFrame({ + 'Month': pd.date_range('2024-01-01', periods=12, freq='M'), + 'Revenue': [180000, 195000, 210000, 225000, 240000, 255000, + 270000, 285000, 300000, 315000, 330000, 345000] + }) + fig = px.line(revenue_data, x='Month', y='Revenue', + line_shape='spline', markers=True) + fig.update_layout(height=300) + st.plotly_chart(fig, use_container_width=True) + +with col2: + st.subheader("Revenue by Product") + product_data = pd.DataFrame({ + 'Product': ['Enterprise', 'Professional', 'Starter', 'Other'], + 'Revenue': [45, 32, 18, 5] + }) + fig = px.pie(product_data, values='Revenue', names='Product', + hole=0.4) + fig.update_layout(height=300) + st.plotly_chart(fig, use_container_width=True) + +# Cohort Heatmap +st.subheader("Cohort Retention") +cohort_data = pd.DataFrame({ + 'Cohort': ['Jan', 'Feb', 'Mar', 'Apr', 'May'], + 'M0': [100, 100, 100, 100, 100], + 'M1': [85, 87, 84, 86, 88], + 'M2': [78, 80, 76, 79, None], + 'M3': [72, 74, 70, None, None], + 'M4': [68, 70, None, None, None], +}) +fig = go.Figure(data=go.Heatmap( + z=cohort_data.iloc[:, 1:].values, + x=['M0', 'M1', 'M2', 'M3', 'M4'], + y=cohort_data['Cohort'], + colorscale='Blues', + text=cohort_data.iloc[:, 1:].values, + texttemplate='%{text}%', + textfont={"size": 12}, +)) +fig.update_layout(height=250) +st.plotly_chart(fig, use_container_width=True) + +# Alerts Section +st.subheader("Alerts") +alerts = [ + {"level": "error", "message": "Churn rate exceeded threshold (>5%)"}, + {"level": "warning", "message": "Support ticket volume 20% above average"}, +] +for alert in alerts: + if alert["level"] == "error": + st.error(f"๐Ÿ”ด {alert['message']}") + elif alert["level"] == "warning": + st.warning(f"๐ŸŸก {alert['message']}") +``` + +## Best Practices + +### Do's + +- **Limit to 5-7 KPIs** - Focus on what matters +- **Show context** - Comparisons, trends, targets +- **Use consistent colors** - Red=bad, green=good +- **Enable drilldown** - From summary to detail +- **Update appropriately** - Match metric frequency + +### Don'ts + +- **Don't show vanity metrics** - Focus on actionable data +- **Don't overcrowd** - White space aids comprehension +- **Don't use 3D charts** - They distort perception +- **Don't hide methodology** - Document calculations +- **Don't ignore mobile** - Ensure responsive design + +## Resources + +- [Stephen Few's Dashboard Design](https://www.perceptualedge.com/articles/visual_business_intelligence/rules_for_using_color.pdf) +- [Edward Tufte's Principles](https://www.edwardtufte.com/tufte/) +- [Google Data Studio Gallery](https://datastudio.google.com/gallery) diff --git a/plugins/antigravity-bundle-business-analyst/skills/market-sizing-analysis/SKILL.md b/plugins/antigravity-bundle-business-analyst/skills/market-sizing-analysis/SKILL.md new file mode 100644 index 00000000..d9a17212 --- /dev/null +++ b/plugins/antigravity-bundle-business-analyst/skills/market-sizing-analysis/SKILL.md @@ -0,0 +1,423 @@ +--- +name: market-sizing-analysis +description: "Comprehensive market sizing methodologies for calculating Total Addressable Market (TAM), Serviceable Available Market (SAM), and Serviceable Obtainable Market (SOM) for startup opportunities." +risk: unknown +source: community +date_added: '2026-02-27' +--- + +# Market Sizing Analysis + +Comprehensive market sizing methodologies for calculating Total Addressable Market (TAM), Serviceable Available Market (SAM), and Serviceable Obtainable Market (SOM) for startup opportunities. + +## Use this skill when + +- Working on market sizing analysis tasks or workflows +- Needing guidance, best practices, or checklists for market sizing analysis + +## Do not use this skill when + +- The task is unrelated to market sizing analysis +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Overview + +Market sizing provides the foundation for startup strategy, fundraising, and business planning. Calculate market opportunity using three complementary methodologies: top-down (industry reports), bottom-up (customer segment calculations), and value theory (willingness to pay). + +## Core Concepts + +### The Three-Tier Market Framework + +**TAM (Total Addressable Market)** +- Total revenue opportunity if achieving 100% market share +- Defines the universe of potential customers +- Used for long-term vision and market validation +- Example: All email marketing software revenue globally + +**SAM (Serviceable Available Market)** +- Portion of TAM targetable with current product/service +- Accounts for geographic, segment, or capability constraints +- Represents realistic addressable opportunity +- Example: AI-powered email marketing for e-commerce in North America + +**SOM (Serviceable Obtainable Market)** +- Realistic market share achievable in 3-5 years +- Accounts for competition, resources, and market dynamics +- Used for financial projections and fundraising +- Example: 2-5% of SAM based on competitive landscape + +### When to Use Each Methodology + +**Top-Down Analysis** +- Use when established market research exists +- Best for mature, well-defined markets +- Validates market existence and growth +- Starts with industry reports and narrows down + +**Bottom-Up Analysis** +- Use when targeting specific customer segments +- Best for new or niche markets +- Most credible for investors +- Builds from customer data and pricing + +**Value Theory** +- Use when creating new market categories +- Best for disruptive innovations +- Estimates based on value creation +- Calculates willingness to pay for problem solution + +## Three-Methodology Framework + +### Methodology 1: Top-Down Analysis + +Start with total market size and narrow to addressable segments. + +**Process:** +1. Identify total market category from research reports +2. Apply geographic filters (target regions) +3. Apply segment filters (target industries/customers) +4. Calculate competitive positioning adjustments + +**Formula:** +``` +TAM = Total Market Category Size +SAM = TAM ร— Geographic % ร— Segment % +SOM = SAM ร— Realistic Capture Rate (2-5%) +``` + +**When to use:** Established markets with available research (e.g., SaaS, fintech, e-commerce) + +**Strengths:** Quick, uses credible data, validates market existence + +**Limitations:** May overestimate for new categories, less granular + +### Methodology 2: Bottom-Up Analysis + +Build market size from customer segment calculations. + +**Process:** +1. Define target customer segments +2. Estimate number of potential customers per segment +3. Determine average revenue per customer +4. Calculate realistic penetration rates + +**Formula:** +``` +TAM = ฮฃ (Segment Size ร— Annual Revenue per Customer) +SAM = TAM ร— (Segments You Can Serve / Total Segments) +SOM = SAM ร— Realistic Penetration Rate (Year 3-5) +``` + +**When to use:** B2B, niche markets, specific customer segments + +**Strengths:** Most credible for investors, granular, defensible + +**Limitations:** Requires detailed customer research, time-intensive + +### Methodology 3: Value Theory + +Calculate based on value created and willingness to pay. + +**Process:** +1. Identify problem being solved +2. Quantify current cost of problem (time, money, inefficiency) +3. Calculate value of solution (savings, gains, efficiency) +4. Estimate willingness to pay (typically 10-30% of value) +5. Multiply by addressable customer base + +**Formula:** +``` +Value per Customer = Problem Cost ร— % Solved by Solution +Price per Customer = Value ร— Willingness to Pay % (10-30%) +TAM = Total Potential Customers ร— Price per Customer +SAM = TAM ร— % Meeting Buy Criteria +SOM = SAM ร— Realistic Adoption Rate +``` + +**When to use:** New categories, disruptive innovations, unclear existing markets + +**Strengths:** Shows value creation, works for new markets + +**Limitations:** Requires assumptions, harder to validate + +## Step-by-Step Process + +### Step 1: Define the Market + +Clearly specify what market is being measured. + +**Questions to answer:** +- What problem is being solved? +- Who are the target customers? +- What's the product/service category? +- What's the geographic scope? +- What's the time horizon? + +**Example:** +- Problem: E-commerce companies struggle with email marketing automation +- Customers: E-commerce stores with >$1M annual revenue +- Category: AI-powered email marketing software +- Geography: North America initially, global expansion +- Horizon: 3-5 year opportunity + +### Step 2: Gather Data Sources + +Identify credible data for calculations. + +**Top-Down Sources:** +- Industry research reports (Gartner, Forrester, IDC) +- Government statistics (Census, BLS, trade associations) +- Public company filings and earnings +- Market research firms (Statista, CB Insights, PitchBook) + +**Bottom-Up Sources:** +- Customer interviews and surveys +- Sales data and CRM records +- Industry databases (LinkedIn, ZoomInfo, Crunchbase) +- Competitive intelligence +- Academic research + +**Value Theory Sources:** +- Customer problem quantification +- Time/cost studies +- ROI case studies +- Pricing research and willingness-to-pay surveys + +### Step 3: Calculate TAM + +Apply chosen methodology to determine total market. + +**For Top-Down:** +1. Find total category size from research +2. Document data source and year +3. Apply growth rate if needed +4. Validate with multiple sources + +**For Bottom-Up:** +1. Count total potential customers +2. Calculate average annual revenue per customer +3. Multiply to get TAM +4. Break down by segment + +**For Value Theory:** +1. Quantify total addressable customer base +2. Calculate value per customer +3. Estimate pricing based on value +4. Multiply for TAM + +### Step 4: Calculate SAM + +Narrow TAM to serviceable addressable market. + +**Apply Filters:** +- Geographic constraints (regions you can serve) +- Product limitations (features you currently have) +- Customer requirements (size, industry, use case) +- Distribution channel access +- Regulatory or compliance restrictions + +**Formula:** +``` +SAM = TAM ร— (% matching all filters) +``` + +**Example:** +- TAM: $10B global email marketing +- Geographic filter: 40% (North America) +- Product filter: 30% (e-commerce focus) +- Feature filter: 60% (need AI capabilities) +- SAM = $10B ร— 0.40 ร— 0.30 ร— 0.60 = $720M + +### Step 5: Calculate SOM + +Determine realistic obtainable market share. + +**Consider:** +- Current market share of competitors +- Typical market share for new entrants (2-5%) +- Resources available (funding, team, time) +- Go-to-market effectiveness +- Competitive advantages +- Time to achieve (3-5 years typically) + +**Conservative Approach:** +``` +SOM (Year 3) = SAM ร— 2% +SOM (Year 5) = SAM ร— 5% +``` + +**Example:** +- SAM: $720M +- Year 3 SOM: $720M ร— 2% = $14.4M +- Year 5 SOM: $720M ร— 5% = $36M + +### Step 6: Validate and Triangulate + +Cross-check using multiple methods. + +**Validation Techniques:** +1. Compare top-down and bottom-up results (should be within 30%) +2. Check against public company revenues in space +3. Validate customer count assumptions +4. Sense-check pricing assumptions +5. Review with industry experts +6. Compare to similar market categories + +**Red Flags:** +- TAM that's too small (< $1B for VC-backed startups) +- TAM that's too large (unsupported by data) +- SOM that's too aggressive (> 10% in 5 years for new entrant) +- Inconsistency between methodologies (> 50% difference) + +## Industry-Specific Considerations + +### SaaS Markets + +**Key Metrics:** +- Number of potential businesses in target segment +- Average contract value (ACV) +- Typical market penetration rates +- Expansion revenue potential + +**TAM Calculation:** +``` +TAM = Total Target Companies ร— Average ACV ร— (1 + Expansion Rate) +``` + +### Marketplace Markets + +**Key Metrics:** +- Gross Merchandise Value (GMV) of category +- Take rate (% of GMV you capture) +- Total transactions or users + +**TAM Calculation:** +``` +TAM = Total Category GMV ร— Expected Take Rate +``` + +### Consumer Markets + +**Key Metrics:** +- Total addressable users/households +- Average revenue per user (ARPU) +- Engagement frequency + +**TAM Calculation:** +``` +TAM = Total Users ร— ARPU ร— Purchase Frequency per Year +``` + +### B2B Services + +**Key Metrics:** +- Number of target companies by size/industry +- Average project value or retainer +- Typical buying frequency + +**TAM Calculation:** +``` +TAM = Total Target Companies ร— Average Deal Size ร— Deals per Year +``` + +## Presenting Market Sizing + +### For Investors + +**Structure:** +1. Market definition and problem scope +2. TAM/SAM/SOM with methodology +3. Data sources and assumptions +4. Growth projections and drivers +5. Competitive landscape context + +**Key Points:** +- Lead with bottom-up calculation (most credible) +- Show triangulation with top-down +- Explain conservative assumptions +- Link to revenue projections +- Highlight market growth rate + +### For Strategy + +**Structure:** +1. Addressable customer segments +2. Prioritization by opportunity size +3. Entry strategy by segment +4. Expected penetration timeline +5. Resource requirements + +**Key Points:** +- Focus on SAM and SOM +- Show segment-level detail +- Connect to go-to-market plan +- Identify expansion opportunities +- Discuss competitive positioning + +## Common Mistakes to Avoid + +**Mistake 1: Confusing TAM with SAM** +- Don't claim entire market as addressable +- Apply realistic product/geographic constraints +- Be honest about serviceable market + +**Mistake 2: Overly Aggressive SOM** +- New entrants rarely capture > 5% in 5 years +- Account for competition and resources +- Show realistic ramp timeline + +**Mistake 3: Using Only Top-Down** +- Investors prefer bottom-up validation +- Top-down alone lacks credibility +- Always triangulate with multiple methods + +**Mistake 4: Cherry-Picking Data** +- Use consistent, recent data sources +- Don't mix methodologies inappropriately +- Document all assumptions clearly + +**Mistake 5: Ignoring Market Dynamics** +- Account for market growth/decline +- Consider competitive intensity +- Factor in switching costs and barriers + +## Additional Resources + +### Reference Files + +For detailed methodologies and frameworks: +- **`references/methodology-deep-dive.md`** - Comprehensive guide to each methodology with step-by-step worksheets +- **`references/data-sources.md`** - Curated list of market research sources, databases, and tools +- **`references/industry-templates.md`** - Specific templates for SaaS, marketplace, consumer, B2B, and fintech markets + +### Example Files + +Working examples with complete calculations: +- **`examples/saas-market-sizing.md`** - Complete TAM/SAM/SOM for a B2B SaaS product +- **`examples/marketplace-sizing.md`** - Marketplace platform market opportunity calculation +- **`examples/value-theory-example.md`** - Value-based market sizing for disruptive innovation + +Use these examples as templates for your own market sizing analysis. Each includes real numbers, data sources, and assumptions documented clearly. + +## Quick Start + +To perform market sizing analysis: + +1. **Define the market** - Problem, customers, category, geography +2. **Choose methodology** - Bottom-up (preferred) or top-down + triangulation +3. **Gather data** - Industry reports, customer data, competitive intelligence +4. **Calculate TAM** - Apply methodology formula +5. **Narrow to SAM** - Apply product, geographic, segment filters +6. **Estimate SOM** - 2-5% realistic capture rate +7. **Validate** - Cross-check with alternative methods +8. **Document** - Show methodology, sources, assumptions +9. **Present** - Structure for audience (investors, strategy, operations) + +For detailed step-by-step guidance on each methodology, reference the files in `references/` directory. For complete worked examples, see `examples/` directory. diff --git a/plugins/antigravity-bundle-business-analyst/skills/market-sizing-analysis/examples/saas-market-sizing.md b/plugins/antigravity-bundle-business-analyst/skills/market-sizing-analysis/examples/saas-market-sizing.md new file mode 100644 index 00000000..931d3878 --- /dev/null +++ b/plugins/antigravity-bundle-business-analyst/skills/market-sizing-analysis/examples/saas-market-sizing.md @@ -0,0 +1,349 @@ +# SaaS Market Sizing Example: AI-Powered Email Marketing for E-Commerce + +Complete TAM/SAM/SOM calculation for a B2B SaaS startup using bottom-up and top-down methodologies. + +## Company Overview + +**Product:** AI-powered email marketing automation platform +**Target:** E-commerce companies with $1M+ annual revenue +**Geography:** North America (initial), global expansion planned +**Pricing:** $500/month average (scales by email volume) +**Timeline:** 3-5 year market opportunity + +## Methodology 1: Bottom-Up Analysis (Primary) + +### Step 1: Define Target Customer Segments + +**Segment Criteria:** +- E-commerce companies (D2C and marketplace sellers) +- $1M+ in annual revenue +- North America based +- Currently using email marketing + +**Segment Breakdown:** + +| Segment | Annual Revenue | Count | ACV | Priority | +|---------|---------------|-------|-----|----------| +| Small E-commerce | $1M-$5M | 85,000 | $3,600 | High | +| Mid-Market E-commerce | $5M-$50M | 18,000 | $9,600 | High | +| Enterprise E-commerce | $50M+ | 2,500 | $24,000 | Medium | + +**Data Sources:** +- U.S. Census Bureau: E-commerce business counts +- Shopify, BigCommerce, WooCommerce: Published merchant counts +- Statista: E-commerce market statistics +- LinkedIn Sales Navigator: Company search validation + +### Step 2: Calculate TAM (Total Addressable Market) + +**Formula:** +``` +TAM = ฮฃ (Segment Count ร— Annual Contract Value) +``` + +**Calculation:** +``` +Small E-commerce: 85,000 ร— $3,600 = $306M +Mid-Market: 18,000 ร— $9,600 = $173M +Enterprise: 2,500 ร— $24,000 = $60M + -------- +TAM (North America): $539M +``` + +**Global Expansion Multiplier:** +- North America = 35% of global e-commerce market +- Global TAM = $539M / 0.35 = $1.54B + +**TAM = $1.54B globally, $539M North America** + +### Step 3: Calculate SAM (Serviceable Available Market) + +**Filters Applied:** + +1. **Geographic Filter: North America Only (Year 1-2)** + - Base TAM: $539M + - Filter: 100% (starting in North America) + - Result: $539M + +2. **Product Capability Filter: AI-Ready Customers** + - Customers ready to adopt AI email marketing + - Excludes: Companies with basic email needs only + - Filter: 45% (based on survey data) + - Result: $539M ร— 0.45 = $242M + +3. **Current Tool Filter: Addressable Switching Market** + - Customers using incumbent tools who would switch + - Excludes: Recently switched, custom built solutions + - Filter: 70% (typical B2B SaaS switching market) + - Result: $242M ร— 0.70 = $169M + +**SAM = $169M** + +**SAM Breakdown by Segment:** +``` +Small E-commerce: $306M ร— 0.45 ร— 0.70 = $96M (57%) +Mid-Market: $173M ร— 0.45 ร— 0.70 = $54M (32%) +Enterprise: $60M ร— 0.45 ร— 0.70 = $19M (11%) +``` + +### Step 4: Calculate SOM (Serviceable Obtainable Market) + +**Market Share Assumptions:** + +**Year 3 Target: 2.5% of SAM** +- Typical new entrant market share +- Requires strong product-market fit +- Assumes $10M in funding for GTM + +**Year 5 Target: 5% of SAM** +- Achievable with scale and brand +- Requires effective sales and marketing +- Assumes additional funding for growth + +**Calculation:** +``` +SOM (Year 3) = $169M ร— 2.5% = $4.2M ARR +SOM (Year 5) = $169M ร— 5.0% = $8.5M ARR +``` + +**SOM by Segment (Year 5):** +``` +Small E-commerce: $96M ร— 5% = $4.8M ARR (565 customers) +Mid-Market: $54M ร— 5% = $2.7M ARR (281 customers) +Enterprise: $19M ร— 5% = $1.0M ARR (42 customers) + -------- +Total: $8.5M ARR (888 customers) +``` + +### Bottom-Up Summary + +| Metric | North America | Notes | +|--------|---------------|-------| +| **TAM** | $539M | All e-commerce $1M+ revenue | +| **SAM** | $169M | AI-ready, addressable switching market | +| **SOM (Year 3)** | $4.2M | 2.5% market share, 495 customers | +| **SOM (Year 5)** | $8.5M | 5% market share, 888 customers | + +## Methodology 2: Top-Down Analysis (Validation) + +### Step 1: Identify Total Market Category + +**Market Category:** Email Marketing Software +**Source:** Gartner Market Share Report (2024) + +**Global Email Marketing Software Market:** +- Market Size: $7.5B (2024) +- Growth Rate: 12% CAGR +- Geography: Worldwide + +**Data Source:** Gartner, "Market Share: Email Marketing Software, Worldwide, 2024" + +### Step 2: Apply Geographic Filter + +**North America Market Share:** +- North America = 40% of global software spending +- Email Marketing NA = $7.5B ร— 0.40 = $3.0B + +### Step 3: Apply Segment Filters + +**E-Commerce Focus:** +- E-commerce email marketing = 25% of total email marketing +- E-commerce segment = $3.0B ร— 0.25 = $750M + +**$1M+ Revenue Filter:** +- Companies with $1M+ revenue = 65% of e-commerce market +- TAM = $750M ร— 0.65 = $488M + +**AI-Powered Subset:** +- AI-powered email marketing = 35% of market (growing rapidly) +- SAM = $488M ร— 0.35 = $171M + +### Top-Down Summary + +| Metric | Amount | Calculation | +|--------|--------|-------------| +| **TAM** | $488M | NA e-commerce email marketing $1M+ | +| **SAM** | $171M | AI-powered subset | + +## Triangulation and Validation + +### Comparing Methodologies + +| Metric | Bottom-Up | Top-Down | Variance | +|--------|-----------|----------|----------| +| **TAM** | $539M | $488M | +10% | +| **SAM** | $169M | $171M | -1% | + +**Validation Result:** โœ… Excellent alignment (< 2% variance on SAM) + +**Why alignment matters:** +- Bottom-up and top-down within 10% gives high confidence +- SAM alignment of 1% is exceptional +- Use bottom-up as primary (more granular) +- Reference top-down for validation + +### Public Company Validation + +**Klaviyo (Public, KVYO):** +- 2024 Revenue: ~$700M +- Focus: E-commerce email/SMS marketing +- Market Share: ~46% of our SAM +- Validates large e-commerce email market exists + +**Mailchimp (Intuit-owned):** +- 2024 Revenue: ~$800M (estimated) +- Broader focus, includes SMBs +- Significant e-commerce customer base + +**Validation:** Market leaders have $700M-$800M revenue, supporting $1.5B+ global TAM + +### Sanity Checks + +**Customer Count Check:** +โœ… 888 customers at Year 5 (5% market share) = reasonable +โœ… Implies ~14,000 total addressable customers +โœ… Aligns with estimated 105,000 e-commerce cos $1M+ in NA + +**Average Revenue Check:** +โœ… $8.5M ARR / 888 customers = $9,571 ACV +โœ… Within expected range of $3.6K-$24K by segment +โœ… Weighted average makes sense given segment mix + +**Market Share Check:** +โœ… 5% market share in Year 5 is achievable for well-funded startup +โœ… Lower than Klaviyo (46%), appropriate for new entrant +โœ… Room for growth beyond Year 5 + +## Growth Projections + +### Market Growth Assumptions + +**Email Marketing Market CAGR: 12%** +- Source: Gartner market forecast +- Drivers: E-commerce growth, marketing automation adoption + +**AI Subset Growth: 25% CAGR** +- Higher than overall market +- AI adoption accelerating in marketing +- More companies seeking AI-powered tools + +### SAM Evolution (5-Year Forecast) + +| Year | SAM | Growth | Notes | +|------|-----|--------|-------| +| 2026 | $169M | - | Starting point | +| 2027 | $211M | +25% | AI adoption accelerating | +| 2028 | $264M | +25% | Mainstream adoption begins | +| 2029 | $330M | +25% | AI becomes table stakes | +| 2030 | $413M | +25% | Market maturity | + +**Growing SAM Impact:** +- Year 5 SOM of 5% applied to $413M SAM = $20.6M potential +- Provides headroom for growth +- Supports expansion beyond initial 5% share + +## Competitive Context + +### Market Share Distribution + +**Current Leaders:** +- Klaviyo: ~46% share +- Mailchimp: ~35% share +- Others: ~19% share (fragmented) + +**Market Dynamics:** +- Two dominant players +- Long tail of smaller competitors +- Opportunity in AI-differentiated positioning +- Typical SaaS market consolidation pattern + +**Implications for SOM:** +- 5% share requires strong differentiation +- AI capabilities could drive 10-15% share long-term +- Acquisition potential if unable to reach scale + +## Investment Thesis Validation + +### Market Opportunity Score: โœ… Strong + +**Positives:** +โœ… Large market: $1.5B+ global TAM +โœ… Growing market: 12% CAGR, 25% for AI subset +โœ… Addressable: $169M SAM with clear path to customers +โœ… Achievable: $8.5M Year 5 ARR reasonable +โœ… Validation: Public companies prove market exists + +**Risks:** +โš ๏ธ Competition: Klaviyo and Mailchimp are strong +โš ๏ธ Switching costs: Customers invested in current tools +โš ๏ธ Market share: 5% requires excellent execution + +**Verdict:** Market opportunity supports venture-scale outcome ($100M+ exit possible) + +## Presentation to Investors + +### Slide 1: Market Opportunity Summary + +``` +AI-Powered Email Marketing for E-Commerce + +TAM: $1.5B Global, $539M North America +SAM: $169M (AI-ready e-commerce companies) +SOM: $8.5M ARR by Year 5 (5% market share) + +Market Growing 25% CAGR (AI subset) +Validated by Klaviyo ($700M revenue) +``` + +### Slide 2: Bottom-Up Validation + +``` +Target: 105,000 E-Commerce Companies ($1M+ revenue) + +Segment Breakdown: +โ€ข Small ($1M-$5M): 85,000 companies ร— $3,600 ACV +โ€ข Mid-Market ($5M-$50M): 18,000 ร— $9,600 +โ€ข Enterprise ($50M+): 2,500 ร— $24,000 + +Year 5: 888 customers, $8.5M ARR (5% market share) +``` + +### Slide 3: Market Validation + +``` +Top-Down: $171M SAM (Gartner + market filters) +Bottom-Up: $169M SAM (<2% variance) + +Public Company Validation: +โ€ข Klaviyo: $700M revenue (46% market share) +โ€ข Mailchimp: $800M revenue (Intuit-owned) + +Demonstrates large, proven market +``` + +## Key Takeaways + +**Market Sizing Results:** +- TAM: $1.5B globally, $539M North America +- SAM: $169M (North America, AI-ready customers) +- SOM: $4.2M (Year 3), $8.5M (Year 5) + +**Methodology:** +- Bottom-up primary (most granular and credible) +- Top-down validation (<2% variance on SAM) +- Public company validation (Klaviyo, Mailchimp) + +**Investment Implications:** +- Market supports venture-scale outcome +- 5% market share achievable with strong execution +- Growing market (25% CAGR) provides tailwinds +- Competitive but differentiated positioning possible + +**Next Steps:** +1. Validate pricing assumptions with customer research +2. Refine segment prioritization based on GTM capacity +3. Update SAM annually as market evolves +4. Track Klaviyo/Mailchimp as competitive benchmarks +5. Monitor AI adoption rates in e-commerce segment + +This bottom-up market sizing provides a defensible, data-driven foundation for business planning and fundraising. diff --git a/plugins/antigravity-bundle-business-analyst/skills/market-sizing-analysis/references/data-sources.md b/plugins/antigravity-bundle-business-analyst/skills/market-sizing-analysis/references/data-sources.md new file mode 100644 index 00000000..c5f3c97a --- /dev/null +++ b/plugins/antigravity-bundle-business-analyst/skills/market-sizing-analysis/references/data-sources.md @@ -0,0 +1,360 @@ +# Market Sizing Data Sources + +Curated list of credible sources for market research and sizing analysis. + +## Industry Research Reports + +### Premium Research Firms + +**Gartner** (https://www.gartner.com) +- Technology market forecasts and sizing +- Magic Quadrants for competitive positioning +- Typical cost: $5K-$50K per report +- Best for: Enterprise software, IT services, emerging tech + +**Forrester** (https://www.forrester.com) +- Business technology and digital transformation +- Wave evaluations for vendor comparison +- Typical cost: $3K-$30K per report +- Best for: Marketing tech, customer experience, B2B + +**IDC** (https://www.idc.com) +- IT market intelligence and sizing +- Detailed segment breakdowns +- Typical cost: $4K-$40K per report +- Best for: Hardware, software, IT services + +**McKinsey** (https://www.mckinsey.com/featured-insights) +- Free insights and reports +- Strategic industry analysis +- Best for: Industry trends, macroeconomic context + +### Accessible Research + +**Statista** (https://www.statista.com) +- Cost: $39/month individual, $199/month business +- Coverage: 80,000+ topics across industries +- Best for: Quick market size estimates, charts, trends + +**CB Insights** (https://www.cbinsights.com) +- Cost: Custom pricing (typically $10K+/year) +- Coverage: Venture capital, startup markets +- Best for: Emerging markets, competitive intelligence + +**PitchBook** (https://pitchbook.com) +- Cost: Institutional pricing +- Coverage: Private company valuations, M&A, VC +- Best for: Startup valuations, funding trends + +**Grand View Research** (https://www.grandviewresearch.com) +- Cost: $2K-$5K per report +- Coverage: B2C and emerging markets +- Best for: Consumer markets, healthcare, cleantech + +## Government and Public Data + +### U.S. Government Sources + +**U.S. Census Bureau** (https://www.census.gov) +- Free, authoritative demographic data +- Economic census every 5 years +- Best for: Business counts, demographics, spending + +**Bureau of Labor Statistics** (https://www.bls.gov) +- Free employment and economic data +- Industry-specific statistics +- Best for: Employment trends, wages, productivity + +**SEC EDGAR** (https://www.sec.gov/edgar) +- Free public company filings +- 10-K, 10-Q reports with segment revenue +- Best for: Validating market size with public company data + +**Data.gov** (https://www.data.gov) +- Free government datasets +- Aggregates across agencies +- Best for: Specialized industry data + +### International Sources + +**OECD** (https://data.oecd.org) +- Free international economic data +- Best for: Cross-country comparisons + +**World Bank** (https://data.worldbank.org) +- Free global development data +- Best for: Emerging markets, macro trends + +**Eurostat** (https://ec.europa.eu/eurostat) +- Free European Union statistics +- Best for: European market sizing + +## Trade Associations + +Industry associations often publish market research: + +**Software & SaaS** +- Software & Information Industry Association (SIIA) +- Cloud Security Alliance (CSA) + +**E-commerce & Retail** +- National Retail Federation (NRF) +- Digital Commerce 360 + +**Financial Services** +- American Bankers Association (ABA) +- Financial Technology Association (FTA) + +**Healthcare** +- Healthcare Information and Management Systems Society (HIMSS) +- American Hospital Association (AHA) + +**Manufacturing** +- National Association of Manufacturers (NAM) +- Industrial Internet Consortium (IIC) + +## Company and Customer Data + +### B2B Databases + +**LinkedIn Sales Navigator** ($99/month) +- Company and employee counts +- Industry filters +- Best for: B2B customer counting + +**ZoomInfo** (Custom pricing) +- Company databases with firmographics +- Contact data +- Best for: B2B TAM calculations + +**Crunchbase** ($29-$99/month) +- Startup company data +- Funding and employee information +- Best for: Tech startup markets + +**BuiltWith** ($295-$995/month) +- Technology usage data +- Website analytics +- Best for: Technology adoption sizing + +### Consumer Data + +**Euromonitor** (Custom pricing) +- Consumer market research +- Best for: B2C product markets + +**Nielsen** (Custom pricing) +- Consumer behavior and media +- Best for: CPG, retail, media markets + +**Mintel** (Custom pricing) +- Consumer trends and insights +- Best for: B2C products and services + +## Search and Discovery Tools + +### Market Research Aggregators + +**Research and Markets** (https://www.researchandmarkets.com) +- Aggregates reports from 100+ publishers +- $500-$10K per report +- Search across all major research firms + +**MarketsandMarkets** (https://www.marketsandmarkets.com) +- Custom and syndicated research +- $4K-$10K per report +- Good for niche B2B markets + +### Free Search Tools + +**Google Scholar** (https://scholar.google.com) +- Free academic research +- Best for: Emerging technologies, academic validation + +**SSRN** (https://www.ssrn.com) +- Free working papers +- Best for: Financial services, economics + +**arXiv** (https://arxiv.org) +- Free preprints in CS, physics, etc. +- Best for: AI/ML, scientific markets + +## Competitive Intelligence + +### Public Company Analysis + +**Yahoo Finance** (Free) +- Public company financials +- Segment revenue from earnings + +**Seeking Alpha** (Free + Premium) +- Earnings transcripts +- Analyst estimates + +**Public company investor relations** +- Annual reports (10-K) +- Investor presentations + +### Private Company Intelligence + +**PrivCo** (Custom pricing) +- Private company financials +- M&A transaction data + +**Owler** (Free + Premium) +- Company profiles and news +- Revenue estimates + +**SimilarWeb** (Free + Premium) +- Website traffic analytics +- Best for: Online business sizing + +## Survey and Primary Research + +### Survey Tools + +**SurveyMonkey** ($25-$75/month) +- DIY surveys +- Best for: Customer willingness to pay + +**Typeform** ($25-$83/month) +- Conversational surveys +- Best for: User research + +**Qualtrics** (Enterprise pricing) +- Professional research platform +- Best for: Large-scale studies + +### Panel Providers + +**Respondent.io** ($100-$200 per response) +- Recruit professionals for interviews +- Best for: B2B customer research + +**UserTesting** ($49 per participant) +- User research and testing +- Best for: Product validation + +**Google Surveys** ($0.10-$3.50 per response) +- Quick consumer surveys +- Best for: Basic consumer insights + +## Data Quality Checklist + +When evaluating sources: + +**Authority** +- [ ] Who published the research? +- [ ] What's their reputation? +- [ ] Do they have industry expertise? + +**Methodology** +- [ ] How was data collected? +- [ ] What's the sample size? +- [ ] When was research conducted? + +**Recency** +- [ ] Is data current (< 2 years old)? +- [ ] Has market changed significantly? +- [ ] Are growth rates still applicable? + +**Consistency** +- [ ] Do multiple sources agree? +- [ ] Are definitions consistent? +- [ ] Do numbers triangulate? + +**Relevance** +- [ ] Does it match your market definition? +- [ ] Is geography appropriate? +- [ ] Are segments aligned? + +## Free vs. Paid Strategy + +**Start with free sources:** +1. Government data for customer counts +2. Public company filings for segment revenue +3. Trade associations for industry trends +4. Google Scholar for academic research + +**Upgrade to paid when:** +- Raising institutional funding (investors expect premium sources) +- Need detailed segment breakdowns +- Market is niche or emerging +- Free sources are outdated or insufficient + +**Cost-effective approach:** +- Buy 1-2 key reports that cover your core market +- Use free sources for triangulation +- Supplement with primary research (customer interviews) +- Cite mix of free and paid sources + +## Citation Best Practices + +Always cite sources in market sizing: + +**Format:** +``` +Market Size: $X.XB +Source: [Publisher], [Report Name], [Date] +URL: [link if available] +``` + +**Example:** +``` +Email Marketing Software TAM: $7.5B (2024) +Source: Gartner, "Market Share: Email Marketing Software, Worldwide, 2024" +Note: Includes all email marketing software revenue globally +``` + +**Include:** +- Publisher and report name +- Publication date +- Geography and scope +- Any adjustments made +- Link to source (if public) + +## Keeping Research Current + +**Set Google Alerts** +- Industry keywords +- Company names +- Market terms + +**Follow Research Firms** +- Twitter accounts +- LinkedIn updates +- Free newsletter summaries + +**Track Public Companies** +- Earnings calendars +- Investor relations pages +- Annual reports + +**Join Industry Groups** +- LinkedIn groups +- Slack communities +- Trade associations + +**Review Annually** +- Update market size with new data +- Adjust growth assumptions +- Revisit methodology if market changed + +## Emergency Research Guide + +**Need market size in < 2 hours?** + +1. **Check Statista** (15 min) - Quick industry overview +2. **Find public companies** (30 min) - Get segment revenue from 10-Ks +3. **LinkedIn search** (20 min) - Count potential B2B customers +4. **Google Scholar** (20 min) - Find academic papers +5. **Calculate bottom-up** (30 min) - Customers ร— Price +6. **Triangulate** (15 min) - Compare sources + +**Document everything:** +- Write down all sources +- Note all assumptions +- Show your methodology +- Caveat data quality + +Better to have a defensible estimate with clear limitations than no data at all. diff --git a/plugins/antigravity-bundle-business-analyst/skills/startup-financial-modeling/SKILL.md b/plugins/antigravity-bundle-business-analyst/skills/startup-financial-modeling/SKILL.md new file mode 100644 index 00000000..6176b67e --- /dev/null +++ b/plugins/antigravity-bundle-business-analyst/skills/startup-financial-modeling/SKILL.md @@ -0,0 +1,465 @@ +--- +name: startup-financial-modeling +description: "Build comprehensive 3-5 year financial models with revenue projections, cost structures, cash flow analysis, and scenario planning for early-stage startups." +risk: unknown +source: community +date_added: '2026-02-27' +--- + +# Startup Financial Modeling + +Build comprehensive 3-5 year financial models with revenue projections, cost structures, cash flow analysis, and scenario planning for early-stage startups. + +## Use this skill when + +- Working on startup financial modeling tasks or workflows +- Needing guidance, best practices, or checklists for startup financial modeling + +## Do not use this skill when + +- The task is unrelated to startup financial modeling +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Overview + +Financial modeling provides the quantitative foundation for startup strategy, fundraising, and operational planning. Create realistic projections using cohort-based revenue modeling, detailed cost structures, and scenario analysis to support decision-making and investor presentations. + +## Core Components + +### Revenue Model + +**Cohort-Based Projections:** +Build revenue from customer acquisition and retention by cohort. + +**Formula:** +``` +MRR = ฮฃ (Cohort Size ร— Retention Rate ร— ARPU) +ARR = MRR ร— 12 +``` + +**Key Inputs:** +- Monthly new customer acquisitions +- Customer retention rates by month +- Average revenue per user (ARPU) +- Pricing and packaging assumptions +- Expansion revenue (upsells, cross-sells) + +### Cost Structure + +**Operating Expenses Categories:** + +1. **Cost of Goods Sold (COGS)** + - Hosting and infrastructure + - Payment processing fees + - Customer support (variable portion) + - Third-party services per customer + +2. **Sales & Marketing (S&M)** + - Customer acquisition cost (CAC) + - Marketing programs and advertising + - Sales team compensation + - Marketing tools and software + +3. **Research & Development (R&D)** + - Engineering team compensation + - Product management + - Design and UX + - Development tools and infrastructure + +4. **General & Administrative (G&A)** + - Executive team + - Finance, legal, HR + - Office and facilities + - Insurance and compliance + +### Cash Flow Analysis + +**Components:** +- Beginning cash balance +- Cash inflows (revenue, fundraising) +- Cash outflows (operating expenses, CapEx) +- Ending cash balance +- Monthly burn rate +- Runway (months of cash remaining) + +**Formula:** +``` +Runway = Current Cash Balance / Monthly Burn Rate +Monthly Burn = Monthly Revenue - Monthly Expenses +``` + +### Headcount Planning + +**Role-Based Hiring Plan:** +Track headcount by department and role. + +**Key Metrics:** +- Fully-loaded cost per employee +- Revenue per employee +- Headcount by department (% of total) + +**Typical Ratios (Early-Stage SaaS):** +- Engineering: 40-50% +- Sales & Marketing: 25-35% +- G&A: 10-15% +- Customer Success: 5-10% + +## Financial Model Structure + +### Three-Scenario Framework + +**Conservative Scenario (P10):** +- Slower customer acquisition +- Lower pricing or conversion +- Higher churn rates +- Extended sales cycles +- Used for cash management + +**Base Scenario (P50):** +- Most likely outcomes +- Realistic assumptions +- Primary planning scenario +- Used for board reporting + +**Optimistic Scenario (P90):** +- Faster growth +- Better unit economics +- Lower churn +- Used for upside planning + +### Time Horizon + +**Detailed Projections: 3 Years** +- Monthly detail for Year 1 +- Monthly detail for Year 2 +- Quarterly detail for Year 3 + +**High-Level Projections: Years 4-5** +- Annual projections +- Key metrics only +- Support long-term planning + +## Step-by-Step Process + +### Step 1: Define Business Model + +Clarify revenue model and pricing. + +**SaaS Model:** +- Subscription pricing tiers +- Annual vs. monthly contracts +- Free trial or freemium approach +- Expansion revenue strategy + +**Marketplace Model:** +- GMV projections +- Take rate (% of transactions) +- Buyer and seller economics +- Transaction frequency + +**Transactional Model:** +- Transaction volume +- Revenue per transaction +- Frequency and seasonality + +### Step 2: Build Revenue Projections + +Use cohort-based methodology for accuracy. + +**Monthly Customer Acquisition:** +Define new customers acquired each month. + +**Retention Curve:** +Model customer retention over time. + +**Typical SaaS Retention:** +- Month 1: 100% +- Month 3: 90% +- Month 6: 85% +- Month 12: 75% +- Month 24: 70% + +**Revenue Calculation:** +For each cohort, calculate retained customers ร— ARPU for each month. + +### Step 3: Model Cost Structure + +Break down costs by category and behavior. + +**Fixed vs. Variable:** +- Fixed: Salaries, software, rent +- Variable: Hosting, payment processing, support + +**Scaling Assumptions:** +- COGS as % of revenue +- S&M as % of revenue (CAC payback) +- R&D growth rate +- G&A as % of total expenses + +### Step 4: Create Hiring Plan + +Model headcount growth by role and department. + +**Inputs:** +- Starting headcount +- Hiring velocity by role +- Fully-loaded compensation by role +- Benefits and taxes (typically 1.3-1.4x salary) + +**Example:** +``` +Engineer: $150K salary ร— 1.35 = $202K fully-loaded +Sales Rep: $100K OTE ร— 1.30 = $130K fully-loaded +``` + +### Step 5: Project Cash Flow + +Calculate monthly cash position and runway. + +**Monthly Cash Flow:** +``` +Beginning Cash ++ Revenue Collected (consider payment terms) +- Operating Expenses Paid +- CapEx += Ending Cash +``` + +**Runway Calculation:** +``` +If Ending Cash < 0: + Funding Need = Negative Cash Balance + Runway = 0 +Else: + Runway = Ending Cash / Average Monthly Burn +``` + +### Step 6: Calculate Key Metrics + +Track metrics that matter for stage. + +**Revenue Metrics:** +- MRR / ARR +- Growth rate (MoM, YoY) +- Revenue by segment or cohort + +**Unit Economics:** +- CAC (Customer Acquisition Cost) +- LTV (Lifetime Value) +- CAC Payback Period +- LTV / CAC Ratio + +**Efficiency Metrics:** +- Burn multiple (Net Burn / Net New ARR) +- Magic number (Net New ARR / S&M Spend) +- Rule of 40 (Growth % + Profit Margin %) + +**Cash Metrics:** +- Monthly burn rate +- Runway (months) +- Cash efficiency + +### Step 7: Scenario Analysis + +Create three scenarios with different assumptions. + +**Variable Assumptions:** +- Customer acquisition rate (ยฑ30%) +- Churn rate (ยฑ20%) +- Average contract value (ยฑ15%) +- CAC (ยฑ25%) + +**Fixed Assumptions:** +- Pricing structure +- Core operating expenses +- Hiring plan (adjust timing, not roles) + +## Business Model Templates + +### SaaS Financial Model + +**Revenue Drivers:** +- New MRR (customers ร— ARPU) +- Expansion MRR (upsells) +- Contraction MRR (downgrades) +- Churned MRR (lost customers) + +**Key Ratios:** +- Gross margin: 75-85% +- S&M as % revenue: 40-60% (early stage) +- CAC payback: < 12 months +- Net retention: 100-120% + +**Example Projection:** +``` +Year 1: $500K ARR, 50 customers, $100K MRR by Dec +Year 2: $2.5M ARR, 200 customers, $208K MRR by Dec +Year 3: $8M ARR, 600 customers, $667K MRR by Dec +``` + +### Marketplace Financial Model + +**Revenue Drivers:** +- GMV (Gross Merchandise Value) +- Take rate (% of GMV) +- Net revenue = GMV ร— Take rate + +**Key Ratios:** +- Take rate: 10-30% depending on category +- CAC for buyers vs. sellers +- Contribution margin: 60-70% + +**Example Projection:** +``` +Year 1: $5M GMV, 15% take rate = $750K revenue +Year 2: $20M GMV, 15% take rate = $3M revenue +Year 3: $60M GMV, 15% take rate = $9M revenue +``` + +### E-Commerce Financial Model + +**Revenue Drivers:** +- Traffic (visitors) +- Conversion rate +- Average order value (AOV) +- Purchase frequency + +**Key Ratios:** +- Gross margin: 40-60% +- Contribution margin: 20-35% +- CAC payback: 3-6 months + +### Services / Agency Financial Model + +**Revenue Drivers:** +- Billable hours or projects +- Hourly rate or project fee +- Utilization rate +- Team capacity + +**Key Ratios:** +- Gross margin: 50-70% +- Utilization: 70-85% +- Revenue per employee + +## Fundraising Integration + +### Funding Scenario Modeling + +**Pre-Money Valuation:** +Based on metrics and comparables. + +**Dilution:** +``` +Post-Money = Pre-Money + Investment +Dilution % = Investment / Post-Money +``` + +**Use of Funds:** +Allocate funding to extend runway and achieve milestones. + +**Example:** +``` +Raise: $5M at $20M pre-money +Post-Money: $25M +Dilution: 20% + +Use of Funds: +- Product Development: $2M (40%) +- Sales & Marketing: $2M (40%) +- G&A and Operations: $0.5M (10%) +- Working Capital: $0.5M (10%) +``` + +### Milestone-Based Planning + +**Identify Key Milestones:** +- Product launch +- First $1M ARR +- Break-even on CAC +- Series A fundraise + +**Funding Amount:** +Ensure runway to achieve next milestone + 6 months buffer. + +## Common Pitfalls + +**Pitfall 1: Overly Optimistic Revenue** +- New startups rarely hit aggressive projections +- Use conservative customer acquisition assumptions +- Model realistic churn rates + +**Pitfall 2: Underestimating Costs** +- Add 20% buffer to expense estimates +- Include fully-loaded compensation +- Account for software and tools + +**Pitfall 3: Ignoring Cash Flow Timing** +- Revenue โ‰  cash (payment terms) +- Expenses paid before revenue collected +- Model cash conversion carefully + +**Pitfall 4: Static Headcount** +- Hiring takes time (3-6 months to fill roles) +- Ramp time for productivity (3-6 months) +- Account for attrition (10-15% annually) + +**Pitfall 5: Not Scenario Planning** +- Single scenario is never accurate +- Always model conservative case +- Plan for what you'll do if base case fails + +## Model Validation + +**Sanity Checks:** +- [ ] Revenue growth rate is achievable (3x in Year 2, 2x in Year 3) +- [ ] Unit economics are realistic (LTV/CAC > 3, payback < 18 months) +- [ ] Burn multiple is reasonable (< 2.0 in Year 2-3) +- [ ] Headcount scales with revenue (revenue per employee growing) +- [ ] Gross margin is appropriate for business model +- [ ] S&M spending aligns with CAC and growth targets + +**Benchmark Against Peers:** +Compare key metrics to similar companies at similar stage. + +**Investor Feedback:** +Share model with advisors or investors for feedback on assumptions. + +## Additional Resources + +### Reference Files + +For detailed model structures and advanced techniques: +- **`references/model-templates.md`** - Complete financial model templates by business model +- **`references/unit-economics.md`** - Deep dive on CAC, LTV, payback, and efficiency metrics +- **`references/fundraising-scenarios.md`** - Modeling funding rounds and dilution + +### Example Files + +Working financial models with formulas: +- **`examples/saas-financial-model.md`** - Complete 3-year SaaS model with cohort analysis +- **`examples/marketplace-model.md`** - Marketplace GMV and take rate projections +- **`examples/scenario-analysis.md`** - Three-scenario framework with sensitivities + +## Quick Start + +To create a startup financial model: + +1. **Define business model** - Revenue drivers and pricing +2. **Project revenue** - Cohort-based with retention +3. **Model costs** - COGS, S&M, R&D, G&A by month +4. **Plan headcount** - Hiring by role and department +5. **Calculate cash flow** - Revenue - expenses = burn/runway +6. **Compute metrics** - CAC, LTV, burn multiple, runway +7. **Create scenarios** - Conservative, base, optimistic +8. **Validate assumptions** - Sanity check and benchmark +9. **Integrate fundraising** - Model funding rounds and milestones + +For complete templates and formulas, reference the `references/` and `examples/` files. diff --git a/plugins/antigravity-bundle-business-analyst/skills/startup-metrics-framework/SKILL.md b/plugins/antigravity-bundle-business-analyst/skills/startup-metrics-framework/SKILL.md new file mode 100644 index 00000000..339dc1fd --- /dev/null +++ b/plugins/antigravity-bundle-business-analyst/skills/startup-metrics-framework/SKILL.md @@ -0,0 +1,32 @@ +--- +name: startup-metrics-framework +description: "Comprehensive guide to tracking, calculating, and optimizing key performance metrics for different startup business models from seed through Series A." +risk: unknown +source: community +date_added: '2026-02-27' +--- + +# Startup Metrics Framework + +Comprehensive guide to tracking, calculating, and optimizing key performance metrics for different startup business models from seed through Series A. + +## Use this skill when + +- Working on startup metrics framework tasks or workflows +- Needing guidance, best practices, or checklists for startup metrics framework + +## Do not use this skill when + +- The task is unrelated to startup metrics framework +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Resources + +- `resources/implementation-playbook.md` for detailed patterns and examples. diff --git a/plugins/antigravity-bundle-business-analyst/skills/startup-metrics-framework/resources/implementation-playbook.md b/plugins/antigravity-bundle-business-analyst/skills/startup-metrics-framework/resources/implementation-playbook.md new file mode 100644 index 00000000..32d5dcac --- /dev/null +++ b/plugins/antigravity-bundle-business-analyst/skills/startup-metrics-framework/resources/implementation-playbook.md @@ -0,0 +1,500 @@ +# Startup Metrics Framework Implementation Playbook + +This file contains detailed patterns, checklists, and code samples referenced by the skill. + +# Startup Metrics Framework + +Comprehensive guide to tracking, calculating, and optimizing key performance metrics for different startup business models from seed through Series A. + +## Overview + +Track the right metrics at the right stage. Focus on unit economics, growth efficiency, and cash management metrics that matter for fundraising and operational excellence. + +## Universal Startup Metrics + +### Revenue Metrics + +**MRR (Monthly Recurring Revenue)** +``` +MRR = ฮฃ (Active Subscriptions ร— Monthly Price) +``` + +**ARR (Annual Recurring Revenue)** +``` +ARR = MRR ร— 12 +``` + +**Growth Rate** +``` +MoM Growth = (This Month MRR - Last Month MRR) / Last Month MRR +YoY Growth = (This Year ARR - Last Year ARR) / Last Year ARR +``` + +**Target Benchmarks:** +- Seed stage: 15-20% MoM growth +- Series A: 10-15% MoM growth, 3-5x YoY +- Series B+: 100%+ YoY (Rule of 40) + +### Unit Economics + +**CAC (Customer Acquisition Cost)** +``` +CAC = Total S&M Spend / New Customers Acquired +``` + +Include: Sales salaries, marketing spend, tools, overhead + +**LTV (Lifetime Value)** +``` +LTV = ARPU ร— Gross Margin% ร— (1 / Churn Rate) +``` + +Simplified: +``` +LTV = ARPU ร— Average Customer Lifetime ร— Gross Margin% +``` + +**LTV:CAC Ratio** +``` +LTV:CAC = LTV / CAC +``` + +**Benchmarks:** +- LTV:CAC > 3.0 = Healthy +- LTV:CAC 1.0-3.0 = Needs improvement +- LTV:CAC < 1.0 = Unsustainable + +**CAC Payback Period** +``` +CAC Payback = CAC / (ARPU ร— Gross Margin%) +``` + +**Benchmarks:** +- < 12 months = Excellent +- 12-18 months = Good +- > 24 months = Concerning + +### Cash Efficiency Metrics + +**Burn Rate** +``` +Monthly Burn = Monthly Revenue - Monthly Expenses +``` + +Negative burn = losing money (typical early-stage) + +**Runway** +``` +Runway (months) = Cash Balance / Monthly Burn Rate +``` + +**Target:** Always maintain 12-18 months runway + +**Burn Multiple** +``` +Burn Multiple = Net Burn / Net New ARR +``` + +**Benchmarks:** +- < 1.0 = Exceptional efficiency +- 1.0-1.5 = Good +- 1.5-2.0 = Acceptable +- > 2.0 = Inefficient + +Lower is better (spending less to generate ARR) + +## SaaS Metrics + +### Revenue Composition + +**New MRR** +New customers ร— ARPU + +**Expansion MRR** +Upsells and cross-sells from existing customers + +**Contraction MRR** +Downgrades from existing customers + +**Churned MRR** +Lost customers + +**Net New MRR Formula:** +``` +Net New MRR = New MRR + Expansion MRR - Contraction MRR - Churned MRR +``` + +### Retention Metrics + +**Logo Retention** +``` +Logo Retention = (Customers End - New Customers) / Customers Start +``` + +**Dollar Retention (NDR - Net Dollar Retention)** +``` +NDR = (ARR Start + Expansion - Contraction - Churn) / ARR Start +``` + +**Benchmarks:** +- NDR > 120% = Best-in-class +- NDR 100-120% = Good +- NDR < 100% = Needs work + +**Gross Retention** +``` +Gross Retention = (ARR Start - Churn - Contraction) / ARR Start +``` + +**Benchmarks:** +- > 90% = Excellent +- 85-90% = Good +- < 85% = Concerning + +### SaaS-Specific Metrics + +**Magic Number** +``` +Magic Number = Net New ARR (quarter) / S&M Spend (prior quarter) +``` + +**Benchmarks:** +- > 0.75 = Efficient, ready to scale +- 0.5-0.75 = Moderate efficiency +- < 0.5 = Inefficient, don't scale yet + +**Rule of 40** +``` +Rule of 40 = Revenue Growth Rate% + Profit Margin% +``` + +**Benchmarks:** +- > 40% = Excellent +- 20-40% = Acceptable +- < 20% = Needs improvement + +**Example:** +50% growth + (10%) margin = 40% โœ“ + +**Quick Ratio** +``` +Quick Ratio = (New MRR + Expansion MRR) / (Churned MRR + Contraction MRR) +``` + +**Benchmarks:** +- > 4.0 = Healthy growth +- 2.0-4.0 = Moderate +- < 2.0 = Churn problem + +## Marketplace Metrics + +### GMV (Gross Merchandise Value) + +**Total Transaction Volume:** +``` +GMV = ฮฃ (Transaction Value) +``` + +**Growth Rate:** +``` +GMV Growth Rate = (Current Period GMV - Prior Period GMV) / Prior Period GMV +``` + +**Target:** 20%+ MoM early-stage + +### Take Rate + +``` +Take Rate = Net Revenue / GMV +``` + +**Typical Ranges:** +- Payment processors: 2-3% +- E-commerce marketplaces: 10-20% +- Service marketplaces: 15-25% +- High-value B2B: 5-15% + +### Marketplace Liquidity + +**Time to Transaction** +How long from listing to sale/match? + +**Fill Rate** +% of requests that result in transaction + +**Repeat Rate** +% of users who transact multiple times + +**Benchmarks:** +- Fill rate > 80% = Strong liquidity +- Repeat rate > 60% = Strong retention + +### Marketplace Balance + +**Supply/Demand Ratio:** +Track relative growth of supply and demand sides. + +**Warning Signs:** +- Too much supply: Low fill rates, frustrated suppliers +- Too much demand: Long wait times, frustrated customers + +**Goal:** Balanced growth (1:1 ratio ideal, but varies by model) + +## Consumer/Mobile Metrics + +### Engagement Metrics + +**DAU (Daily Active Users)** +Unique users active each day + +**MAU (Monthly Active Users)** +Unique users active each month + +**DAU/MAU Ratio** +``` +DAU/MAU = DAU / MAU +``` + +**Benchmarks:** +- > 50% = Exceptional (daily habit) +- 20-50% = Good +- < 20% = Weak engagement + +**Session Frequency** +Average sessions per user per day/week + +**Session Duration** +Average time spent per session + +### Retention Curves + +**Day 1 Retention:** % users who return next day +**Day 7 Retention:** % users active 7 days after signup +**Day 30 Retention:** % users active 30 days after signup + +**Benchmarks (Day 30):** +- > 40% = Excellent +- 25-40% = Good +- < 25% = Weak + +**Retention Curve Shape:** +- Flattening curve = good (users becoming habitual) +- Steep decline = poor product-market fit + +### Viral Coefficient (K-Factor) + +``` +K-Factor = Invites per User ร— Invite Conversion Rate +``` + +**Example:** +10 invites/user ร— 20% conversion = 2.0 K-factor + +**Benchmarks:** +- K > 1.0 = Viral growth +- K = 0.5-1.0 = Strong referrals +- K < 0.5 = Weak virality + +## B2B Metrics + +### Sales Efficiency + +**Win Rate** +``` +Win Rate = Deals Won / Total Opportunities +``` + +**Target:** 20-30% for new sales team, 30-40% mature + +**Sales Cycle Length** +Average days from opportunity to close + +**Shorter is better:** +- SMB: 30-60 days +- Mid-market: 60-120 days +- Enterprise: 120-270 days + +**Average Contract Value (ACV)** +``` +ACV = Total Contract Value / Contract Length (years) +``` + +### Pipeline Metrics + +**Pipeline Coverage** +``` +Pipeline Coverage = Total Pipeline Value / Quota +``` + +**Target:** 3-5x coverage (3-5x pipeline needed to hit quota) + +**Conversion Rates by Stage:** +- Lead โ†’ Opportunity: 10-20% +- Opportunity โ†’ Demo: 50-70% +- Demo โ†’ Proposal: 30-50% +- Proposal โ†’ Close: 20-40% + +## Metrics by Stage + +### Pre-Seed (Product-Market Fit) + +**Focus Metrics:** +1. Active users growth +2. User retention (Day 7, Day 30) +3. Core engagement (sessions, features used) +4. Qualitative feedback (NPS, interviews) + +**Don't worry about:** +- Revenue (may be zero) +- CAC (not optimizing yet) +- Unit economics + +### Seed ($500K-$2M ARR) + +**Focus Metrics:** +1. MRR growth rate (15-20% MoM) +2. CAC and LTV (establish baseline) +3. Gross retention (> 85%) +4. Core product engagement + +**Start tracking:** +- Sales efficiency +- Burn rate and runway + +### Series A ($2M-$10M ARR) + +**Focus Metrics:** +1. ARR growth (3-5x YoY) +2. Unit economics (LTV:CAC > 3, payback < 18 months) +3. Net dollar retention (> 100%) +4. Burn multiple (< 2.0) +5. Magic number (> 0.5) + +**Mature tracking:** +- Rule of 40 +- Sales efficiency +- Pipeline coverage + +## Metric Tracking Best Practices + +### Data Infrastructure + +**Requirements:** +- Single source of truth (analytics platform) +- Real-time or daily updates +- Automated calculations +- Historical tracking + +**Tools:** +- Mixpanel, Amplitude (product analytics) +- ChartMogul, Baremetrics (SaaS metrics) +- Looker, Tableau (BI dashboards) + +### Reporting Cadence + +**Daily:** +- MRR, active users +- Sign-ups, conversions + +**Weekly:** +- Growth rates +- Retention cohorts +- Sales pipeline + +**Monthly:** +- Full metric suite +- Board reporting +- Investor updates + +**Quarterly:** +- Trend analysis +- Benchmarking +- Strategy review + +### Common Mistakes + +**Mistake 1: Vanity Metrics** +Don't focus on: +- Total users (without retention) +- Page views (without engagement) +- Downloads (without activation) + +Focus on actionable metrics tied to value. + +**Mistake 2: Too Many Metrics** +Track 5-7 core metrics intensely, not 50 loosely. + +**Mistake 3: Ignoring Unit Economics** +CAC and LTV are critical even at seed stage. + +**Mistake 4: Not Segmenting** +Break down metrics by customer segment, channel, cohort. + +**Mistake 5: Gaming Metrics** +Optimize for real business outcomes, not dashboard numbers. + +## Investor Metrics + +### What VCs Want to See + +**Seed Round:** +- MRR growth rate +- User retention +- Early unit economics +- Product engagement + +**Series A:** +- ARR and growth rate +- CAC payback < 18 months +- LTV:CAC > 3.0 +- Net dollar retention > 100% +- Burn multiple < 2.0 + +**Series B+:** +- Rule of 40 > 40% +- Efficient growth (magic number) +- Path to profitability +- Market leadership metrics + +### Metric Presentation + +**Dashboard Format:** +``` +Current MRR: $250K (โ†‘ 18% MoM) +ARR: $3.0M (โ†‘ 280% YoY) +CAC: $1,200 | LTV: $4,800 | LTV:CAC = 4.0x +NDR: 112% | Logo Retention: 92% +Burn: $180K/mo | Runway: 18 months +``` + +**Include:** +- Current value +- Growth rate or trend +- Context (target, benchmark) + +## Additional Resources + +### Reference Files +- **`references/metric-definitions.md`** - Complete definitions and formulas for 50+ metrics +- **`references/benchmarks-by-stage.md`** - Target ranges for each metric by company stage +- **`references/calculation-examples.md`** - Step-by-step calculation examples + +### Example Files +- **`examples/saas-metrics-dashboard.md`** - Complete metrics suite for B2B SaaS company +- **`examples/marketplace-metrics.md`** - Marketplace-specific metrics with examples +- **`examples/investor-metrics-deck.md`** - How to present metrics for fundraising + +## Quick Start + +To implement startup metrics framework: + +1. **Identify business model** - SaaS, marketplace, consumer, B2B +2. **Choose 5-7 core metrics** - Based on stage and model +3. **Establish tracking** - Set up analytics and dashboards +4. **Calculate unit economics** - CAC, LTV, payback +5. **Set targets** - Use benchmarks for goals +6. **Review regularly** - Weekly for core metrics +7. **Share with team** - Align on goals and progress +8. **Update investors** - Monthly/quarterly reporting + +For detailed definitions, benchmarks, and examples, see `references/` and `examples/`. diff --git a/plugins/antigravity-bundle-commerce-payments/.codex-plugin/plugin.json b/plugins/antigravity-bundle-commerce-payments/.codex-plugin/plugin.json new file mode 100644 index 00000000..bd0b9db7 --- /dev/null +++ b/plugins/antigravity-bundle-commerce-payments/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-commerce-payments", + "version": "8.10.0", + "description": "Install the \"Commerce & Payments\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "commerce-payments", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Commerce & Payments", + "shortDescription": "Specialized Packs ยท 6 curated skills", + "longDescription": "For monetization, payments, and commerce workflows. Covers Stripe Integration, Paypal Integration, and 4 more skills.", + "developerName": "sickn33 and contributors", + "category": "Specialized Packs", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-commerce-payments/skills/algolia-search/SKILL.md b/plugins/antigravity-bundle-commerce-payments/skills/algolia-search/SKILL.md new file mode 100644 index 00000000..15284c07 --- /dev/null +++ b/plugins/antigravity-bundle-commerce-payments/skills/algolia-search/SKILL.md @@ -0,0 +1,68 @@ +--- +name: algolia-search +description: "Expert patterns for Algolia search implementation, indexing strategies, React InstantSearch, and relevance tuning Use when: adding search to, algolia, instantsearch, search api, search functionality." +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# Algolia Search Integration + +## Patterns + +### React InstantSearch with Hooks + +Modern React InstantSearch setup using hooks for type-ahead search. + +Uses react-instantsearch-hooks-web package with algoliasearch client. +Widgets are components that can be customized with classnames. + +Key hooks: +- useSearchBox: Search input handling +- useHits: Access search results +- useRefinementList: Facet filtering +- usePagination: Result pagination +- useInstantSearch: Full state access + +### Next.js Server-Side Rendering + +SSR integration for Next.js with react-instantsearch-nextjs package. + +Use instead of for SSR. +Supports both Pages Router and App Router (experimental). + +Key considerations: +- Set dynamic = 'force-dynamic' for fresh results +- Handle URL synchronization with routing prop +- Use getServerState for initial state + +### Data Synchronization and Indexing + +Indexing strategies for keeping Algolia in sync with your data. + +Three main approaches: +1. Full Reindexing - Replace entire index (expensive) +2. Full Record Updates - Replace individual records +3. Partial Updates - Update specific attributes only + +Best practices: +- Batch records (ideal: 10MB, 1K-10K records per batch) +- Use incremental updates when possible +- partialUpdateObjects for attribute-only changes +- Avoid deleteBy (computationally expensive) + +## โš ๏ธ Sharp Edges + +| Issue | Severity | Solution | +|-------|----------|----------| +| Issue | critical | See docs | +| Issue | high | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-commerce-payments/skills/hubspot-integration/SKILL.md b/plugins/antigravity-bundle-commerce-payments/skills/hubspot-integration/SKILL.md new file mode 100644 index 00000000..a622711a --- /dev/null +++ b/plugins/antigravity-bundle-commerce-payments/skills/hubspot-integration/SKILL.md @@ -0,0 +1,47 @@ +--- +name: hubspot-integration +description: "Authentication for single-account integrations" +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# HubSpot Integration + +## Patterns + +### OAuth 2.0 Authentication + +Secure authentication for public apps + +### Private App Token + +Authentication for single-account integrations + +### CRM Object CRUD Operations + +Create, read, update, delete CRM records + +## Anti-Patterns + +### โŒ Using Deprecated API Keys + +### โŒ Individual Requests Instead of Batch + +### โŒ Polling Instead of Webhooks + +## โš ๏ธ Sharp Edges + +| Issue | Severity | Solution | +|-------|----------|----------| +| Issue | high | See docs | +| Issue | high | See docs | +| Issue | critical | See docs | +| Issue | high | See docs | +| Issue | critical | See docs | +| Issue | medium | See docs | +| Issue | high | See docs | +| Issue | medium | See docs | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-commerce-payments/skills/monetization/SKILL.md b/plugins/antigravity-bundle-commerce-payments/skills/monetization/SKILL.md new file mode 100644 index 00000000..cf025e07 --- /dev/null +++ b/plugins/antigravity-bundle-commerce-payments/skills/monetization/SKILL.md @@ -0,0 +1,408 @@ +--- +name: monetization +description: "Estrategia e implementacao de monetizacao para produtos digitais - Stripe, subscriptions, pricing experiments, freemium, upgrade flows, churn prevention, revenue optimization e modelos de negocio SaaS." +risk: none +source: community +date_added: '2026-03-06' +author: renat +tags: +- monetization +- stripe +- saas +- pricing +- subscriptions +tools: +- claude-code +- antigravity +- cursor +- gemini-cli +- codex-cli +--- + +# MONETIZATION - Do Produto ao Revenue + +## Overview + +Estrategia e implementacao de monetizacao para produtos digitais - Stripe, subscriptions, pricing experiments, freemium, upgrade flows, churn prevention, revenue optimization e modelos de negocio SaaS. Ativar para: integrar Stripe, criar planos de assinatura, pricing strategy, upgrade/downgrade, webhook de pagamento, trial gratuito, churn, LTV/CAC, unit economics, modelo de negocio. + +## When to Use This Skill + +- When you need specialized assistance with this domain + +## Do Not Use This Skill When + +- The task is unrelated to monetization +- A simpler, more specific tool can handle the request +- The user needs general-purpose assistance without domain expertise + +## How It Works + +> Price is what you pay. Value is what you get. - Warren Buffett +> A monetizacao perfeita captura valor proporcional ao valor entregue. + +--- + +## A Regra De Ouro + +Usuarios pagam quando: +1. O produto resolve um problema real (need) +2. A solucao e melhor que alternativas (differentiation) +3. O preco e percebido como justo (value perception) +4. O momento de cobranca e natural (timing) + +## Erros Classicos + +- Cobranca antes de mostrar valor (kill activation) +- Preco muito baixo (sinaliza baixa qualidade) +- Planos demais (paralisia de escolha) +- Trial sem carta de credito (baixa conversao) +- Churn invisivel (sem alertas de cancelamento iminente) + +--- + +## Setup Inicial + +```bash +pip install stripe + +## Ou + +npm install stripe +``` + +```python + +## Config.Py + +import stripe +import os + +stripe.api_key = os.environ["STRIPE_SECRET_KEY"] +STRIPE_WEBHOOK_SECRET = os.environ["STRIPE_WEBHOOK_SECRET"] + +PLANS = { + "free": None, + "pro": os.environ["STRIPE_PRICE_PRO"], + "business": os.environ["STRIPE_PRICE_BIZ"], +} +``` + +## Criar Customer E Subscription + +```python +def create_customer(email: str, name: str, user_id: str) -> str: + customer = stripe.Customer.create( + email=email, + name=name, + metadata={"user_id": user_id} + ) + return customer.id + +def create_subscription(customer_id: str, price_id: str, trial_days: int = 14): + subscription = stripe.Subscription.create( + customer=customer_id, + items=[{"price": price_id}], + trial_period_days=trial_days, + payment_behavior="default_incomplete", + expand=["latest_invoice.payment_intent"], + ) + return { + "subscription_id": subscription.id, + "client_secret": subscription.latest_invoice.payment_intent.client_secret, + "status": subscription.status + } +``` + +## Checkout Session (Recomendado Para Conversao) + +```python +def create_checkout_session( + customer_id: str, + price_id: str, + success_url: str, + cancel_url: str, + trial_days: int = 14 +) -> str: + session = stripe.checkout.Session.create( + customer=customer_id, + mode="subscription", + line_items=[{"price": price_id, "quantity": 1}], + subscription_data={"trial_period_days": trial_days}, + success_url=success_url + "?session_id={CHECKOUT_SESSION_ID}", + cancel_url=cancel_url, + allow_promotion_codes=True, + ) + return session.url +``` + +## Customer Portal (Self-Service) + +```python +def create_portal_session(customer_id: str, return_url: str) -> str: + session = stripe.billing_portal.Session.create( + customer=customer_id, + return_url=return_url, + ) + return session.url +``` + +## Webhook - Processar Eventos + +```python +from fastapi import Request, HTTPException +import stripe + +async def stripe_webhook(request: Request): + payload = await request.body() + sig_header = request.headers.get("stripe-signature") + + try: + event = stripe.Webhook.construct_event( + payload, sig_header, STRIPE_WEBHOOK_SECRET + ) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid payload") + except stripe.error.SignatureVerificationError: + raise HTTPException(status_code=400, detail="Invalid signature") + + handlers = { + "customer.subscription.created": handle_subscription_created, + "customer.subscription.updated": handle_subscription_updated, + "customer.subscription.deleted": handle_subscription_deleted, + "invoice.payment_succeeded": handle_payment_succeeded, + "invoice.payment_failed": handle_payment_failed, + "customer.subscription.trial_will_end": handle_trial_ending, + } + + handler = handlers.get(event["type"]) + if handler: + await handler(event["data"]["object"]) + + return {"status": "ok"} +``` + +## Verificar Status Da Subscription + +```python +def get_subscription_status(customer_id: str) -> dict: + subscriptions = stripe.Subscription.list( + customer=customer_id, + status="all", + limit=1 + ) + if not subscriptions.data: + return {"tier": "free", "status": "none"} + + sub = subscriptions.data[0] + return { + "tier": get_tier_from_price(sub.items.data[0].price.id), + "status": sub.status, + "trial_end": sub.trial_end, + "current_period_end": sub.current_period_end, + "cancel_at_period_end": sub.cancel_at_period_end, + } +``` + +--- + +## Framework De Pricing Para Saas + +**Metodo 1: Value-Based Pricing (Recomendado)** +``` +1. Calcule o valor economico entregue ao usuario + Ex: produto economiza 2h/semana = R$ 200/mes de valor +2. Capture 10-30% do valor criado + Ex: R$ 29/mes = 14% do valor +3. Valide com pesquisa de willingness-to-pay +4. Teste 3 price points (A/B test) +``` + +**Metodo 2: Competitive Anchor** +``` +Referencia: ChatGPT Plus = $20/mes (R$ 100) +Anchor: Notion = R$ 32/mes +Posicao: Pro = R$ 29/mes (mais barato que ChatGPT, similar ao Notion) +Mensagem: Tudo que o ChatGPT faz, por voz no Alexa +``` + +## Psicologia De Pricing + +``` +R$ 29/mes (nao R$ 30 - efeito do digito esquerdo) +Plano anual com desconto claro: R$ 249/ano (economize R$ 99) +Destaque no plano que voce quer vender (visual hierarchy) +Ancoragem: mostra o plano caro primeiro +Trial sem cartao para ativacao, com cartao para retencao +Badge Mais popular no plano middle +``` + +## Estrutura De Planos (3 E O Numero Certo) + +| Feature | Free | Pro | Business | +|---------------------|---------|------------|------------| +| Preco | Gratis | R$ 29/mes | R$ 99/mes | +| Conversas/mes | 50 | Ilimitado | Ilimitado | +| Memoria | 7 dias | 1 ano | Permanente | +| Board especialistas | Nao | Sim | Sim | +| Multi-usuarios | Nao | Nao | Ate 10 | +| API access | Nao | Nao | Sim | +| Suporte | Nao | Email | Priority | + +--- + +## Sinais De Churn Iminente + +```python +CHURN_SIGNALS = { + "high_risk": [ + "nao logou nos ultimos 14 dias", + "uso caiu >70% em 2 semanas", + "abriu cancelamento mas nao concluiu", + "ticket de suporte aberto sem resolucao", + ], + "medium_risk": [ + "nao logou em 7 dias", + "uso caiu >40%", + "nao completou onboarding", + "nunca usou feature core", + ] +} +``` + +## Sequencia Anti-Churn + +``` +Dia 0: Usuario nao usa por 7 dias + -> Email: Sentimos sua falta. O que aconteceu? + +Dia 3: Sem resposta + -> Push/Email: case study de usuario similar com sucesso + +Dia 7: Nao voltou + -> Email: oferta especial (20% off por 3 meses) + +Dia 14: Trial expirando + -> In-app modal + email urgente: Sua conta vai dormir em 3 dias + +Dia 30: Cancelou + -> Offboarding email: Lamentamos ver voce ir. + -> 3 meses depois: reativacao com novidades +``` + +## Exit Survey (Obrigatorio) + +```python +CANCELLATION_REASONS = [ + "Muito caro", + "Nao uso o suficiente", + "Falta funcionalidade X", + "Encontrei alternativa melhor", + "Problemas tecnicos", + "Outro" +] + +## Falta Feature -> Roadmap + Notificacao Quando Lancar + +``` + +--- + +## Calculos Essenciais + +```python +def calculate_unit_economics( + mrr: float, + customers: int, + new_customers: int, + churned: int, + cac_total: float, +): + arpu = mrr / customers + churn_rate = churned / customers + ltv = arpu / churn_rate + cac = cac_total / new_customers + ltv_cac = ltv / cac + months_to_recover_cac = cac / arpu + + return { + "ARPU": f"R$ {arpu:.2f}", + "Churn Rate": f"{churn_rate*100:.1f}%", + "LTV": f"R$ {ltv:.0f}", + "CAC": f"R$ {cac:.0f}", + "LTV/CAC": f"{ltv_cac:.1f}x", + "Payback": f"{months_to_recover_cac:.1f} meses", + "Status": "Saudavel" if ltv_cac > 3 else "Otimizar" + } +``` + +## Benchmarks Saas B2C Brasil + +| Metrica | Ruim | Ok | Bom | Excelente | +|-----------------------|-------|--------|--------|-----------| +| Churn Mensal | >7% | 5-7% | 2-5% | <2% | +| LTV/CAC | <1x | 1-3x | 3-5x | >5x | +| Payback | >18m | 12-18m | 6-12m | <6m | +| Conversao trial->pago | <3% | 3-8% | 8-15% | >15% | +| MoM Growth | <5% | 5-10% | 10-20% | >20% | + +--- + +## Dashboard De Revenue (Metricas Diarias) + +``` +MRR atual: R$ XX.XXX + New MRR (novos assinantes): +R$ X.XXX + Expansion MRR (upgrades): +R$ XXX + Contraction MRR (downgrades): -R$ XXX + Churned MRR (cancelamentos): -R$ XXX + Net New MRR: +/- R$ XXX + +ARR (Annualized): R$ XX.XXX x 12 +Churn Rate: X.X% +Net Revenue Retention: XXX% (meta: >100%) +``` + +## Automacao De Revenue Com Stripe + +```python +async def check_usage_and_upsell(user_id: str, usage: dict): + if usage["conversations_this_month"] >= 45: + await send_upgrade_prompt( + user_id=user_id, + message="Voce esta usando 90% do seu limite. Faca upgrade para Pro.", + cta_url=f"/upgrade?utm=usage-limit" + ) +``` + +--- + +## 7. Comandos Rapidos + +| Comando | Acao | +|----------------------|------------------------------------------| +| /stripe-setup | Configura Stripe do zero | +| /pricing-analysis | Analisa estrategia de pricing atual | +| /churn-playbook | Sequencia anti-churn personalizada | +| /unit-economics | Calcula LTV/CAC e saude financeira | +| /upgrade-flow | Design do fluxo de upgrade | +| /revenue-dashboard | Template de dashboard de revenue | +| /trial-optimization | Otimiza conversao de trial | + +## Best Practices + +- Provide clear, specific context about your project and requirements +- Review all suggestions before applying them to production code +- Combine with other complementary skills for comprehensive analysis + +## Common Pitfalls + +- Using this skill for tasks outside its domain expertise +- Applying recommendations without understanding your specific context +- Not providing enough project context for accurate analysis + +## Related Skills + +- `analytics-product` - Complementary skill for enhanced analysis +- `growth-engine` - Complementary skill for enhanced analysis +- `product-design` - Complementary skill for enhanced analysis +- `product-inventor` - Complementary skill for enhanced analysis diff --git a/plugins/antigravity-bundle-commerce-payments/skills/paypal-integration/SKILL.md b/plugins/antigravity-bundle-commerce-payments/skills/paypal-integration/SKILL.md new file mode 100644 index 00000000..539c7abb --- /dev/null +++ b/plugins/antigravity-bundle-commerce-payments/skills/paypal-integration/SKILL.md @@ -0,0 +1,482 @@ +--- +name: paypal-integration +description: "Master PayPal payment integration including Express Checkout, IPN handling, recurring billing, and refund workflows." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# PayPal Integration + +Master PayPal payment integration including Express Checkout, IPN handling, recurring billing, and refund workflows. + +## Do not use this skill when + +- The task is unrelated to paypal integration +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Use this skill when + +- Integrating PayPal as a payment option +- Implementing express checkout flows +- Setting up recurring billing with PayPal +- Processing refunds and payment disputes +- Handling PayPal webhooks (IPN) +- Supporting international payments +- Implementing PayPal subscriptions + +## Core Concepts + +### 1. Payment Products +**PayPal Checkout** +- One-time payments +- Express checkout experience +- Guest and PayPal account payments + +**PayPal Subscriptions** +- Recurring billing +- Subscription plans +- Automatic renewals + +**PayPal Payouts** +- Send money to multiple recipients +- Marketplace and platform payments + +### 2. Integration Methods +**Client-Side (JavaScript SDK)** +- Smart Payment Buttons +- Hosted payment flow +- Minimal backend code + +**Server-Side (REST API)** +- Full control over payment flow +- Custom checkout UI +- Advanced features + +### 3. IPN (Instant Payment Notification) +- Webhook-like payment notifications +- Asynchronous payment updates +- Verification required + +## Quick Start + +```javascript +// Frontend - PayPal Smart Buttons +
+ + + +``` + +```python +# Backend - Verify and capture order +from paypalrestsdk import Payment +import paypalrestsdk + +paypalrestsdk.configure({ + "mode": "sandbox", # or "live" + "client_id": "YOUR_CLIENT_ID", + "client_secret": "YOUR_CLIENT_SECRET" +}) + +def capture_paypal_order(order_id): + """Capture a PayPal order.""" + payment = Payment.find(order_id) + + if payment.execute({"payer_id": payment.payer.payer_info.payer_id}): + # Payment successful + return { + 'status': 'success', + 'transaction_id': payment.id, + 'amount': payment.transactions[0].amount.total + } + else: + # Payment failed + return { + 'status': 'failed', + 'error': payment.error + } +``` + +## Express Checkout Implementation + +### Server-Side Order Creation +```python +import requests +import json + +class PayPalClient: + def __init__(self, client_id, client_secret, mode='sandbox'): + self.client_id = client_id + self.client_secret = client_secret + self.base_url = 'https://api-m.sandbox.paypal.com' if mode == 'sandbox' else 'https://api-m.paypal.com' + self.access_token = self.get_access_token() + + def get_access_token(self): + """Get OAuth access token.""" + url = f"{self.base_url}/v1/oauth2/token" + headers = {"Accept": "application/json", "Accept-Language": "en_US"} + + response = requests.post( + url, + headers=headers, + data={"grant_type": "client_credentials"}, + auth=(self.client_id, self.client_secret) + ) + + return response.json()['access_token'] + + def create_order(self, amount, currency='USD'): + """Create a PayPal order.""" + url = f"{self.base_url}/v2/checkout/orders" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}" + } + + payload = { + "intent": "CAPTURE", + "purchase_units": [{ + "amount": { + "currency_code": currency, + "value": str(amount) + } + }] + } + + response = requests.post(url, headers=headers, json=payload) + return response.json() + + def capture_order(self, order_id): + """Capture payment for an order.""" + url = f"{self.base_url}/v2/checkout/orders/{order_id}/capture" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.access_token}" + } + + response = requests.post(url, headers=headers) + return response.json() + + def get_order_details(self, order_id): + """Get order details.""" + url = f"{self.base_url}/v2/checkout/orders/{order_id}" + headers = { + "Authorization": f"Bearer {self.access_token}" + } + + response = requests.get(url, headers=headers) + return response.json() +``` + +## IPN (Instant Payment Notification) Handling + +### IPN Verification and Processing +```python +from flask import Flask, request +import requests +from urllib.parse import parse_qs + +app = Flask(__name__) + +@app.route('/ipn', methods=['POST']) +def handle_ipn(): + """Handle PayPal IPN notifications.""" + # Get IPN message + ipn_data = request.form.to_dict() + + # Verify IPN with PayPal + if not verify_ipn(ipn_data): + return 'IPN verification failed', 400 + + # Process IPN based on transaction type + payment_status = ipn_data.get('payment_status') + txn_type = ipn_data.get('txn_type') + + if payment_status == 'Completed': + handle_payment_completed(ipn_data) + elif payment_status == 'Refunded': + handle_refund(ipn_data) + elif payment_status == 'Reversed': + handle_chargeback(ipn_data) + + return 'IPN processed', 200 + +def verify_ipn(ipn_data): + """Verify IPN message authenticity.""" + # Add 'cmd' parameter + verify_data = ipn_data.copy() + verify_data['cmd'] = '_notify-validate' + + # Send back to PayPal for verification + paypal_url = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr' # or production URL + + response = requests.post(paypal_url, data=verify_data) + + return response.text == 'VERIFIED' + +def handle_payment_completed(ipn_data): + """Process completed payment.""" + txn_id = ipn_data.get('txn_id') + payer_email = ipn_data.get('payer_email') + mc_gross = ipn_data.get('mc_gross') + item_name = ipn_data.get('item_name') + + # Check if already processed (prevent duplicates) + if is_transaction_processed(txn_id): + return + + # Update database + # Send confirmation email + # Fulfill order + print(f"Payment completed: {txn_id}, Amount: ${mc_gross}") + +def handle_refund(ipn_data): + """Handle refund.""" + parent_txn_id = ipn_data.get('parent_txn_id') + mc_gross = ipn_data.get('mc_gross') + + # Process refund in your system + print(f"Refund processed: {parent_txn_id}, Amount: ${mc_gross}") + +def handle_chargeback(ipn_data): + """Handle payment reversal/chargeback.""" + txn_id = ipn_data.get('txn_id') + reason_code = ipn_data.get('reason_code') + + # Handle chargeback + print(f"Chargeback: {txn_id}, Reason: {reason_code}") +``` + +## Subscription/Recurring Billing + +### Create Subscription Plan +```python +def create_subscription_plan(name, amount, interval='MONTH'): + """Create a subscription plan.""" + client = PayPalClient(CLIENT_ID, CLIENT_SECRET) + + url = f"{client.base_url}/v1/billing/plans" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {client.access_token}" + } + + payload = { + "product_id": "PRODUCT_ID", # Create product first + "name": name, + "billing_cycles": [{ + "frequency": { + "interval_unit": interval, + "interval_count": 1 + }, + "tenure_type": "REGULAR", + "sequence": 1, + "total_cycles": 0, # Infinite + "pricing_scheme": { + "fixed_price": { + "value": str(amount), + "currency_code": "USD" + } + } + }], + "payment_preferences": { + "auto_bill_outstanding": True, + "setup_fee": { + "value": "0", + "currency_code": "USD" + }, + "setup_fee_failure_action": "CONTINUE", + "payment_failure_threshold": 3 + } + } + + response = requests.post(url, headers=headers, json=payload) + return response.json() + +def create_subscription(plan_id, subscriber_email): + """Create a subscription for a customer.""" + client = PayPalClient(CLIENT_ID, CLIENT_SECRET) + + url = f"{client.base_url}/v1/billing/subscriptions" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {client.access_token}" + } + + payload = { + "plan_id": plan_id, + "subscriber": { + "email_address": subscriber_email + }, + "application_context": { + "return_url": "https://yourdomain.com/subscription/success", + "cancel_url": "https://yourdomain.com/subscription/cancel" + } + } + + response = requests.post(url, headers=headers, json=payload) + subscription = response.json() + + # Get approval URL + for link in subscription.get('links', []): + if link['rel'] == 'approve': + return { + 'subscription_id': subscription['id'], + 'approval_url': link['href'] + } +``` + +## Refund Workflows + +```python +def create_refund(capture_id, amount=None, note=None): + """Create a refund for a captured payment.""" + client = PayPalClient(CLIENT_ID, CLIENT_SECRET) + + url = f"{client.base_url}/v2/payments/captures/{capture_id}/refund" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {client.access_token}" + } + + payload = {} + if amount: + payload["amount"] = { + "value": str(amount), + "currency_code": "USD" + } + + if note: + payload["note_to_payer"] = note + + response = requests.post(url, headers=headers, json=payload) + return response.json() + +def get_refund_details(refund_id): + """Get refund details.""" + client = PayPalClient(CLIENT_ID, CLIENT_SECRET) + + url = f"{client.base_url}/v2/payments/refunds/{refund_id}" + headers = { + "Authorization": f"Bearer {client.access_token}" + } + + response = requests.get(url, headers=headers) + return response.json() +``` + +## Error Handling + +```python +class PayPalError(Exception): + """Custom PayPal error.""" + pass + +def handle_paypal_api_call(api_function): + """Wrapper for PayPal API calls with error handling.""" + try: + result = api_function() + return result + except requests.exceptions.RequestException as e: + # Network error + raise PayPalError(f"Network error: {str(e)}") + except Exception as e: + # Other errors + raise PayPalError(f"PayPal API error: {str(e)}") + +# Usage +try: + order = handle_paypal_api_call(lambda: client.create_order(25.00)) +except PayPalError as e: + # Handle error appropriately + log_error(e) +``` + +## Testing + +```python +# Use sandbox credentials +SANDBOX_CLIENT_ID = "..." +SANDBOX_SECRET = "..." + +# Test accounts +# Create test buyer and seller accounts at developer.paypal.com + +def test_payment_flow(): + """Test complete payment flow.""" + client = PayPalClient(SANDBOX_CLIENT_ID, SANDBOX_SECRET, mode='sandbox') + + # Create order + order = client.create_order(10.00) + assert 'id' in order + + # Get approval URL + approval_url = next((link['href'] for link in order['links'] if link['rel'] == 'approve'), None) + assert approval_url is not None + + # After approval (manual step with test account) + # Capture order + # captured = client.capture_order(order['id']) + # assert captured['status'] == 'COMPLETED' +``` + +## Resources + +- **references/express-checkout.md**: Express Checkout implementation guide +- **references/ipn-handling.md**: IPN verification and processing +- **references/refund-workflows.md**: Refund handling patterns +- **references/billing-agreements.md**: Recurring billing setup +- **assets/paypal-client.py**: Production PayPal client +- **assets/ipn-processor.py**: IPN webhook processor +- **assets/recurring-billing.py**: Subscription management + +## Best Practices + +1. **Always Verify IPN**: Never trust IPN without verification +2. **Idempotent Processing**: Handle duplicate IPN notifications +3. **Error Handling**: Implement robust error handling +4. **Logging**: Log all transactions and errors +5. **Test Thoroughly**: Use sandbox extensively +6. **Webhook Backup**: Don't rely solely on client-side callbacks +7. **Currency Handling**: Always specify currency explicitly + +## Common Pitfalls + +- **Not Verifying IPN**: Accepting IPN without verification +- **Duplicate Processing**: Not checking for duplicate transactions +- **Wrong Environment**: Mixing sandbox and production URLs/credentials +- **Missing Webhooks**: Not handling all payment states +- **Hardcoded Values**: Not making configurable for different environments diff --git a/plugins/antigravity-bundle-commerce-payments/skills/plaid-fintech/SKILL.md b/plugins/antigravity-bundle-commerce-payments/skills/plaid-fintech/SKILL.md new file mode 100644 index 00000000..298595c6 --- /dev/null +++ b/plugins/antigravity-bundle-commerce-payments/skills/plaid-fintech/SKILL.md @@ -0,0 +1,52 @@ +--- +name: plaid-fintech +description: "Create a linktoken for Plaid Link, exchange publictoken for accesstoken. Link tokens are short-lived, one-time use. Access tokens don't expire but may need updating when users change passwords." +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# Plaid Fintech + +## Patterns + +### Link Token Creation and Exchange + +Create a link_token for Plaid Link, exchange public_token for access_token. +Link tokens are short-lived, one-time use. Access tokens don't expire but +may need updating when users change passwords. + +### Transactions Sync + +Use /transactions/sync for incremental transaction updates. More efficient +than /transactions/get. Handle webhooks for real-time updates instead of +polling. + +### Item Error Handling and Update Mode + +Handle ITEM_LOGIN_REQUIRED errors by putting users through Link update mode. +Listen for PENDING_DISCONNECT webhook to proactively prompt users. + +## Anti-Patterns + +### โŒ Storing Access Tokens in Plain Text + +### โŒ Polling Instead of Webhooks + +### โŒ Ignoring Item Errors + +## โš ๏ธ Sharp Edges + +| Issue | Severity | Solution | +|-------|----------|----------| +| Issue | critical | See docs | +| Issue | high | See docs | +| Issue | high | See docs | +| Issue | high | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-commerce-payments/skills/stripe-integration/SKILL.md b/plugins/antigravity-bundle-commerce-payments/skills/stripe-integration/SKILL.md new file mode 100644 index 00000000..faa7f3e7 --- /dev/null +++ b/plugins/antigravity-bundle-commerce-payments/skills/stripe-integration/SKILL.md @@ -0,0 +1,457 @@ +--- +name: stripe-integration +description: "Master Stripe payment processing integration for robust, PCI-compliant payment flows including checkout, subscriptions, webhooks, and refunds." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Stripe Integration + +Master Stripe payment processing integration for robust, PCI-compliant payment flows including checkout, subscriptions, webhooks, and refunds. + +## Do not use this skill when + +- The task is unrelated to stripe integration +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Use this skill when + +- Implementing payment processing in web/mobile applications +- Setting up subscription billing systems +- Handling one-time payments and recurring charges +- Processing refunds and disputes +- Managing customer payment methods +- Implementing SCA (Strong Customer Authentication) for European payments +- Building marketplace payment flows with Stripe Connect + +## Core Concepts + +### 1. Payment Flows +**Checkout Session (Hosted)** +- Stripe-hosted payment page +- Minimal PCI compliance burden +- Fastest implementation +- Supports one-time and recurring payments + +**Payment Intents (Custom UI)** +- Full control over payment UI +- Requires Stripe.js for PCI compliance +- More complex implementation +- Better customization options + +**Setup Intents (Save Payment Methods)** +- Collect payment method without charging +- Used for subscriptions and future payments +- Requires customer confirmation + +### 2. Webhooks +**Critical Events:** +- `payment_intent.succeeded`: Payment completed +- `payment_intent.payment_failed`: Payment failed +- `customer.subscription.updated`: Subscription changed +- `customer.subscription.deleted`: Subscription canceled +- `charge.refunded`: Refund processed +- `invoice.payment_succeeded`: Subscription payment successful + +### 3. Subscriptions +**Components:** +- **Product**: What you're selling +- **Price**: How much and how often +- **Subscription**: Customer's recurring payment +- **Invoice**: Generated for each billing cycle + +### 4. Customer Management +- Create and manage customer records +- Store multiple payment methods +- Track customer metadata +- Manage billing details + +## Quick Start + +```python +import stripe + +stripe.api_key = "sk_test_..." + +# Create a checkout session +session = stripe.checkout.Session.create( + payment_method_types=['card'], + line_items=[{ + 'price_data': { + 'currency': 'usd', + 'product_data': { + 'name': 'Premium Subscription', + }, + 'unit_amount': 2000, # $20.00 + 'recurring': { + 'interval': 'month', + }, + }, + 'quantity': 1, + }], + mode='subscription', + success_url='https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}', + cancel_url='https://yourdomain.com/cancel', +) + +# Redirect user to session.url +print(session.url) +``` + +## Payment Implementation Patterns + +### Pattern 1: One-Time Payment (Hosted Checkout) +```python +def create_checkout_session(amount, currency='usd'): + """Create a one-time payment checkout session.""" + try: + session = stripe.checkout.Session.create( + payment_method_types=['card'], + line_items=[{ + 'price_data': { + 'currency': currency, + 'product_data': { + 'name': 'Purchase', + 'images': ['https://example.com/product.jpg'], + }, + 'unit_amount': amount, # Amount in cents + }, + 'quantity': 1, + }], + mode='payment', + success_url='https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}', + cancel_url='https://yourdomain.com/cancel', + metadata={ + 'order_id': 'order_123', + 'user_id': 'user_456' + } + ) + return session + except stripe.error.StripeError as e: + # Handle error + print(f"Stripe error: {e.user_message}") + raise +``` + +### Pattern 2: Custom Payment Intent Flow +```python +def create_payment_intent(amount, currency='usd', customer_id=None): + """Create a payment intent for custom checkout UI.""" + intent = stripe.PaymentIntent.create( + amount=amount, + currency=currency, + customer=customer_id, + automatic_payment_methods={ + 'enabled': True, + }, + metadata={ + 'integration_check': 'accept_a_payment' + } + ) + return intent.client_secret # Send to frontend + +# Frontend (JavaScript) +""" +const stripe = Stripe('pk_test_...'); +const elements = stripe.elements(); +const cardElement = elements.create('card'); +cardElement.mount('#card-element'); + +const {error, paymentIntent} = await stripe.confirmCardPayment( + clientSecret, + { + payment_method: { + card: cardElement, + billing_details: { + name: 'Customer Name' + } + } + } +); + +if (error) { + // Handle error +} else if (paymentIntent.status === 'succeeded') { + // Payment successful +} +""" +``` + +### Pattern 3: Subscription Creation +```python +def create_subscription(customer_id, price_id): + """Create a subscription for a customer.""" + try: + subscription = stripe.Subscription.create( + customer=customer_id, + items=[{'price': price_id}], + payment_behavior='default_incomplete', + payment_settings={'save_default_payment_method': 'on_subscription'}, + expand=['latest_invoice.payment_intent'], + ) + + return { + 'subscription_id': subscription.id, + 'client_secret': subscription.latest_invoice.payment_intent.client_secret + } + except stripe.error.StripeError as e: + print(f"Subscription creation failed: {e}") + raise +``` + +### Pattern 4: Customer Portal +```python +def create_customer_portal_session(customer_id): + """Create a portal session for customers to manage subscriptions.""" + session = stripe.billing_portal.Session.create( + customer=customer_id, + return_url='https://yourdomain.com/account', + ) + return session.url # Redirect customer here +``` + +## Webhook Handling + +### Secure Webhook Endpoint +```python +from flask import Flask, request +import stripe + +app = Flask(__name__) + +endpoint_secret = 'whsec_...' + +@app.route('/webhook', methods=['POST']) +def webhook(): + payload = request.data + sig_header = request.headers.get('Stripe-Signature') + + try: + event = stripe.Webhook.construct_event( + payload, sig_header, endpoint_secret + ) + except ValueError: + # Invalid payload + return 'Invalid payload', 400 + except stripe.error.SignatureVerificationError: + # Invalid signature + return 'Invalid signature', 400 + + # Handle the event + if event['type'] == 'payment_intent.succeeded': + payment_intent = event['data']['object'] + handle_successful_payment(payment_intent) + elif event['type'] == 'payment_intent.payment_failed': + payment_intent = event['data']['object'] + handle_failed_payment(payment_intent) + elif event['type'] == 'customer.subscription.deleted': + subscription = event['data']['object'] + handle_subscription_canceled(subscription) + + return 'Success', 200 + +def handle_successful_payment(payment_intent): + """Process successful payment.""" + customer_id = payment_intent.get('customer') + amount = payment_intent['amount'] + metadata = payment_intent.get('metadata', {}) + + # Update your database + # Send confirmation email + # Fulfill order + print(f"Payment succeeded: {payment_intent['id']}") + +def handle_failed_payment(payment_intent): + """Handle failed payment.""" + error = payment_intent.get('last_payment_error', {}) + print(f"Payment failed: {error.get('message')}") + # Notify customer + # Update order status + +def handle_subscription_canceled(subscription): + """Handle subscription cancellation.""" + customer_id = subscription['customer'] + # Update user access + # Send cancellation email + print(f"Subscription canceled: {subscription['id']}") +``` + +### Webhook Best Practices +```python +import hashlib +import hmac + +def verify_webhook_signature(payload, signature, secret): + """Manually verify webhook signature.""" + expected_sig = hmac.new( + secret.encode('utf-8'), + payload, + hashlib.sha256 + ).hexdigest() + + return hmac.compare_digest(signature, expected_sig) + +def handle_webhook_idempotently(event_id, handler): + """Ensure webhook is processed exactly once.""" + # Check if event already processed + if is_event_processed(event_id): + return + + # Process event + try: + handler() + mark_event_processed(event_id) + except Exception as e: + log_error(e) + # Stripe will retry failed webhooks + raise +``` + +## Customer Management + +```python +def create_customer(email, name, payment_method_id=None): + """Create a Stripe customer.""" + customer = stripe.Customer.create( + email=email, + name=name, + payment_method=payment_method_id, + invoice_settings={ + 'default_payment_method': payment_method_id + } if payment_method_id else None, + metadata={ + 'user_id': '12345' + } + ) + return customer + +def attach_payment_method(customer_id, payment_method_id): + """Attach a payment method to a customer.""" + stripe.PaymentMethod.attach( + payment_method_id, + customer=customer_id + ) + + # Set as default + stripe.Customer.modify( + customer_id, + invoice_settings={ + 'default_payment_method': payment_method_id + } + ) + +def list_customer_payment_methods(customer_id): + """List all payment methods for a customer.""" + payment_methods = stripe.PaymentMethod.list( + customer=customer_id, + type='card' + ) + return payment_methods.data +``` + +## Refund Handling + +```python +def create_refund(payment_intent_id, amount=None, reason=None): + """Create a refund.""" + refund_params = { + 'payment_intent': payment_intent_id + } + + if amount: + refund_params['amount'] = amount # Partial refund + + if reason: + refund_params['reason'] = reason # 'duplicate', 'fraudulent', 'requested_by_customer' + + refund = stripe.Refund.create(**refund_params) + return refund + +def handle_dispute(charge_id, evidence): + """Update dispute with evidence.""" + stripe.Dispute.modify( + charge_id, + evidence={ + 'customer_name': evidence.get('customer_name'), + 'customer_email_address': evidence.get('customer_email'), + 'shipping_documentation': evidence.get('shipping_proof'), + 'customer_communication': evidence.get('communication'), + } + ) +``` + +## Testing + +```python +# Use test mode keys +stripe.api_key = "sk_test_..." + +# Test card numbers +TEST_CARDS = { + 'success': '4242424242424242', + 'declined': '4000000000000002', + '3d_secure': '4000002500003155', + 'insufficient_funds': '4000000000009995' +} + +def test_payment_flow(): + """Test complete payment flow.""" + # Create test customer + customer = stripe.Customer.create( + email="test@example.com" + ) + + # Create payment intent + intent = stripe.PaymentIntent.create( + amount=1000, + currency='usd', + customer=customer.id, + payment_method_types=['card'] + ) + + # Confirm with test card + confirmed = stripe.PaymentIntent.confirm( + intent.id, + payment_method='pm_card_visa' # Test payment method + ) + + assert confirmed.status == 'succeeded' +``` + +## Resources + +- **references/checkout-flows.md**: Detailed checkout implementation +- **references/webhook-handling.md**: Webhook security and processing +- **references/subscription-management.md**: Subscription lifecycle +- **references/customer-management.md**: Customer and payment method handling +- **references/invoice-generation.md**: Invoicing and billing +- **assets/stripe-client.py**: Production-ready Stripe client wrapper +- **assets/webhook-handler.py**: Complete webhook processor +- **assets/checkout-config.json**: Checkout configuration templates + +## Best Practices + +1. **Always Use Webhooks**: Don't rely solely on client-side confirmation +2. **Idempotency**: Handle webhook events idempotently +3. **Error Handling**: Gracefully handle all Stripe errors +4. **Test Mode**: Thoroughly test with test keys before production +5. **Metadata**: Use metadata to link Stripe objects to your database +6. **Monitoring**: Track payment success rates and errors +7. **PCI Compliance**: Never handle raw card data on your server +8. **SCA Ready**: Implement 3D Secure for European payments + +## Common Pitfalls + +- **Not Verifying Webhooks**: Always verify webhook signatures +- **Missing Webhook Events**: Handle all relevant webhook events +- **Hardcoded Amounts**: Use cents/smallest currency unit +- **No Retry Logic**: Implement retries for API calls +- **Ignoring Test Mode**: Test all edge cases with test cards diff --git a/plugins/antigravity-bundle-creative-director/.codex-plugin/plugin.json b/plugins/antigravity-bundle-creative-director/.codex-plugin/plugin.json new file mode 100644 index 00000000..ab4af6b6 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-creative-director", + "version": "8.10.0", + "description": "Install the \"Creative Director\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "creative-director", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Creative Director", + "shortDescription": "Creative & Content ยท 6 curated skills", + "longDescription": "For visuals, content, and branding. Covers Canvas Design, Frontend Design, and 4 more skills.", + "developerName": "sickn33 and contributors", + "category": "Creative & Content", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-creative-director/skills/algorithmic-art/LICENSE.txt b/plugins/antigravity-bundle-creative-director/skills/algorithmic-art/LICENSE.txt new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/algorithmic-art/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/plugins/antigravity-bundle-creative-director/skills/algorithmic-art/SKILL.md b/plugins/antigravity-bundle-creative-director/skills/algorithmic-art/SKILL.md new file mode 100644 index 00000000..14e723af --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/algorithmic-art/SKILL.md @@ -0,0 +1,410 @@ +--- +name: algorithmic-art +description: "Algorithmic philosophies are computational aesthetic movements that are then expressed through code. Output .md files (philosophy), .html files (interactive viewer), and .js files (generative algorithms)." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +Algorithmic philosophies are computational aesthetic movements that are then expressed through code. Output .md files (philosophy), .html files (interactive viewer), and .js files (generative algorithms). + +This happens in two steps: +1. Algorithmic Philosophy Creation (.md file) +2. Express by creating p5.js generative art (.html + .js files) + +First, undertake this task: + +## ALGORITHMIC PHILOSOPHY CREATION + +To begin, create an ALGORITHMIC PHILOSOPHY (not static images or templates) that will be interpreted through: +- Computational processes, emergent behavior, mathematical beauty +- Seeded randomness, noise fields, organic systems +- Particles, flows, fields, forces +- Parametric variation and controlled chaos + +### THE CRITICAL UNDERSTANDING +- What is received: Some subtle input or instructions by the user to take into account, but use as a foundation; it should not constrain creative freedom. +- What is created: An algorithmic philosophy/generative aesthetic movement. +- What happens next: The same version receives the philosophy and EXPRESSES IT IN CODE - creating p5.js sketches that are 90% algorithmic generation, 10% essential parameters. + +Consider this approach: +- Write a manifesto for a generative art movement +- The next phase involves writing the algorithm that brings it to life + +The philosophy must emphasize: Algorithmic expression. Emergent behavior. Computational beauty. Seeded variation. + +### HOW TO GENERATE AN ALGORITHMIC PHILOSOPHY + +**Name the movement** (1-2 words): "Organic Turbulence" / "Quantum Harmonics" / "Emergent Stillness" + +**Articulate the philosophy** (4-6 paragraphs - concise but complete): + +To capture the ALGORITHMIC essence, express how this philosophy manifests through: +- Computational processes and mathematical relationships? +- Noise functions and randomness patterns? +- Particle behaviors and field dynamics? +- Temporal evolution and system states? +- Parametric variation and emergent complexity? + +**CRITICAL GUIDELINES:** +- **Avoid redundancy**: Each algorithmic aspect should be mentioned once. Avoid repeating concepts about noise theory, particle dynamics, or mathematical principles unless adding new depth. +- **Emphasize craftsmanship REPEATEDLY**: The philosophy MUST stress multiple times that the final algorithm should appear as though it took countless hours to develop, was refined with care, and comes from someone at the absolute top of their field. This framing is essential - repeat phrases like "meticulously crafted algorithm," "the product of deep computational expertise," "painstaking optimization," "master-level implementation." +- **Leave creative space**: Be specific about the algorithmic direction, but concise enough that the next Claude has room to make interpretive implementation choices at an extremely high level of craftsmanship. + +The philosophy must guide the next version to express ideas ALGORITHMICALLY, not through static images. Beauty lives in the process, not the final frame. + +### PHILOSOPHY EXAMPLES + +**"Organic Turbulence"** +Philosophy: Chaos constrained by natural law, order emerging from disorder. +Algorithmic expression: Flow fields driven by layered Perlin noise. Thousands of particles following vector forces, their trails accumulating into organic density maps. Multiple noise octaves create turbulent regions and calm zones. Color emerges from velocity and density - fast particles burn bright, slow ones fade to shadow. The algorithm runs until equilibrium - a meticulously tuned balance where every parameter was refined through countless iterations by a master of computational aesthetics. + +**"Quantum Harmonics"** +Philosophy: Discrete entities exhibiting wave-like interference patterns. +Algorithmic expression: Particles initialized on a grid, each carrying a phase value that evolves through sine waves. When particles are near, their phases interfere - constructive interference creates bright nodes, destructive creates voids. Simple harmonic motion generates complex emergent mandalas. The result of painstaking frequency calibration where every ratio was carefully chosen to produce resonant beauty. + +**"Recursive Whispers"** +Philosophy: Self-similarity across scales, infinite depth in finite space. +Algorithmic expression: Branching structures that subdivide recursively. Each branch slightly randomized but constrained by golden ratios. L-systems or recursive subdivision generate tree-like forms that feel both mathematical and organic. Subtle noise perturbations break perfect symmetry. Line weights diminish with each recursion level. Every branching angle the product of deep mathematical exploration. + +**"Field Dynamics"** +Philosophy: Invisible forces made visible through their effects on matter. +Algorithmic expression: Vector fields constructed from mathematical functions or noise. Particles born at edges, flowing along field lines, dying when they reach equilibrium or boundaries. Multiple fields can attract, repel, or rotate particles. The visualization shows only the traces - ghost-like evidence of invisible forces. A computational dance meticulously choreographed through force balance. + +**"Stochastic Crystallization"** +Philosophy: Random processes crystallizing into ordered structures. +Algorithmic expression: Randomized circle packing or Voronoi tessellation. Start with random points, let them evolve through relaxation algorithms. Cells push apart until equilibrium. Color based on cell size, neighbor count, or distance from center. The organic tiling that emerges feels both random and inevitable. Every seed produces unique crystalline beauty - the mark of a master-level generative algorithm. + +*These are condensed examples. The actual algorithmic philosophy should be 4-6 substantial paragraphs.* + +### ESSENTIAL PRINCIPLES +- **ALGORITHMIC PHILOSOPHY**: Creating a computational worldview to be expressed through code +- **PROCESS OVER PRODUCT**: Always emphasize that beauty emerges from the algorithm's execution - each run is unique +- **PARAMETRIC EXPRESSION**: Ideas communicate through mathematical relationships, forces, behaviors - not static composition +- **ARTISTIC FREEDOM**: The next Claude interprets the philosophy algorithmically - provide creative implementation room +- **PURE GENERATIVE ART**: This is about making LIVING ALGORITHMS, not static images with randomness +- **EXPERT CRAFTSMANSHIP**: Repeatedly emphasize the final algorithm must feel meticulously crafted, refined through countless iterations, the product of deep expertise by someone at the absolute top of their field in computational aesthetics + +**The algorithmic philosophy should be 4-6 paragraphs long.** Fill it with poetic computational philosophy that brings together the intended vision. Avoid repeating the same points. Output this algorithmic philosophy as a .md file. + +--- + +## DEDUCING THE CONCEPTUAL SEED + +**CRITICAL STEP**: Before implementing the algorithm, identify the subtle conceptual thread from the original request. + +**THE ESSENTIAL PRINCIPLE**: +The concept is a **subtle, niche reference embedded within the algorithm itself** - not always literal, always sophisticated. Someone familiar with the subject should feel it intuitively, while others simply experience a masterful generative composition. The algorithmic philosophy provides the computational language. The deduced concept provides the soul - the quiet conceptual DNA woven invisibly into parameters, behaviors, and emergence patterns. + +This is **VERY IMPORTANT**: The reference must be so refined that it enhances the work's depth without announcing itself. Think like a jazz musician quoting another song through algorithmic harmony - only those who know will catch it, but everyone appreciates the generative beauty. + +--- + +## P5.JS IMPLEMENTATION + +With the philosophy AND conceptual framework established, express it through code. Pause to gather thoughts before proceeding. Use only the algorithmic philosophy created and the instructions below. + +### โš ๏ธ STEP 0: READ THE TEMPLATE FIRST โš ๏ธ + +**CRITICAL: BEFORE writing any HTML:** + +1. **Read** `templates/viewer.html` using the Read tool +2. **Study** the exact structure, styling, and Anthropic branding +3. **Use that file as the LITERAL STARTING POINT** - not just inspiration +4. **Keep all FIXED sections exactly as shown** (header, sidebar structure, Anthropic colors/fonts, seed controls, action buttons) +5. **Replace only the VARIABLE sections** marked in the file's comments (algorithm, parameters, UI controls for parameters) + +**Avoid:** +- โŒ Creating HTML from scratch +- โŒ Inventing custom styling or color schemes +- โŒ Using system fonts or dark themes +- โŒ Changing the sidebar structure + +**Follow these practices:** +- โœ… Copy the template's exact HTML structure +- โœ… Keep Anthropic branding (Poppins/Lora fonts, light colors, gradient backdrop) +- โœ… Maintain the sidebar layout (Seed โ†’ Parameters โ†’ Colors? โ†’ Actions) +- โœ… Replace only the p5.js algorithm and parameter controls + +The template is the foundation. Build on it, don't rebuild it. + +--- + +To create gallery-quality computational art that lives and breathes, use the algorithmic philosophy as the foundation. + +### TECHNICAL REQUIREMENTS + +**Seeded Randomness (Art Blocks Pattern)**: +```javascript +// ALWAYS use a seed for reproducibility +let seed = 12345; // or hash from user input +randomSeed(seed); +noiseSeed(seed); +``` + +**Parameter Structure - FOLLOW THE PHILOSOPHY**: + +To establish parameters that emerge naturally from the algorithmic philosophy, consider: "What qualities of this system can be adjusted?" + +```javascript +let params = { + seed: 12345, // Always include seed for reproducibility + // colors + // Add parameters that control YOUR algorithm: + // - Quantities (how many?) + // - Scales (how big? how fast?) + // - Probabilities (how likely?) + // - Ratios (what proportions?) + // - Angles (what direction?) + // - Thresholds (when does behavior change?) +}; +``` + +**To design effective parameters, focus on the properties the system needs to be tunable rather than thinking in terms of "pattern types".** + +**Core Algorithm - EXPRESS THE PHILOSOPHY**: + +**CRITICAL**: The algorithmic philosophy should dictate what to build. + +To express the philosophy through code, avoid thinking "which pattern should I use?" and instead think "how to express this philosophy through code?" + +If the philosophy is about **organic emergence**, consider using: +- Elements that accumulate or grow over time +- Random processes constrained by natural rules +- Feedback loops and interactions + +If the philosophy is about **mathematical beauty**, consider using: +- Geometric relationships and ratios +- Trigonometric functions and harmonics +- Precise calculations creating unexpected patterns + +If the philosophy is about **controlled chaos**, consider using: +- Random variation within strict boundaries +- Bifurcation and phase transitions +- Order emerging from disorder + +**The algorithm flows from the philosophy, not from a menu of options.** + +To guide the implementation, let the conceptual essence inform creative and original choices. Build something that expresses the vision for this particular request. + +**Canvas Setup**: Standard p5.js structure: +```javascript +function setup() { + createCanvas(1200, 1200); + // Initialize your system +} + +function draw() { + // Your generative algorithm + // Can be static (noLoop) or animated +} +``` + +### CRAFTSMANSHIP REQUIREMENTS + +**CRITICAL**: To achieve mastery, create algorithms that feel like they emerged through countless iterations by a master generative artist. Tune every parameter carefully. Ensure every pattern emerges with purpose. This is NOT random noise - this is CONTROLLED CHAOS refined through deep expertise. + +- **Balance**: Complexity without visual noise, order without rigidity +- **Color Harmony**: Thoughtful palettes, not random RGB values +- **Composition**: Even in randomness, maintain visual hierarchy and flow +- **Performance**: Smooth execution, optimized for real-time if animated +- **Reproducibility**: Same seed ALWAYS produces identical output + +### OUTPUT FORMAT + +Output: +1. **Algorithmic Philosophy** - As markdown or text explaining the generative aesthetic +2. **Single HTML Artifact** - Self-contained interactive generative art built from `templates/viewer.html` (see STEP 0 and next section) + +The HTML artifact contains everything: p5.js (from CDN), the algorithm, parameter controls, and UI - all in one file that works immediately in claude.ai artifacts or any browser. Start from the template file, not from scratch. + +--- + +## INTERACTIVE ARTIFACT CREATION + +**REMINDER: `templates/viewer.html` should have already been read (see STEP 0). Use that file as the starting point.** + +To allow exploration of the generative art, create a single, self-contained HTML artifact. Ensure this artifact works immediately in claude.ai or any browser - no setup required. Embed everything inline. + +### CRITICAL: WHAT'S FIXED VS VARIABLE + +The `templates/viewer.html` file is the foundation. It contains the exact structure and styling needed. + +**FIXED (always include exactly as shown):** +- Layout structure (header, sidebar, main canvas area) +- Anthropic branding (UI colors, fonts, gradients) +- Seed section in sidebar: + - Seed display + - Previous/Next buttons + - Random button + - Jump to seed input + Go button +- Actions section in sidebar: + - Regenerate button + - Reset button + +**VARIABLE (customize for each artwork):** +- The entire p5.js algorithm (setup/draw/classes) +- The parameters object (define what the art needs) +- The Parameters section in sidebar: + - Number of parameter controls + - Parameter names + - Min/max/step values for sliders + - Control types (sliders, inputs, etc.) +- Colors section (optional): + - Some art needs color pickers + - Some art might use fixed colors + - Some art might be monochrome (no color controls needed) + - Decide based on the art's needs + +**Every artwork should have unique parameters and algorithm!** The fixed parts provide consistent UX - everything else expresses the unique vision. + +### REQUIRED FEATURES + +**1. Parameter Controls** +- Sliders for numeric parameters (particle count, noise scale, speed, etc.) +- Color pickers for palette colors +- Real-time updates when parameters change +- Reset button to restore defaults + +**2. Seed Navigation** +- Display current seed number +- "Previous" and "Next" buttons to cycle through seeds +- "Random" button for random seed +- Input field to jump to specific seed +- Generate 100 variations when requested (seeds 1-100) + +**3. Single Artifact Structure** +```html + + + + + + + + +
+
+ +
+ + + +``` + +**CRITICAL**: This is a single artifact. No external files, no imports (except p5.js CDN). Everything inline. + +**4. Implementation Details - BUILD THE SIDEBAR** + +The sidebar structure: + +**1. Seed (FIXED)** - Always include exactly as shown: +- Seed display +- Prev/Next/Random/Jump buttons + +**2. Parameters (VARIABLE)** - Create controls for the art: +```html +
+ + + ... +
+``` +Add as many control-group divs as there are parameters. + +**3. Colors (OPTIONAL/VARIABLE)** - Include if the art needs adjustable colors: +- Add color pickers if users should control palette +- Skip this section if the art uses fixed colors +- Skip if the art is monochrome + +**4. Actions (FIXED)** - Always include exactly as shown: +- Regenerate button +- Reset button +- Download PNG button + +**Requirements**: +- Seed controls must work (prev/next/random/jump/display) +- All parameters must have UI controls +- Regenerate, Reset, Download buttons must work +- Keep Anthropic branding (UI styling, not art colors) + +### USING THE ARTIFACT + +The HTML artifact works immediately: +1. **In claude.ai**: Displayed as an interactive artifact - runs instantly +2. **As a file**: Save and open in any browser - no server needed +3. **Sharing**: Send the HTML file - it's completely self-contained + +--- + +## VARIATIONS & EXPLORATION + +The artifact includes seed navigation by default (prev/next/random buttons), allowing users to explore variations without creating multiple files. If the user wants specific variations highlighted: + +- Include seed presets (buttons for "Variation 1: Seed 42", "Variation 2: Seed 127", etc.) +- Add a "Gallery Mode" that shows thumbnails of multiple seeds side-by-side +- All within the same single artifact + +This is like creating a series of prints from the same plate - the algorithm is consistent, but each seed reveals different facets of its potential. The interactive nature means users discover their own favorites by exploring the seed space. + +--- + +## THE CREATIVE PROCESS + +**User request** โ†’ **Algorithmic philosophy** โ†’ **Implementation** + +Each request is unique. The process involves: + +1. **Interpret the user's intent** - What aesthetic is being sought? +2. **Create an algorithmic philosophy** (4-6 paragraphs) describing the computational approach +3. **Implement it in code** - Build the algorithm that expresses this philosophy +4. **Design appropriate parameters** - What should be tunable? +5. **Build matching UI controls** - Sliders/inputs for those parameters + +**The constants**: +- Anthropic branding (colors, fonts, layout) +- Seed navigation (always present) +- Self-contained HTML artifact + +**Everything else is variable**: +- The algorithm itself +- The parameters +- The UI controls +- The visual outcome + +To achieve the best results, trust creativity and let the philosophy guide the implementation. + +--- + +## RESOURCES + +This skill includes helpful templates and documentation: + +- **templates/viewer.html**: REQUIRED STARTING POINT for all HTML artifacts. + - This is the foundation - contains the exact structure and Anthropic branding + - **Keep unchanged**: Layout structure, sidebar organization, Anthropic colors/fonts, seed controls, action buttons + - **Replace**: The p5.js algorithm, parameter definitions, and UI controls in Parameters section + - The extensive comments in the file mark exactly what to keep vs replace + +- **templates/generator_template.js**: Reference for p5.js best practices and code structure principles. + - Shows how to organize parameters, use seeded randomness, structure classes + - NOT a pattern menu - use these principles to build unique algorithms + - Embed algorithms inline in the HTML artifact (don't create separate .js files) + +**Critical reminder**: +- The **template is the STARTING POINT**, not inspiration +- The **algorithm is where to create** something unique +- Don't copy the flow field example - build what the philosophy demands +- But DO keep the exact UI structure and Anthropic branding from the template + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-creative-director/skills/algorithmic-art/templates/generator_template.js b/plugins/antigravity-bundle-creative-director/skills/algorithmic-art/templates/generator_template.js new file mode 100644 index 00000000..e263fbde --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/algorithmic-art/templates/generator_template.js @@ -0,0 +1,223 @@ +/** + * โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + * P5.JS GENERATIVE ART - BEST PRACTICES + * โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + * + * This file shows STRUCTURE and PRINCIPLES for p5.js generative art. + * It does NOT prescribe what art you should create. + * + * Your algorithmic philosophy should guide what you build. + * These are just best practices for how to structure your code. + * + * โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + */ + +// ============================================================================ +// 1. PARAMETER ORGANIZATION +// ============================================================================ +// Keep all tunable parameters in one object +// This makes it easy to: +// - Connect to UI controls +// - Reset to defaults +// - Serialize/save configurations + +let params = { + // Define parameters that match YOUR algorithm + // Examples (customize for your art): + // - Counts: how many elements (particles, circles, branches, etc.) + // - Scales: size, speed, spacing + // - Probabilities: likelihood of events + // - Angles: rotation, direction + // - Colors: palette arrays + + seed: 12345, + // define colorPalette as an array -- choose whatever colors you'd like ['#d97757', '#6a9bcc', '#788c5d', '#b0aea5'] + // Add YOUR parameters here based on your algorithm +}; + +// ============================================================================ +// 2. SEEDED RANDOMNESS (Critical for reproducibility) +// ============================================================================ +// ALWAYS use seeded random for Art Blocks-style reproducible output + +function initializeSeed(seed) { + randomSeed(seed); + noiseSeed(seed); + // Now all random() and noise() calls will be deterministic +} + +// ============================================================================ +// 3. P5.JS LIFECYCLE +// ============================================================================ + +function setup() { + createCanvas(800, 800); + + // Initialize seed first + initializeSeed(params.seed); + + // Set up your generative system + // This is where you initialize: + // - Arrays of objects + // - Grid structures + // - Initial positions + // - Starting states + + // For static art: call noLoop() at the end of setup + // For animated art: let draw() keep running +} + +function draw() { + // Option 1: Static generation (runs once, then stops) + // - Generate everything in setup() + // - Call noLoop() in setup() + // - draw() doesn't do much or can be empty + + // Option 2: Animated generation (continuous) + // - Update your system each frame + // - Common patterns: particle movement, growth, evolution + // - Can optionally call noLoop() after N frames + + // Option 3: User-triggered regeneration + // - Use noLoop() by default + // - Call redraw() when parameters change +} + +// ============================================================================ +// 4. CLASS STRUCTURE (When you need objects) +// ============================================================================ +// Use classes when your algorithm involves multiple entities +// Examples: particles, agents, cells, nodes, etc. + +class Entity { + constructor() { + // Initialize entity properties + // Use random() here - it will be seeded + } + + update() { + // Update entity state + // This might involve: + // - Physics calculations + // - Behavioral rules + // - Interactions with neighbors + } + + display() { + // Render the entity + // Keep rendering logic separate from update logic + } +} + +// ============================================================================ +// 5. PERFORMANCE CONSIDERATIONS +// ============================================================================ + +// For large numbers of elements: +// - Pre-calculate what you can +// - Use simple collision detection (spatial hashing if needed) +// - Limit expensive operations (sqrt, trig) when possible +// - Consider using p5 vectors efficiently + +// For smooth animation: +// - Aim for 60fps +// - Profile if things are slow +// - Consider reducing particle counts or simplifying calculations + +// ============================================================================ +// 6. UTILITY FUNCTIONS +// ============================================================================ + +// Color utilities +function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +} + +function colorFromPalette(index) { + return params.colorPalette[index % params.colorPalette.length]; +} + +// Mapping and easing +function mapRange(value, inMin, inMax, outMin, outMax) { + return outMin + (outMax - outMin) * ((value - inMin) / (inMax - inMin)); +} + +function easeInOutCubic(t) { + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; +} + +// Constrain to bounds +function wrapAround(value, max) { + if (value < 0) return max; + if (value > max) return 0; + return value; +} + +// ============================================================================ +// 7. PARAMETER UPDATES (Connect to UI) +// ============================================================================ + +function updateParameter(paramName, value) { + params[paramName] = value; + // Decide if you need to regenerate or just update + // Some params can update in real-time, others need full regeneration +} + +function regenerate() { + // Reinitialize your generative system + // Useful when parameters change significantly + initializeSeed(params.seed); + // Then regenerate your system +} + +// ============================================================================ +// 8. COMMON P5.JS PATTERNS +// ============================================================================ + +// Drawing with transparency for trails/fading +function fadeBackground(opacity) { + fill(250, 249, 245, opacity); // Anthropic light with alpha + noStroke(); + rect(0, 0, width, height); +} + +// Using noise for organic variation +function getNoiseValue(x, y, scale = 0.01) { + return noise(x * scale, y * scale); +} + +// Creating vectors from angles +function vectorFromAngle(angle, magnitude = 1) { + return createVector(cos(angle), sin(angle)).mult(magnitude); +} + +// ============================================================================ +// 9. EXPORT FUNCTIONS +// ============================================================================ + +function exportImage() { + saveCanvas('generative-art-' + params.seed, 'png'); +} + +// ============================================================================ +// REMEMBER +// ============================================================================ +// +// These are TOOLS and PRINCIPLES, not a recipe. +// Your algorithmic philosophy should guide WHAT you create. +// This structure helps you create it WELL. +// +// Focus on: +// - Clean, readable code +// - Parameterized for exploration +// - Seeded for reproducibility +// - Performant execution +// +// The art itself is entirely up to you! +// +// ============================================================================ \ No newline at end of file diff --git a/plugins/antigravity-bundle-creative-director/skills/algorithmic-art/templates/viewer.html b/plugins/antigravity-bundle-creative-director/skills/algorithmic-art/templates/viewer.html new file mode 100644 index 00000000..88a50cce --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/algorithmic-art/templates/viewer.html @@ -0,0 +1,599 @@ + + + + + + + Generative Art Viewer + + + + + + + +
+ + + + +
+
+
Initializing generative art...
+
+
+
+ + + + diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/LICENSE.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/LICENSE.txt new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/SKILL.md b/plugins/antigravity-bundle-creative-director/skills/canvas-design/SKILL.md new file mode 100644 index 00000000..43c6f431 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/SKILL.md @@ -0,0 +1,135 @@ +--- +name: canvas-design +description: "These are instructions for creating design philosophies - aesthetic movements that are then EXPRESSED VISUALLY. Output only .md files, .pdf files, and .png files." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +These are instructions for creating design philosophies - aesthetic movements that are then EXPRESSED VISUALLY. Output only .md files, .pdf files, and .png files. + +Complete this in two steps: +1. Design Philosophy Creation (.md file) +2. Express by creating it on a canvas (.pdf file or .png file) + +First, undertake this task: + +## DESIGN PHILOSOPHY CREATION + +To begin, create a VISUAL PHILOSOPHY (not layouts or templates) that will be interpreted through: +- Form, space, color, composition +- Images, graphics, shapes, patterns +- Minimal text as visual accent + +### THE CRITICAL UNDERSTANDING +- What is received: Some subtle input or instructions by the user that should be taken into account, but used as a foundation; it should not constrain creative freedom. +- What is created: A design philosophy/aesthetic movement. +- What happens next: Then, the same version receives the philosophy and EXPRESSES IT VISUALLY - creating artifacts that are 90% visual design, 10% essential text. + +Consider this approach: +- Write a manifesto for an art movement +- The next phase involves making the artwork + +The philosophy must emphasize: Visual expression. Spatial communication. Artistic interpretation. Minimal words. + +### HOW TO GENERATE A VISUAL PHILOSOPHY + +**Name the movement** (1-2 words): "Brutalist Joy" / "Chromatic Silence" / "Metabolist Dreams" + +**Articulate the philosophy** (4-6 paragraphs - concise but complete): + +To capture the VISUAL essence, express how the philosophy manifests through: +- Space and form +- Color and material +- Scale and rhythm +- Composition and balance +- Visual hierarchy + +**CRITICAL GUIDELINES:** +- **Avoid redundancy**: Each design aspect should be mentioned once. Avoid repeating points about color theory, spatial relationships, or typographic principles unless adding new depth. +- **Emphasize craftsmanship REPEATEDLY**: The philosophy MUST stress multiple times that the final work should appear as though it took countless hours to create, was labored over with care, and comes from someone at the absolute top of their field. This framing is essential - repeat phrases like "meticulously crafted," "the product of deep expertise," "painstaking attention," "master-level execution." +- **Leave creative space**: Remain specific about the aesthetic direction, but concise enough that the next Claude has room to make interpretive choices also at a extremely high level of craftmanship. + +The philosophy must guide the next version to express ideas VISUALLY, not through text. Information lives in design, not paragraphs. + +### PHILOSOPHY EXAMPLES + +**"Concrete Poetry"** +Philosophy: Communication through monumental form and bold geometry. +Visual expression: Massive color blocks, sculptural typography (huge single words, tiny labels), Brutalist spatial divisions, Polish poster energy meets Le Corbusier. Ideas expressed through visual weight and spatial tension, not explanation. Text as rare, powerful gesture - never paragraphs, only essential words integrated into the visual architecture. Every element placed with the precision of a master craftsman. + +**"Chromatic Language"** +Philosophy: Color as the primary information system. +Visual expression: Geometric precision where color zones create meaning. Typography minimal - small sans-serif labels letting chromatic fields communicate. Think Josef Albers' interaction meets data visualization. Information encoded spatially and chromatically. Words only to anchor what color already shows. The result of painstaking chromatic calibration. + +**"Analog Meditation"** +Philosophy: Quiet visual contemplation through texture and breathing room. +Visual expression: Paper grain, ink bleeds, vast negative space. Photography and illustration dominate. Typography whispered (small, restrained, serving the visual). Japanese photobook aesthetic. Images breathe across pages. Text appears sparingly - short phrases, never explanatory blocks. Each composition balanced with the care of a meditation practice. + +**"Organic Systems"** +Philosophy: Natural clustering and modular growth patterns. +Visual expression: Rounded forms, organic arrangements, color from nature through architecture. Information shown through visual diagrams, spatial relationships, iconography. Text only for key labels floating in space. The composition tells the story through expert spatial orchestration. + +**"Geometric Silence"** +Philosophy: Pure order and restraint. +Visual expression: Grid-based precision, bold photography or stark graphics, dramatic negative space. Typography precise but minimal - small essential text, large quiet zones. Swiss formalism meets Brutalist material honesty. Structure communicates, not words. Every alignment the work of countless refinements. + +*These are condensed examples. The actual design philosophy should be 4-6 substantial paragraphs.* + +### ESSENTIAL PRINCIPLES +- **VISUAL PHILOSOPHY**: Create an aesthetic worldview to be expressed through design +- **MINIMAL TEXT**: Always emphasize that text is sparse, essential-only, integrated as visual element - never lengthy +- **SPATIAL EXPRESSION**: Ideas communicate through space, form, color, composition - not paragraphs +- **ARTISTIC FREEDOM**: The next Claude interprets the philosophy visually - provide creative room +- **PURE DESIGN**: This is about making ART OBJECTS, not documents with decoration +- **EXPERT CRAFTSMANSHIP**: Repeatedly emphasize the final work must look meticulously crafted, labored over with care, the product of countless hours by someone at the top of their field + +**The design philosophy should be 4-6 paragraphs long.** Fill it with poetic design philosophy that brings together the core vision. Avoid repeating the same points. Keep the design philosophy generic without mentioning the intention of the art, as if it can be used wherever. Output the design philosophy as a .md file. + +--- + +## DEDUCING THE SUBTLE REFERENCE + +**CRITICAL STEP**: Before creating the canvas, identify the subtle conceptual thread from the original request. + +**THE ESSENTIAL PRINCIPLE**: +The topic is a **subtle, niche reference embedded within the art itself** - not always literal, always sophisticated. Someone familiar with the subject should feel it intuitively, while others simply experience a masterful abstract composition. The design philosophy provides the aesthetic language. The deduced topic provides the soul - the quiet conceptual DNA woven invisibly into form, color, and composition. + +This is **VERY IMPORTANT**: The reference must be refined so it enhances the work's depth without announcing itself. Think like a jazz musician quoting another song - only those who know will catch it, but everyone appreciates the music. + +--- + +## CANVAS CREATION + +With both the philosophy and the conceptual framework established, express it on a canvas. Take a moment to gather thoughts and clear the mind. Use the design philosophy created and the instructions below to craft a masterpiece, embodying all aspects of the philosophy with expert craftsmanship. + +**IMPORTANT**: For any type of content, even if the user requests something for a movie/game/book, the approach should still be sophisticated. Never lose sight of the idea that this should be art, not something that's cartoony or amateur. + +To create museum or magazine quality work, use the design philosophy as the foundation. Create one single page, highly visual, design-forward PDF or PNG output (unless asked for more pages). Generally use repeating patterns and perfect shapes. Treat the abstract philosophical design as if it were a scientific bible, borrowing the visual language of systematic observationโ€”dense accumulation of marks, repeated elements, or layered patterns that build meaning through patient repetition and reward sustained viewing. Add sparse, clinical typography and systematic reference markers that suggest this could be a diagram from an imaginary discipline, treating the invisible subject with the same reverence typically reserved for documenting observable phenomena. Anchor the piece with simple phrase(s) or details positioned subtly, using a limited color palette that feels intentional and cohesive. Embrace the paradox of using analytical visual language to express ideas about human experience: the result should feel like an artifact that proves something ephemeral can be studied, mapped, and understood through careful attention. This is true art. + +**Text as a contextual element**: Text is always minimal and visual-first, but let context guide whether that means whisper-quiet labels or bold typographic gestures. A punk venue poster might have larger, more aggressive type than a minimalist ceramics studio identity. Most of the time, font should be thin. All use of fonts must be design-forward and prioritize visual communication. Regardless of text scale, nothing falls off the page and nothing overlaps. Every element must be contained within the canvas boundaries with proper margins. Check carefully that all text, graphics, and visual elements have breathing room and clear separation. This is non-negotiable for professional execution. **IMPORTANT: Use different fonts if writing text. Search the `./canvas-fonts` directory. Regardless of approach, sophistication is non-negotiable.** + +Download and use whatever fonts are needed to make this a reality. Get creative by making the typography actually part of the art itself -- if the art is abstract, bring the font onto the canvas, not typeset digitally. + +To push boundaries, follow design instinct/intuition while using the philosophy as a guiding principle. Embrace ultimate design freedom and choice. Push aesthetics and design to the frontier. + +**CRITICAL**: To achieve human-crafted quality (not AI-generated), create work that looks like it took countless hours. Make it appear as though someone at the absolute top of their field labored over every detail with painstaking care. Ensure the composition, spacing, color choices, typography - everything screams expert-level craftsmanship. Double-check that nothing overlaps, formatting is flawless, every detail perfect. Create something that could be shown to people to prove expertise and rank as undeniably impressive. + +Output the final result as a single, downloadable .pdf or .png file, alongside the design philosophy used as a .md file. + +--- + +## FINAL STEP + +**IMPORTANT**: The user ALREADY said "It isn't perfect enough. It must be pristine, a masterpiece if craftsmanship, as if it were about to be displayed in a museum." + +**CRITICAL**: To refine the work, avoid adding more graphics; instead refine what has been created and make it extremely crisp, respecting the design philosophy and the principles of minimalism entirely. Rather than adding a fun filter or refactoring a font, consider how to make the existing composition more cohesive with the art. If the instinct is to call a new function or draw a new shape, STOP and instead ask: "How can I make what's already here more of a piece of art?" + +Take a second pass. Go back to the code and refine/polish further to make this a philosophically designed masterpiece. + +## MULTI-PAGE OPTION + +To create additional pages when requested, create more creative pages along the same lines as the design philosophy but distinctly different as well. Bundle those pages in the same .pdf or many .pngs. Treat the first page as just a single page in a whole coffee table book waiting to be filled. Make the next pages unique twists and memories of the original. Have them almost tell a story in a very tasteful way. Exercise full creative freedom. + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt new file mode 100644 index 00000000..1dad6ca6 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2012 The Arsenal Project Authors (andrij.design@gmail.com) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf new file mode 100644 index 00000000..fe5409b2 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf new file mode 100644 index 00000000..fc5f8fdd Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt new file mode 100644 index 00000000..b220280e --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2019 The Big Shoulders Project Authors (https://github.com/xotypeco/big_shoulders) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf new file mode 100644 index 00000000..de8308ce Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt new file mode 100644 index 00000000..1890cb1c --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2024 The Boldonse Project Authors (https://github.com/googlefonts/boldonse) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf new file mode 100644 index 00000000..43fa30af Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf new file mode 100644 index 00000000..f3b1deda Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt new file mode 100644 index 00000000..fc2b2167 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Bricolage Grotesque Project Authors (https://github.com/ateliertriay/bricolage) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf new file mode 100644 index 00000000..0674ae3e Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf new file mode 100644 index 00000000..58730fb4 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf new file mode 100644 index 00000000..786a1bd6 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt new file mode 100644 index 00000000..f976fdc9 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2018 The Crimson Pro Project Authors (https://github.com/Fonthausen/CrimsonPro) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf new file mode 100644 index 00000000..f5666b9b Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/DMMono-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/DMMono-OFL.txt new file mode 100644 index 00000000..5b17f0c6 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/DMMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The DM Mono Project Authors (https://www.github.com/googlefonts/dm-mono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf new file mode 100644 index 00000000..7efe813d Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt new file mode 100644 index 00000000..490d0120 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt @@ -0,0 +1,94 @@ +Copyright (c) 2011 by LatinoType Limitada (luciano@latinotype.com), +with Reserved Font Names "Erica One" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf new file mode 100644 index 00000000..8bd91d11 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf new file mode 100644 index 00000000..736ff7c3 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt new file mode 100644 index 00000000..679a685a --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf new file mode 100644 index 00000000..1a30262a Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Gloock-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Gloock-OFL.txt new file mode 100644 index 00000000..363acd33 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Gloock-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Gloock Project Authors (https://github.com/duartp/gloock) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf new file mode 100644 index 00000000..3e58c4e4 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf new file mode 100644 index 00000000..247979ca Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt new file mode 100644 index 00000000..e423b747 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright ยฉ 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf new file mode 100644 index 00000000..601ae945 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf new file mode 100644 index 00000000..78f6e500 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf new file mode 100644 index 00000000..369b89d2 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf new file mode 100644 index 00000000..a4d859a7 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf new file mode 100644 index 00000000..35f454ce Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf new file mode 100644 index 00000000..f602dcef Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf new file mode 100644 index 00000000..122b2730 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf new file mode 100644 index 00000000..4b98fb8d Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt new file mode 100644 index 00000000..4bb99142 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf new file mode 100644 index 00000000..14c6113c Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf new file mode 100644 index 00000000..8fa958d9 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf new file mode 100644 index 00000000..97630318 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Italiana-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Italiana-OFL.txt new file mode 100644 index 00000000..ba8af215 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Italiana-OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2011, Santiago Orozco (hi@typemade.mx), with Reserved Font Name "Italiana". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf new file mode 100644 index 00000000..a9b828c0 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf new file mode 100644 index 00000000..1926c804 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt new file mode 100644 index 00000000..5ceee002 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf new file mode 100644 index 00000000..436c982f Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Jura-Light.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Jura-Light.ttf new file mode 100644 index 00000000..dffbb339 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Jura-Light.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Jura-Medium.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Jura-Medium.ttf new file mode 100644 index 00000000..4bf91a33 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Jura-Medium.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Jura-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Jura-OFL.txt new file mode 100644 index 00000000..64ad4c67 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Jura-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2019 The Jura Project Authors (https://github.com/ossobuffo/jura) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt new file mode 100644 index 00000000..8c531fa5 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2012 The Libre Baskerville Project Authors (https://github.com/impallari/Libre-Baskerville) with Reserved Font Name Libre Baskerville. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf new file mode 100644 index 00000000..c1abc264 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Lora-Bold.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Lora-Bold.ttf new file mode 100644 index 00000000..edae21eb Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Lora-Bold.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf new file mode 100644 index 00000000..12dea8c6 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Lora-Italic.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Lora-Italic.ttf new file mode 100644 index 00000000..e24b69b2 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Lora-Italic.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Lora-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Lora-OFL.txt new file mode 100644 index 00000000..4cf1b950 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Lora-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Lora Project Authors (https://github.com/cyrealtype/Lora-Cyrillic), with Reserved Font Name "Lora". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Lora-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Lora-Regular.ttf new file mode 100644 index 00000000..dc751db0 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Lora-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf new file mode 100644 index 00000000..f4d7c021 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt new file mode 100644 index 00000000..f4ec3fba --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2025 The National Park Project Authors (https://github.com/benhoepner/National-Park) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf new file mode 100644 index 00000000..e4cbfbf5 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt new file mode 100644 index 00000000..c81eccde --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2010, Kimberly Geswein (kimberlygeswein.com) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf new file mode 100644 index 00000000..b086bced Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf new file mode 100644 index 00000000..f9f2f72a Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Outfit-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Outfit-OFL.txt new file mode 100644 index 00000000..fd0cb995 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Outfit-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2021 The Outfit Project Authors (https://github.com/Outfitio/Outfit-Fonts) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf new file mode 100644 index 00000000..3939ab24 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf new file mode 100644 index 00000000..95cd3725 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt new file mode 100644 index 00000000..b02d1b67 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2021 The Pixelify Sans Project Authors (https://github.com/eifetx/Pixelify-Sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt new file mode 100644 index 00000000..607bdad3 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2011, Denis Masharov (denis.masharov@gmail.com) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf new file mode 100644 index 00000000..b339511b Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf new file mode 100644 index 00000000..a6e3cf15 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt new file mode 100644 index 00000000..16cf394b --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2024 The Red Hat Project Authors (https://github.com/RedHatOfficial/RedHatFont) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf new file mode 100644 index 00000000..3bf6a698 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt new file mode 100644 index 00000000..a1fe7d5f --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2001 The Silkscreen Project Authors (https://github.com/googlefonts/silkscreen) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf new file mode 100644 index 00000000..8abaa7c5 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf new file mode 100644 index 00000000..0af9ead0 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt new file mode 100644 index 00000000..4c2f033a --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Smooch Sans Project Authors (https://github.com/googlefonts/smooch-sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf new file mode 100644 index 00000000..34fc7971 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Tektur-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Tektur-OFL.txt new file mode 100644 index 00000000..2cad55f1 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Tektur-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2023 The Tektur Project Authors (https://www.github.com/hyvyys/Tektur) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf new file mode 100644 index 00000000..f280fba4 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf new file mode 100644 index 00000000..5c979892 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf new file mode 100644 index 00000000..54418b8a Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf new file mode 100644 index 00000000..40529b68 Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt new file mode 100644 index 00000000..070f3416 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2019 The Work Sans Project Authors (https://github.com/weiweihuanghuang/Work-Sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf new file mode 100644 index 00000000..d24586cc Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt new file mode 100644 index 00000000..f09443cb --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2023 The Young Serif Project Authors (https://github.com/noirblancrouge/YoungSerif) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf new file mode 100644 index 00000000..f454fbed Binary files /dev/null and b/plugins/antigravity-bundle-creative-director/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf differ diff --git a/plugins/antigravity-bundle-creative-director/skills/content-creator/SKILL.md b/plugins/antigravity-bundle-creative-director/skills/content-creator/SKILL.md new file mode 100644 index 00000000..d7e9bb16 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/content-creator/SKILL.md @@ -0,0 +1,246 @@ +--- +name: content-creator +description: "Professional-grade brand voice analysis, SEO optimization, and platform-specific content frameworks." +category: marketing +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Content Creator + +Professional-grade brand voice analysis, SEO optimization, and platform-specific content frameworks. + +## When to Use +Use this skill when writing blog posts, creating social media content, establishing brand voice, optimizing content for SEO, or planning content calendars. + +## Keywords +content creation, blog posts, SEO, brand voice, social media, content calendar, marketing content, content strategy, content marketing, brand consistency, content optimization, social media marketing, content planning, blog writing, content frameworks, brand guidelines, social media strategy + +## Quick Start + +### For Brand Voice Development +1. Run `scripts/brand_voice_analyzer.py` on existing content to establish baseline +2. Review `references/brand_guidelines.md` to select voice attributes +3. Apply chosen voice consistently across all content + +### For Blog Content Creation +1. Choose template from `references/content_frameworks.md` +2. Research keywords for topic +3. Write content following template structure +4. Run `scripts/seo_optimizer.py [file] [primary-keyword]` to optimize +5. Apply recommendations before publishing + +### For Social Media Content +1. Review platform best practices in `references/social_media_optimization.md` +2. Use appropriate template from `references/content_frameworks.md` +3. Optimize based on platform-specific guidelines +4. Schedule using `assets/content_calendar_template.md` + +## Core Workflows + +### Establishing Brand Voice (First Time Setup) + +When creating content for a new brand or client: + +1. **Analyze Existing Content** (if available) + ```bash + python scripts/brand_voice_analyzer.py existing_content.txt + ``` + +2. **Define Voice Attributes** + - Review brand personality archetypes in `references/brand_guidelines.md` + - Select primary and secondary archetypes + - Choose 3-5 tone attributes + - Document in brand guidelines + +3. **Create Voice Sample** + - Write 3 sample pieces in chosen voice + - Test consistency using analyzer + - Refine based on results + +### Creating SEO-Optimized Blog Posts + +1. **Keyword Research** + - Identify primary keyword (search volume 500-5000/month) + - Find 3-5 secondary keywords + - List 10-15 LSI keywords + +2. **Content Structure** + - Use blog template from `references/content_frameworks.md` + - Include keyword in title, first paragraph, and 2-3 H2s + - Aim for 1,500-2,500 words for comprehensive coverage + +3. **Optimization Check** + ```bash + python scripts/seo_optimizer.py blog_post.md "primary keyword" "secondary,keywords,list" + ``` + +4. **Apply SEO Recommendations** + - Adjust keyword density to 1-3% + - Ensure proper heading structure + - Add internal and external links + - Optimize meta description + +### Social Media Content Creation + +1. **Platform Selection** + - Identify primary platforms based on audience + - Review platform-specific guidelines in `references/social_media_optimization.md` + +2. **Content Adaptation** + - Start with blog post or core message + - Use repurposing matrix from `references/content_frameworks.md` + - Adapt for each platform following templates + +3. **Optimization Checklist** + - Platform-appropriate length + - Optimal posting time + - Correct image dimensions + - Platform-specific hashtags + - Engagement elements (polls, questions) + +### Content Calendar Planning + +1. **Monthly Planning** + - Copy `assets/content_calendar_template.md` + - Set monthly goals and KPIs + - Identify key campaigns/themes + +2. **Weekly Distribution** + - Follow 40/25/25/10 content pillar ratio + - Balance platforms throughout week + - Align with optimal posting times + +3. **Batch Creation** + - Create all weekly content in one session + - Maintain consistent voice across pieces + - Prepare all visual assets together + +## Key Scripts + +### brand_voice_analyzer.py +Analyzes text content for voice characteristics, readability, and consistency. + +**Usage**: `python scripts/brand_voice_analyzer.py [json|text]` + +**Returns**: +- Voice profile (formality, tone, perspective) +- Readability score +- Sentence structure analysis +- Improvement recommendations + +### seo_optimizer.py +Analyzes content for SEO optimization and provides actionable recommendations. + +**Usage**: `python scripts/seo_optimizer.py [primary_keyword] [secondary_keywords]` + +**Returns**: +- SEO score (0-100) +- Keyword density analysis +- Structure assessment +- Meta tag suggestions +- Specific optimization recommendations + +## Reference Guides + +### When to Use Each Reference + +**references/brand_guidelines.md** +- Setting up new brand voice +- Ensuring consistency across content +- Training new team members +- Resolving voice/tone questions + +**references/content_frameworks.md** +- Starting any new content piece +- Structuring different content types +- Creating content templates +- Planning content repurposing + +**references/social_media_optimization.md** +- Platform-specific optimization +- Hashtag strategy development +- Understanding algorithm factors +- Setting up analytics tracking + +## Best Practices + +### Content Creation Process +1. Always start with audience need/pain point +2. Research before writing +3. Create outline using templates +4. Write first draft without editing +5. Optimize for SEO +6. Edit for brand voice +7. Proofread and fact-check +8. Optimize for platform +9. Schedule strategically + +### Quality Indicators +- SEO score above 75/100 +- Readability appropriate for audience +- Consistent brand voice throughout +- Clear value proposition +- Actionable takeaways +- Proper visual formatting +- Platform-optimized + +### Common Pitfalls to Avoid +- Writing before researching keywords +- Ignoring platform-specific requirements +- Inconsistent brand voice +- Over-optimizing for SEO (keyword stuffing) +- Missing clear CTAs +- Publishing without proofreading +- Ignoring analytics feedback + +## Performance Metrics + +Track these KPIs for content success: + +### Content Metrics +- Organic traffic growth +- Average time on page +- Bounce rate +- Social shares +- Backlinks earned + +### Engagement Metrics +- Comments and discussions +- Email click-through rates +- Social media engagement rate +- Content downloads +- Form submissions + +### Business Metrics +- Leads generated +- Conversion rate +- Customer acquisition cost +- Revenue attribution +- ROI per content piece + +## Integration Points + +This skill works best with: +- Analytics platforms (Google Analytics, social media insights) +- SEO tools (for keyword research) +- Design tools (for visual content) +- Scheduling platforms (for content distribution) +- Email marketing systems (for newsletter content) + +## Quick Commands + +```bash +# Analyze brand voice +python scripts/brand_voice_analyzer.py content.txt + +# Optimize for SEO +python scripts/seo_optimizer.py article.md "main keyword" + +# Check content against brand guidelines +grep -f references/brand_guidelines.md content.txt + +# Create monthly calendar +cp assets/content_calendar_template.md this_month_calendar.md +``` diff --git a/plugins/antigravity-bundle-creative-director/skills/content-creator/assets/content_calendar_template.md b/plugins/antigravity-bundle-creative-director/skills/content-creator/assets/content_calendar_template.md new file mode 100644 index 00000000..725f6b14 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/content-creator/assets/content_calendar_template.md @@ -0,0 +1,99 @@ +# Content Calendar Template - [Month Year] + +## Monthly Goals +- **Traffic Goal**: +- **Lead Generation Goal**: +- **Engagement Goal**: +- **Key Campaign**: + +## Week 1: [Date Range] + +### Monday [Date] +**Platform**: Blog +**Topic**: +**Keywords**: +**Status**: [ ] Planned [ ] Written [ ] Reviewed [ ] Published +**Owner**: +**Notes**: + +**Platform**: LinkedIn +**Type**: Article Share +**Caption**: +**Hashtags**: +**Time**: 10:00 AM + +### Tuesday [Date] +**Platform**: Instagram +**Type**: Carousel +**Topic**: +**Visuals**: [ ] Created [ ] Approved +**Caption**: +**Hashtags**: +**Time**: 12:00 PM + +### Wednesday [Date] +**Platform**: Email Newsletter +**Subject Line**: +**Segment**: +**CTA**: +**Status**: [ ] Drafted [ ] Designed [ ] Scheduled + +### Thursday [Date] +**Platform**: Twitter/X +**Type**: Thread +**Topic**: +**Thread Length**: +**Media**: [ ] Images [ ] GIFs [ ] None +**Time**: 2:00 PM + +### Friday [Date] +**Platform**: Multi-channel +**Campaign**: +**Assets Needed**: +- [ ] Blog post +- [ ] Social graphics +- [ ] Email +- [ ] Video + +## Week 2: [Date Range] +[Repeat structure] + +## Week 3: [Date Range] +[Repeat structure] + +## Week 4: [Date Range] +[Repeat structure] + +## Content Bank (Ideas for Future) +1. +2. +3. +4. +5. + +## Performance Review (End of Month) + +### Top Performing Content +1. **Title/Topic**: + - **Metric**: + - **Why it worked**: + +2. **Title/Topic**: + - **Metric**: + - **Why it worked**: + +### Lessons Learned +- +- +- + +### Adjustments for Next Month +- +- +- + +## Resource Links +- Brand Guidelines: [Link] +- Asset Library: [Link] +- Analytics Dashboard: [Link] +- Team Calendar: [Link] diff --git a/plugins/antigravity-bundle-creative-director/skills/content-creator/references/brand_guidelines.md b/plugins/antigravity-bundle-creative-director/skills/content-creator/references/brand_guidelines.md new file mode 100644 index 00000000..90b3124f --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/content-creator/references/brand_guidelines.md @@ -0,0 +1,199 @@ +# Brand Voice & Style Guidelines + +## Brand Voice Framework + +### 1. Voice Dimensions + +#### Formality Spectrum +- **Formal**: Legal documents, investor communications, crisis responses +- **Professional**: B2B content, whitepapers, case studies +- **Conversational**: Blog posts, social media, email newsletters +- **Casual**: Community engagement, behind-the-scenes content + +#### Tone Attributes +Choose 3-5 primary attributes for your brand: +- **Authoritative**: Position as industry expert +- **Friendly**: Approachable and warm +- **Innovative**: Forward-thinking and creative +- **Trustworthy**: Reliable and transparent +- **Inspiring**: Motivational and uplifting +- **Educational**: Informative and helpful +- **Witty**: Clever and entertaining (use sparingly) + +#### Perspective +- **First Person Plural (We/Our)**: Creates partnership feeling +- **Second Person (You/Your)**: Direct and engaging +- **Third Person**: Objective and professional + +### 2. Brand Personality Archetypes + +Choose one primary and one secondary archetype: + +**The Expert** +- Tone: Knowledgeable, confident, informative +- Content: Data-driven, research-backed, educational +- Example: "Our research shows that 87% of businesses..." + +**The Friend** +- Tone: Warm, supportive, conversational +- Content: Relatable, helpful, encouraging +- Example: "We get it - marketing can be overwhelming..." + +**The Innovator** +- Tone: Visionary, bold, forward-thinking +- Content: Cutting-edge, disruptive, trendsetting +- Example: "The future of marketing is here..." + +**The Guide** +- Tone: Wise, patient, instructive +- Content: Step-by-step, clear, actionable +- Example: "Let's walk through this together..." + +**The Motivator** +- Tone: Energetic, positive, inspiring +- Content: Empowering, action-oriented, transformative +- Example: "You have the power to transform your business..." + +### 3. Writing Principles + +#### Clarity First +- Use simple words when possible +- Break complex ideas into digestible pieces +- Lead with the main point +- Use active voice (80% of the time) + +#### Customer-Centric +- Focus on benefits, not features +- Address pain points directly +- Use "you" more than "we" +- Include customer success stories + +#### Consistency +- Maintain voice across all channels +- Use approved terminology +- Follow formatting standards +- Apply style rules uniformly + +### 4. Language Guidelines + +#### Words We Use +- **Action verbs**: Transform, accelerate, optimize, unlock, elevate +- **Positive descriptors**: Seamless, powerful, intuitive, strategic +- **Outcome-focused**: Results, growth, success, impact, ROI + +#### Words We Avoid +- **Jargon**: Synergy, leverage (as verb), bandwidth (for availability) +- **Overused**: Innovative, disruptive, cutting-edge (unless truly applicable) +- **Weak**: Very, really, just, maybe, hopefully +- **Negative**: Can't, won't, impossible, problem (use "challenge") + +### 5. Content Structure Templates + +#### Blog Post Structure +1. **Hook** (1-2 sentences): Grab attention with a question, statistic, or bold statement +2. **Context** (1 paragraph): Explain why this matters now +3. **Main Content** (3-5 sections): Deliver value with clear subheadings +4. **Conclusion** (1 paragraph): Summarize key points +5. **Call to Action**: Clear next step for readers + +#### Social Media Framework +- **LinkedIn**: Professional insights, industry news, thought leadership +- **Twitter/X**: Quick tips, engaging questions, thread stories +- **Instagram**: Visual storytelling, behind-the-scenes, inspiration +- **Facebook**: Community building, longer narratives, events + +### 6. Messaging Pillars + +Define 3-4 core themes that appear consistently: + +1. **Innovation & Technology** + - AI-powered solutions + - Data-driven insights + - Future-ready strategies + +2. **Customer Success** + - Real results and ROI + - Partnership approach + - Tailored solutions + +3. **Expertise & Trust** + - Industry leadership + - Proven methodologies + - Transparent communication + +4. **Growth & Transformation** + - Scaling businesses + - Digital transformation + - Continuous improvement + +### 7. Audience Personas + +#### Decision Makers (C-Suite) +- **Tone**: Professional, strategic, ROI-focused +- **Content**: High-level insights, business impact, competitive advantages +- **Pain Points**: Growth, efficiency, competition + +#### Practitioners (Marketing Managers) +- **Tone**: Practical, supportive, educational +- **Content**: How-to guides, best practices, tools +- **Pain Points**: Time, resources, skills + +#### Innovators (Early Adopters) +- **Tone**: Exciting, cutting-edge, visionary +- **Content**: Trends, new features, future predictions +- **Pain Points**: Staying ahead, differentiation + +### 8. Channel-Specific Guidelines + +#### Website Copy +- Headlines: 6-12 words, benefit-focused +- Body: Short paragraphs (2-3 sentences) +- CTAs: Action-oriented, specific + +#### Email Marketing +- Subject Lines: 30-50 characters, personalized +- Preview Text: Complement subject, add urgency +- Body: Scannable, one main message + +#### Blog Content +- Title: Include primary keyword, under 60 characters +- Introduction: Hook within first 50 words +- Sections: 200-300 words each +- Lists: 5-7 items optimal + +### 9. Grammar & Mechanics + +#### Punctuation +- Oxford comma: Always use +- Em dashes: For emphasisโ€”like this +- Exclamation points: Maximum one per piece + +#### Capitalization +- Headlines: Title Case for H1, Sentence case for H2-H6 +- Product names: As trademarked +- Job titles: Lowercase unless before name + +#### Numbers +- Spell out one through nine +- Use numerals for 10 and above +- Always use numerals for percentages + +### 10. Inclusivity Guidelines + +- Use gender-neutral language +- Avoid idioms that don't translate +- Consider global audience +- Ensure accessibility in formatting +- Represent diverse perspectives + +## Quick Reference Checklist + +Before publishing any content, verify: +- [ ] Matches brand voice and tone +- [ ] Free of jargon and complex terms +- [ ] Includes clear value proposition +- [ ] Has appropriate CTA +- [ ] Follows grammar guidelines +- [ ] Mobile-friendly formatting +- [ ] Accessible to all audiences +- [ ] Proofread and fact-checked diff --git a/plugins/antigravity-bundle-creative-director/skills/content-creator/references/content_frameworks.md b/plugins/antigravity-bundle-creative-director/skills/content-creator/references/content_frameworks.md new file mode 100644 index 00000000..8ecdc066 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/content-creator/references/content_frameworks.md @@ -0,0 +1,534 @@ +# Content Creation Frameworks & Templates + +## Content Types & Templates + +### 1. Blog Post Templates + +#### How-To Guide Template +```markdown +# How to [Achieve Desired Outcome] in [Timeframe] + +## Introduction +- Hook: Question or surprising fact +- Problem statement +- What reader will learn +- Why it matters now + +## Prerequisites/What You'll Need +- Tool/Resource 1 +- Tool/Resource 2 +- Estimated time + +## Step 1: [Action] +- Clear instruction +- Why this step matters +- Common mistakes to avoid +- Visual aid or example + +## Step 2: [Action] +[Repeat structure] + +## Step 3: [Action] +[Repeat structure] + +## Troubleshooting Common Issues +### Issue 1: [Problem] +**Solution**: [Fix] + +### Issue 2: [Problem] +**Solution**: [Fix] + +## Results You Can Expect +- Immediate outcomes +- Long-term benefits +- Success metrics + +## Next Steps +- Advanced techniques +- Related guides +- CTA for product/service + +## Conclusion +- Recap key points +- Reinforce value +- Final encouragement +``` + +#### Listicle Template +```markdown +# [Number] [Adjective] Ways to [Achieve Goal] in [Year] + +## Introduction +- Context/trend driving this topic +- Promise of what reader gains +- Credibility statement + +## 1. [First Item - Most Important] +**Why it matters**: [Brief explanation] +**How to implement**: [2-3 actionable steps] +**Pro tip**: [Expert insight] +**Example**: [Real-world application] + +## 2. [Second Item] +[Repeat structure] + +[Continue for all items] + +## Bonus Tip: [Overdelivery] +[Something extra valuable] + +## Bringing It All Together +- How items work synergistically +- Priority order for implementation +- Expected timeline for results + +## Your Action Plan +1. Start with [easiest item] +2. Progress to [next steps] +3. Measure [metrics] + +## Conclusion & CTA +``` + +#### Case Study Template +```markdown +# How [Company] Achieved [Result] Using [Solution] + +## Executive Summary +- Company overview +- Challenge faced +- Solution implemented +- Key results (3 metrics) + +## The Challenge +### Background +- Industry context +- Company situation +- Previous attempts + +### Specific Pain Points +- Pain point 1 +- Pain point 2 +- Pain point 3 + +## The Solution +### Strategy Development +- Discovery process +- Strategic approach +- Why this solution + +### Implementation +- Phase 1: [Timeline & Actions] +- Phase 2: [Timeline & Actions] +- Phase 3: [Timeline & Actions] + +## The Results +### Quantitative Outcomes +- Metric 1: X% increase +- Metric 2: $Y saved +- Metric 3: Z improvement + +### Qualitative Benefits +- Team feedback +- Customer response +- Market position + +## Key Takeaways +1. Lesson learned +2. Best practice discovered +3. Unexpected benefit + +## How You Can Achieve Similar Results +- Prerequisite conditions +- Implementation roadmap +- Success factors + +## CTA: Start Your Success Story +``` + +#### Thought Leadership Template +```markdown +# [Provocative Statement About Industry Future] + +## The Current State +- Industry snapshot +- Prevailing wisdom +- Why status quo is insufficient + +## The Emerging Trend +### What's Changing +- Driver 1: [Technology/Market/Behavior] +- Driver 2: [Technology/Market/Behavior] +- Driver 3: [Technology/Market/Behavior] + +### Evidence & Examples +- Data point 1 +- Case example +- Expert validation + +## Implications for [Industry] +### Short-term (6-12 months) +- Immediate adjustments needed +- Quick wins available +- Risks of inaction + +### Long-term (2-5 years) +- Fundamental shifts +- New opportunities +- Competitive landscape + +## Strategic Recommendations +### For Leaders +- Strategic priorities +- Investment areas +- Organizational changes + +### For Practitioners +- Skill development +- Process adaptation +- Tool adoption + +## The Path Forward +- Call for industry action +- Your organization's role +- Next steps for readers + +## Join the Conversation +- Thought-provoking question +- Invitation to share perspectives +- CTA for deeper engagement +``` + +### 2. Social Media Templates + +#### LinkedIn Post Framework +``` +๐ŸŽฏ Hook/Pattern Interrupt + +Context paragraph explaining the situation or challenge. + +Key insight or lesson learned: + +โ€ข Bullet point 1 (specific detail) +โ€ข Bullet point 2 (measurable outcome) +โ€ข Bullet point 3 (unexpected discovery) + +Brief story or example that illustrates the point. + +Takeaway message with clear value. + +Question to encourage engagement? + +#Hashtag1 #Hashtag2 #Hashtag3 +``` + +#### Twitter/X Thread Template +``` +1/ Bold opening statement or question that stops the scroll + +2/ Context - why this matters right now + +3/ Problem most people face + +4/ Conventional solution (and why it falls short) + +5/ Better approach - introduction + +6/ Step 1 of better approach + โ€ข Specific action + โ€ข Why it works + +7/ Step 2 of better approach + [Continue pattern] + +8/ Real example or case study + +9/ Common objection addressed + +10/ Results you can expect + +11/ One powerful tip most people miss + +12/ Recap in 3 key points: + - Point 1 + - Point 2 + - Point 3 + +13/ CTA: If you found this helpful, [action] + +14/ P.S. - Bonus insight or resource +``` + +#### Instagram Caption Template +``` +[Attention-grabbing first line - appears in preview] + +[Story or relatable scenario - 2-3 sentences] + +Here's what I learned: + +[Key insight or lesson] + +3 things that changed everything: +1๏ธโƒฃ [First point] +2๏ธโƒฃ [Second point] +3๏ธโƒฃ [Third point] + +[Call-out or question to audience] + +Drop a [emoji] if you've experienced this too! + +What's your biggest challenge with [topic]? Let me know below ๐Ÿ‘‡ + +- +#hashtag1 #hashtag2 #hashtag3 #hashtag4 #hashtag5 +[10-30 relevant hashtags total] +``` + +### 3. Email Marketing Templates + +#### Newsletter Template +``` +Subject: [Benefit] + [Urgency/Curiosity] +Preview: [Complements subject, doesn't repeat] + +Hi [Name], + +[Personal observation or timely hook - 1-2 sentences] + +[Transition to main topic - why reading this matters] + +## Main Content Section + +[Key points in scannable format] +โ€ข Point 1: [Benefit-focused] +โ€ข Point 2: [Specific example] +โ€ข Point 3: [Actionable tip] + +[Brief elaboration on most important point - 2-3 sentences] + +## Resource of the Week + +[Title with link] +[One sentence on why it's valuable] + +## Quick Win You Can Implement Today + +[Specific, actionable tip - 2-3 steps max] + +[Closing thought or question] + +[Signature] +[Name] + +P.S. [Additional value or soft CTA] +``` + +#### Promotional Email Template +``` +Subject: [Specific benefit] by [deadline/timeframe] +Preview: [Scarcity or exclusivity element] + +Hi [Name], + +[Acknowledge pain point or aspiration] + +[Agitate - why this problem persists] + +I've got something that can help: + +[Solution introduction - what it is] + +Here's what you get: +โœ“ Benefit 1 (not feature) +โœ“ Benefit 2 (not feature) +โœ“ Benefit 3 (not feature) + +[Social proof - testimonial or results] + +[Handle main objection] + +[Clear CTA button: "Get Started" / "Claim Yours"] + +[Urgency element - deadline or limited availability] + +[Signature] + +P.S. [Reinforce urgency or add bonus] +``` + +### 4. Content Planning Frameworks + +#### Content Pillar Strategy +``` +Pillar 1: Educational (40%) +- How-to guides +- Tutorials +- Best practices +- Tips & tricks + +Pillar 2: Inspirational (25%) +- Success stories +- Case studies +- Transformations +- Vision pieces + +Pillar 3: Conversational (25%) +- Behind-the-scenes +- Team spotlights +- Q&As +- Polls/questions + +Pillar 4: Promotional (10%) +- Product updates +- Offers +- Event announcements +- CTAs +``` + +#### Monthly Content Calendar Structure +``` +Week 1: +- Monday: Educational (blog post) +- Wednesday: Inspirational (social) +- Friday: Conversational (email) + +Week 2: +- Monday: Educational (video/guide) +- Wednesday: Case study +- Friday: Curated content + +Week 3: +- Monday: Educational (infographic) +- Wednesday: Behind-the-scenes +- Friday: Community spotlight + +Week 4: +- Monday: Monthly roundup +- Wednesday: Thought leadership +- Friday: Promotional +``` + +### 5. SEO Content Framework + +#### SEO-Optimized Article Structure +``` +URL: /primary-keyword-secondary-keyword + +Title Tag: Primary Keyword - Secondary Benefit | Brand +Meta Description: Action verb + primary keyword + benefit + CTA (155 chars) + +# H1: Primary Keyword + Unique Angle + +Introduction (50-100 words) +- Include primary keyword in first 100 words +- State what reader will learn +- Why it matters + +## H2: Secondary Keyword Variation 1 + +[Content with LSI keywords naturally integrated] + +### H3: Specific subtopic +- Detail point 1 +- Detail point 2 +- Detail point 3 + +## H2: Secondary Keyword Variation 2 + +[Content continues...] + +## H2: Related Questions (FAQ Schema) + +### Question 1? +[Concise answer with keyword] + +### Question 2? +[Concise answer with keyword] + +## Conclusion +- Recap main points +- Include primary keyword +- Clear next action + +Internal Links: 2-3 relevant articles +External Links: 1-2 authoritative sources +``` + +### 6. Video Script Templates + +#### Educational Video Script +``` +[0-5 seconds: Hook] +"What if I told you [surprising statement]?" + +[5-15 seconds: Introduction] +"Hi, I'm [Name] and today we're solving [problem]" + +[15-30 seconds: Context] +- Why this matters +- What you'll learn +- What you'll achieve + +[30 seconds - 2 minutes: Main Content] +Section 1: [Key Point] +- Explanation +- Example +- Visual aid + +Section 2: [Key Point] +[Repeat structure] + +Section 3: [Key Point] +[Repeat structure] + +[Final 15-30 seconds] +- Quick recap +- Call to action +- End screen elements +``` + +### 7. Content Repurposing Matrix + +``` +Original: Blog Post (2000 words) +โ”œโ”€โ”€ Social Media +โ”‚ โ”œโ”€โ”€ 5 Twitter posts (key quotes) +โ”‚ โ”œโ”€โ”€ 1 LinkedIn article (executive summary) +โ”‚ โ”œโ”€โ”€ 3 Instagram carousels (main points) +โ”‚ โ””โ”€โ”€ 1 Facebook post (intro + link) +โ”œโ”€โ”€ Email +โ”‚ โ””โ”€โ”€ Newsletter feature (summary + CTA) +โ”œโ”€โ”€ Video +โ”‚ โ”œโ”€โ”€ YouTube explainer (script from post) +โ”‚ โ””โ”€โ”€ TikTok/Reels (quick tips) +โ”œโ”€โ”€ Audio +โ”‚ โ””โ”€โ”€ Podcast talking points +โ””โ”€โ”€ Visual + โ”œโ”€โ”€ Infographic (data points) + โ””โ”€โ”€ Slide deck (presentation) +``` + +## Quick-Start Checklists + +### Pre-Publishing Checklist +- [ ] Keyword research completed +- [ ] Title under 60 characters +- [ ] Meta description written (155 chars) +- [ ] Headers properly structured (H1, H2, H3) +- [ ] Internal links added (2-3) +- [ ] Images optimized with alt text +- [ ] CTA included and clear +- [ ] Proofread and fact-checked +- [ ] Mobile preview checked + +### Content Quality Checklist +- [ ] Addresses specific audience need +- [ ] Provides unique value/perspective +- [ ] Includes actionable takeaways +- [ ] Uses appropriate brand voice +- [ ] Contains supporting data/examples +- [ ] Free of jargon and complex terms +- [ ] Scannable format (bullets, headers) +- [ ] Engaging hook in introduction +- [ ] Clear conclusion and next steps diff --git a/plugins/antigravity-bundle-creative-director/skills/content-creator/references/social_media_optimization.md b/plugins/antigravity-bundle-creative-director/skills/content-creator/references/social_media_optimization.md new file mode 100644 index 00000000..d93766a2 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/content-creator/references/social_media_optimization.md @@ -0,0 +1,317 @@ +# Social Media Optimization Guide + +## Platform-Specific Best Practices + +### LinkedIn +**Audience**: B2B professionals, decision-makers, thought leaders +**Best Times**: Tuesday-Thursday, 8-10 AM and 5-6 PM +**Optimal Length**: 1,300-2,000 characters for posts + +#### Content Formats +- **Text Posts**: 1,300 characters optimal, use line breaks +- **Articles**: 1,900-2,000 words, include 5+ images +- **Videos**: 30 seconds - 10 minutes, native upload preferred +- **Documents**: PDF carousels, 10-15 slides +- **Polls**: 4 options max, 1-2 week duration + +#### Optimization Tips +- First 2 lines are crucial (shown in preview) +- Use emoji sparingly for visual breaks +- Include 3-5 relevant hashtags +- Tag people and companies when relevant +- Native video gets 5x more engagement +- Post consistently (3-5x per week optimal) + +#### Algorithm Factors +- Dwell time (time spent reading) +- Comments valued over likes +- Early engagement (first hour) crucial +- Creator mode boosts reach +- Replies to comments increase visibility + +### Twitter/X +**Audience**: News junkies, tech enthusiasts, real-time conversation +**Best Times**: Weekdays 9-10 AM and 7-9 PM +**Optimal Length**: 100-250 characters + +#### Content Formats +- **Single Tweets**: 250 characters, 1-2 hashtags +- **Threads**: 5-15 tweets, numbered format +- **Images**: 16:9 ratio, up to 4 per tweet +- **Videos**: Up to 2:20, square or landscape +- **Polls**: 2-4 options, 5 minutes - 7 days + +#### Optimization Tips +- Front-load important information +- Use threads for complex topics +- Include visuals (2-3x more engagement) +- Retweet with comment > regular RT +- Schedule threads for consistency +- Engage genuinely with replies + +#### Algorithm Factors +- Engagement rate (likes, RTs, replies) +- Relationship (mutual follows prioritized) +- Recency over evergreen +- Topic relevance to user interests +- Link posts receive less reach + +### Instagram +**Audience**: Visual-first, millennials & Gen Z, lifestyle focused +**Best Times**: Weekdays 11 AM - 1 PM and 7-9 PM +**Optimal Length**: 138-150 characters shown in preview + +#### Content Formats +- **Feed Posts**: Square (1:1) or vertical (4:5) +- **Stories**: 15 seconds max, vertical (9:16) +- **Reels**: 15-90 seconds, vertical (9:16) +- **Carousels**: 2-10 images/videos +- **IGTV/Video**: 1-60 minutes + +#### Optimization Tips +- First sentence crucial (caption preview) +- Use up to 30 hashtags (5-10 in caption, rest in comment) +- Carousel posts get highest engagement +- Stories with polls/questions boost views +- Reels get maximum organic reach +- Post consistently (1-2 feed posts daily) + +#### Algorithm Factors +- Relationship (DMs, comments, tags) +- Interest (based on past interactions) +- Timeliness (newer posts prioritized) +- Frequency of app usage +- Time spent on posts (saves valuable) + +### Facebook +**Audience**: Broad demographic, community-focused, local businesses +**Best Times**: Wednesday-Friday, 11 AM - 2 PM +**Optimal Length**: 50-80 characters for posts + +#### Content Formats +- **Text Posts**: 50-80 characters optimal +- **Images**: 1200x630px for links +- **Videos**: 1-3 minutes, square format +- **Stories**: Same as Instagram +- **Live Videos**: Minimum 10 minutes + +#### Optimization Tips +- Native video gets priority +- Ask questions to boost comments +- Share to relevant groups +- Use Facebook Creator Studio +- Tag locations for local reach +- Post 1-2 times per day max + +#### Algorithm Factors +- Meaningful interactions (comments > reactions) +- Video completion rate +- Friends and family prioritized +- Group posts get high visibility +- Live videos get 6x engagement + +### TikTok +**Audience**: Gen Z, entertainment-focused, trend-driven +**Best Times**: 6-10 AM and 7-11 PM +**Optimal Length**: 15-30 seconds + +#### Content Formats +- **Videos**: 15 seconds - 10 minutes +- **Aspect Ratio**: 9:16 vertical +- **Sounds**: Trending audio crucial +- **Effects**: Filters and transitions + +#### Optimization Tips +- Hook viewers in first 3 seconds +- Use trending sounds and hashtags +- Create content for FYP, not followers +- Post 1-4 times daily +- Engage with comments quickly +- Jump on trends within 24-48 hours + +#### Algorithm Factors +- Completion rate most important +- Shares and saves valued +- Comment engagement +- Following similar creators +- Time spent on app + +## Content Optimization Strategies + +### Hashtag Strategy + +#### Research Methods +1. **Competitor Analysis**: Study successful competitors +2. **Platform Search**: Use native search for suggestions +3. **Hashtag Tools**: RiteTag, Hashtagify, All Hashtag +4. **Trending Topics**: Monitor daily/weekly trends +5. **Brand Hashtags**: Create unique campaign tags + +#### Hashtag Mix Formula +- 30% High-volume (1M+ posts) +- 40% Medium-volume (100K-1M posts) +- 30% Low-volume/Niche (<100K posts) + +#### Platform-Specific Guidelines +- **Instagram**: 10-30 hashtags (mix in caption and first comment) +- **LinkedIn**: 3-5 professional hashtags +- **Twitter**: 1-2 hashtags max +- **Facebook**: 1-3 hashtags +- **TikTok**: 3-5 trending + niche tags + +### Visual Content Optimization + +#### Image Best Practices +- **Resolution**: Minimum 1080px width +- **File Size**: Under 5MB for faster loading +- **Alt Text**: Always include for accessibility +- **Branding**: Consistent filters/overlays +- **Text Overlay**: Less than 20% of image + +#### Video Optimization +- **Captions**: Always include (85% watch without sound) +- **Thumbnail**: Custom, eye-catching +- **Length**: Platform-specific optimal duration +- **Format**: MP4 for best compatibility +- **Aspect Ratio**: Vertical for stories/reels, square for feed + +### Caption Writing Formulas + +#### AIDA Formula +- **Attention**: Hook in first line +- **Interest**: Expand on the hook +- **Desire**: Benefits and value +- **Action**: Clear CTA + +#### PAS Formula +- **Problem**: Identify pain point +- **Agitate**: Emphasize consequences +- **Solution**: Present your answer + +#### Before-After-Bridge +- **Before**: Current situation +- **After**: Desired outcome +- **Bridge**: How to get there + +### Engagement Tactics + +#### Conversation Starters +- Ask open-ended questions +- Create polls and surveys +- "Fill in the blank" posts +- "This or that" choices +- Caption contests +- Opinion requests + +#### Community Building +- Respond to comments within 2 hours +- Like and reply to user comments +- Share user-generated content +- Create branded hashtags +- Host Q&A sessions +- Run challenges or contests + +### Analytics & KPIs + +#### Vanity Metrics (Track but don't obsess) +- Follower count +- Like count +- View count + +#### Performance Metrics (Focus here) +- Engagement rate: (Likes + Comments + Shares) / Reach ร— 100 +- Click-through rate: Clicks / Impressions ร— 100 +- Conversion rate: Conversions / Clicks ร— 100 +- Share/Save rate: Shares / Reach ร— 100 + +#### Business Metrics (Ultimate goal) +- Website traffic from social +- Lead generation +- Sales attribution +- Customer acquisition cost +- Customer lifetime value + +### Content Calendar Planning + +#### Weekly Posting Schedule Template +``` +Monday: Motivational (Quote/Inspiration) +Tuesday: Educational (How-to/Tips) +Wednesday: Promotional (Product/Service) +Thursday: Engaging (Poll/Question) +Friday: Fun (Behind-scenes/Casual) +Saturday: User-Generated Content +Sunday: Curated Content/Rest +``` + +#### Monthly Theme Structure +- Week 1: Awareness content +- Week 2: Consideration content +- Week 3: Decision content +- Week 4: Retention/Community + +### Crisis Management Protocol + +#### Response Timeline +- **0-15 minutes**: Acknowledge awareness +- **15-60 minutes**: Gather facts +- **1-2 hours**: Official response +- **24 hours**: Follow-up update +- **48-72 hours**: Resolution summary + +#### Response Guidelines +1. Acknowledge quickly +2. Take responsibility if appropriate +3. Show empathy +4. Provide facts only +5. Outline action steps +6. Follow up publicly + +## Tool Stack Recommendations + +### Content Creation +- **Design**: Canva, Adobe Creative Suite +- **Video**: CapCut, InShot, Adobe Premiere +- **Copy**: Grammarly, Hemingway Editor +- **AI Assistance**: ChatGPT, Claude, Jasper + +### Scheduling & Management +- **All-in-One**: Hootsuite, Buffer, Sprout Social +- **Visual-First**: Later, Planoly +- **Enterprise**: Sprinklr, Khoros +- **Free Options**: Meta Business Suite, TweetDeck + +### Analytics & Monitoring +- **Native**: Platform Insights/Analytics +- **Third-Party**: Socialbakers, Brandwatch +- **Listening**: Mention, Brand24 +- **Competitor Analysis**: Social Blade, Rival IQ + +### Influencer & UGC +- **Discovery**: AspireIQ, GRIN +- **Management**: CreatorIQ, Klear +- **UGC Curation**: TINT, Stackla +- **Rights Management**: Rights Manager + +## Compliance & Best Practices + +### Legal Considerations +- Include #ad or #sponsored for paid partnerships +- Respect copyright and attribution +- Follow GDPR for data collection +- Comply with platform terms of service +- Get permission for UGC usage + +### Accessibility Guidelines +- Add alt text to all images +- Include captions on videos +- Use CamelCase for hashtags (#LikeThis) +- Avoid text-only images +- Ensure color contrast compliance + +### Brand Safety +- Moderate comments regularly +- Set up keyword filters +- Have crisis management plan +- Monitor brand mentions +- Establish posting permissions diff --git a/plugins/antigravity-bundle-creative-director/skills/content-creator/scripts/brand_voice_analyzer.py b/plugins/antigravity-bundle-creative-director/skills/content-creator/scripts/brand_voice_analyzer.py new file mode 100644 index 00000000..92ab6f70 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/content-creator/scripts/brand_voice_analyzer.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +""" +Brand Voice Analyzer - Analyzes content to establish and maintain brand voice consistency +""" + +import re +from typing import Dict, List, Tuple +import json + +class BrandVoiceAnalyzer: + def __init__(self): + self.voice_dimensions = { + 'formality': { + 'formal': ['hereby', 'therefore', 'furthermore', 'pursuant', 'regarding'], + 'casual': ['hey', 'cool', 'awesome', 'stuff', 'yeah', 'gonna'] + }, + 'tone': { + 'professional': ['expertise', 'solution', 'optimize', 'leverage', 'strategic'], + 'friendly': ['happy', 'excited', 'love', 'enjoy', 'together', 'share'] + }, + 'perspective': { + 'authoritative': ['proven', 'research shows', 'experts agree', 'data indicates'], + 'conversational': ['you might', 'let\'s explore', 'we think', 'imagine if'] + } + } + + def analyze_text(self, text: str) -> Dict: + """Analyze text for brand voice characteristics""" + text_lower = text.lower() + word_count = len(text.split()) + + results = { + 'word_count': word_count, + 'readability_score': self._calculate_readability(text), + 'voice_profile': {}, + 'sentence_analysis': self._analyze_sentences(text), + 'recommendations': [] + } + + # Analyze voice dimensions + for dimension, categories in self.voice_dimensions.items(): + dim_scores = {} + for category, keywords in categories.items(): + score = sum(1 for keyword in keywords if keyword in text_lower) + dim_scores[category] = score + + # Determine dominant voice + if sum(dim_scores.values()) > 0: + dominant = max(dim_scores, key=dim_scores.get) + results['voice_profile'][dimension] = { + 'dominant': dominant, + 'scores': dim_scores + } + + # Generate recommendations + results['recommendations'] = self._generate_recommendations(results) + + return results + + def _calculate_readability(self, text: str) -> float: + """Calculate Flesch Reading Ease score""" + sentences = re.split(r'[.!?]+', text) + words = text.split() + syllables = sum(self._count_syllables(word) for word in words) + + if len(sentences) == 0 or len(words) == 0: + return 0 + + avg_sentence_length = len(words) / len(sentences) + avg_syllables_per_word = syllables / len(words) + + # Flesch Reading Ease formula + score = 206.835 - 1.015 * avg_sentence_length - 84.6 * avg_syllables_per_word + return max(0, min(100, score)) + + def _count_syllables(self, word: str) -> int: + """Count syllables in a word (simplified)""" + word = word.lower() + vowels = 'aeiou' + syllable_count = 0 + previous_was_vowel = False + + for char in word: + is_vowel = char in vowels + if is_vowel and not previous_was_vowel: + syllable_count += 1 + previous_was_vowel = is_vowel + + # Adjust for silent e + if word.endswith('e'): + syllable_count -= 1 + + return max(1, syllable_count) + + def _analyze_sentences(self, text: str) -> Dict: + """Analyze sentence structure""" + sentences = re.split(r'[.!?]+', text) + sentences = [s.strip() for s in sentences if s.strip()] + + if not sentences: + return {'average_length': 0, 'variety': 'low'} + + lengths = [len(s.split()) for s in sentences] + avg_length = sum(lengths) / len(lengths) if lengths else 0 + + # Calculate variety + if len(set(lengths)) < 3: + variety = 'low' + elif len(set(lengths)) < 5: + variety = 'medium' + else: + variety = 'high' + + return { + 'average_length': round(avg_length, 1), + 'variety': variety, + 'count': len(sentences) + } + + def _generate_recommendations(self, analysis: Dict) -> List[str]: + """Generate recommendations based on analysis""" + recommendations = [] + + # Readability recommendations + if analysis['readability_score'] < 30: + recommendations.append("Consider simplifying language for better readability") + elif analysis['readability_score'] > 70: + recommendations.append("Content is very easy to read - consider if this matches your audience") + + # Sentence variety + if analysis['sentence_analysis']['variety'] == 'low': + recommendations.append("Vary sentence length for better flow and engagement") + + # Voice consistency + if analysis['voice_profile']: + recommendations.append("Maintain consistent voice across all content") + + return recommendations + +def analyze_content(content: str, output_format: str = 'json') -> str: + """Main function to analyze content""" + analyzer = BrandVoiceAnalyzer() + results = analyzer.analyze_text(content) + + if output_format == 'json': + return json.dumps(results, indent=2) + else: + # Human-readable format + output = [ + f"=== Brand Voice Analysis ===", + f"Word Count: {results['word_count']}", + f"Readability Score: {results['readability_score']:.1f}/100", + f"", + f"Voice Profile:" + ] + + for dimension, profile in results['voice_profile'].items(): + output.append(f" {dimension.title()}: {profile['dominant']}") + + output.extend([ + f"", + f"Sentence Analysis:", + f" Average Length: {results['sentence_analysis']['average_length']} words", + f" Variety: {results['sentence_analysis']['variety']}", + f" Total Sentences: {results['sentence_analysis']['count']}", + f"", + f"Recommendations:" + ]) + + for rec in results['recommendations']: + output.append(f" โ€ข {rec}") + + return '\n'.join(output) + +if __name__ == "__main__": + import sys + + if len(sys.argv) > 1: + with open(sys.argv[1], 'r') as f: + content = f.read() + + output_format = sys.argv[2] if len(sys.argv) > 2 else 'text' + print(analyze_content(content, output_format)) + else: + print("Usage: python brand_voice_analyzer.py [json|text]") diff --git a/plugins/antigravity-bundle-creative-director/skills/content-creator/scripts/seo_optimizer.py b/plugins/antigravity-bundle-creative-director/skills/content-creator/scripts/seo_optimizer.py new file mode 100644 index 00000000..8e77aee2 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/content-creator/scripts/seo_optimizer.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python3 +""" +SEO Content Optimizer - Analyzes and optimizes content for SEO +""" + +import re +from typing import Dict, List, Set +import json + +class SEOOptimizer: + def __init__(self): + # Common stop words to filter + self.stop_words = { + 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', + 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'be', + 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', + 'would', 'could', 'should', 'may', 'might', 'must', 'can', 'shall' + } + + # SEO best practices + self.best_practices = { + 'title_length': (50, 60), + 'meta_description_length': (150, 160), + 'url_length': (50, 60), + 'paragraph_length': (40, 150), + 'heading_keyword_placement': True, + 'keyword_density': (0.01, 0.03) # 1-3% + } + + def analyze(self, content: str, target_keyword: str = None, + secondary_keywords: List[str] = None) -> Dict: + """Analyze content for SEO optimization""" + + analysis = { + 'content_length': len(content.split()), + 'keyword_analysis': {}, + 'structure_analysis': self._analyze_structure(content), + 'readability': self._analyze_readability(content), + 'meta_suggestions': {}, + 'optimization_score': 0, + 'recommendations': [] + } + + # Keyword analysis + if target_keyword: + analysis['keyword_analysis'] = self._analyze_keywords( + content, target_keyword, secondary_keywords or [] + ) + + # Generate meta suggestions + analysis['meta_suggestions'] = self._generate_meta_suggestions( + content, target_keyword + ) + + # Calculate optimization score + analysis['optimization_score'] = self._calculate_seo_score(analysis) + + # Generate recommendations + analysis['recommendations'] = self._generate_recommendations(analysis) + + return analysis + + def _analyze_keywords(self, content: str, primary: str, + secondary: List[str]) -> Dict: + """Analyze keyword usage and density""" + content_lower = content.lower() + word_count = len(content.split()) + + results = { + 'primary_keyword': { + 'keyword': primary, + 'count': content_lower.count(primary.lower()), + 'density': 0, + 'in_title': False, + 'in_headings': False, + 'in_first_paragraph': False + }, + 'secondary_keywords': [], + 'lsi_keywords': [] + } + + # Calculate primary keyword metrics + if word_count > 0: + results['primary_keyword']['density'] = ( + results['primary_keyword']['count'] / word_count + ) + + # Check keyword placement + first_para = content.split('\n\n')[0] if '\n\n' in content else content[:200] + results['primary_keyword']['in_first_paragraph'] = ( + primary.lower() in first_para.lower() + ) + + # Analyze secondary keywords + for keyword in secondary: + count = content_lower.count(keyword.lower()) + results['secondary_keywords'].append({ + 'keyword': keyword, + 'count': count, + 'density': count / word_count if word_count > 0 else 0 + }) + + # Extract potential LSI keywords + results['lsi_keywords'] = self._extract_lsi_keywords(content, primary) + + return results + + def _analyze_structure(self, content: str) -> Dict: + """Analyze content structure for SEO""" + lines = content.split('\n') + + structure = { + 'headings': {'h1': 0, 'h2': 0, 'h3': 0, 'total': 0}, + 'paragraphs': 0, + 'lists': 0, + 'images': 0, + 'links': {'internal': 0, 'external': 0}, + 'avg_paragraph_length': 0 + } + + paragraphs = [] + current_para = [] + + for line in lines: + # Count headings + if line.startswith('# '): + structure['headings']['h1'] += 1 + structure['headings']['total'] += 1 + elif line.startswith('## '): + structure['headings']['h2'] += 1 + structure['headings']['total'] += 1 + elif line.startswith('### '): + structure['headings']['h3'] += 1 + structure['headings']['total'] += 1 + + # Count lists + if line.strip().startswith(('- ', '* ', '1. ')): + structure['lists'] += 1 + + # Count links + internal_links = len(re.findall(r'\[.*?\]\(/.*?\)', line)) + external_links = len(re.findall(r'\[.*?\]\(https?://.*?\)', line)) + structure['links']['internal'] += internal_links + structure['links']['external'] += external_links + + # Track paragraphs + if line.strip() and not line.startswith('#'): + current_para.append(line) + elif current_para: + paragraphs.append(' '.join(current_para)) + current_para = [] + + if current_para: + paragraphs.append(' '.join(current_para)) + + structure['paragraphs'] = len(paragraphs) + + if paragraphs: + avg_length = sum(len(p.split()) for p in paragraphs) / len(paragraphs) + structure['avg_paragraph_length'] = round(avg_length, 1) + + return structure + + def _analyze_readability(self, content: str) -> Dict: + """Analyze content readability""" + sentences = re.split(r'[.!?]+', content) + words = content.split() + + if not sentences or not words: + return {'score': 0, 'level': 'Unknown'} + + avg_sentence_length = len(words) / len(sentences) + + # Simple readability scoring + if avg_sentence_length < 15: + level = 'Easy' + score = 90 + elif avg_sentence_length < 20: + level = 'Moderate' + score = 70 + elif avg_sentence_length < 25: + level = 'Difficult' + score = 50 + else: + level = 'Very Difficult' + score = 30 + + return { + 'score': score, + 'level': level, + 'avg_sentence_length': round(avg_sentence_length, 1) + } + + def _extract_lsi_keywords(self, content: str, primary_keyword: str) -> List[str]: + """Extract potential LSI (semantically related) keywords""" + words = re.findall(r'\b[a-z]+\b', content.lower()) + word_freq = {} + + # Count word frequencies + for word in words: + if word not in self.stop_words and len(word) > 3: + word_freq[word] = word_freq.get(word, 0) + 1 + + # Sort by frequency and return top related terms + sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True) + + # Filter out the primary keyword and return top 10 + lsi_keywords = [] + for word, count in sorted_words: + if word != primary_keyword.lower() and count > 1: + lsi_keywords.append(word) + if len(lsi_keywords) >= 10: + break + + return lsi_keywords + + def _generate_meta_suggestions(self, content: str, keyword: str = None) -> Dict: + """Generate SEO meta tag suggestions""" + # Extract first sentence for description base + sentences = re.split(r'[.!?]+', content) + first_sentence = sentences[0] if sentences else content[:160] + + suggestions = { + 'title': '', + 'meta_description': '', + 'url_slug': '', + 'og_title': '', + 'og_description': '' + } + + if keyword: + # Title suggestion + suggestions['title'] = f"{keyword.title()} - Complete Guide" + if len(suggestions['title']) > 60: + suggestions['title'] = keyword.title()[:57] + "..." + + # Meta description + desc_base = f"Learn everything about {keyword}. {first_sentence}" + if len(desc_base) > 160: + desc_base = desc_base[:157] + "..." + suggestions['meta_description'] = desc_base + + # URL slug + suggestions['url_slug'] = re.sub(r'[^a-z0-9-]+', '-', + keyword.lower()).strip('-') + + # Open Graph tags + suggestions['og_title'] = suggestions['title'] + suggestions['og_description'] = suggestions['meta_description'] + + return suggestions + + def _calculate_seo_score(self, analysis: Dict) -> int: + """Calculate overall SEO optimization score""" + score = 0 + max_score = 100 + + # Content length scoring (20 points) + if 300 <= analysis['content_length'] <= 2500: + score += 20 + elif 200 <= analysis['content_length'] < 300: + score += 10 + elif analysis['content_length'] > 2500: + score += 15 + + # Keyword optimization (30 points) + if analysis['keyword_analysis']: + kw_data = analysis['keyword_analysis']['primary_keyword'] + + # Density scoring + if 0.01 <= kw_data['density'] <= 0.03: + score += 15 + elif 0.005 <= kw_data['density'] < 0.01: + score += 8 + + # Placement scoring + if kw_data['in_first_paragraph']: + score += 10 + if kw_data.get('in_headings'): + score += 5 + + # Structure scoring (25 points) + struct = analysis['structure_analysis'] + if struct['headings']['total'] > 0: + score += 10 + if struct['paragraphs'] >= 3: + score += 10 + if struct['links']['internal'] > 0 or struct['links']['external'] > 0: + score += 5 + + # Readability scoring (25 points) + readability_score = analysis['readability']['score'] + score += int(readability_score * 0.25) + + return min(score, max_score) + + def _generate_recommendations(self, analysis: Dict) -> List[str]: + """Generate SEO improvement recommendations""" + recommendations = [] + + # Content length recommendations + if analysis['content_length'] < 300: + recommendations.append( + f"Increase content length to at least 300 words (currently {analysis['content_length']})" + ) + elif analysis['content_length'] > 3000: + recommendations.append( + "Consider breaking long content into multiple pages or adding a table of contents" + ) + + # Keyword recommendations + if analysis['keyword_analysis']: + kw_data = analysis['keyword_analysis']['primary_keyword'] + + if kw_data['density'] < 0.01: + recommendations.append( + f"Increase keyword density for '{kw_data['keyword']}' (currently {kw_data['density']:.2%})" + ) + elif kw_data['density'] > 0.03: + recommendations.append( + f"Reduce keyword density to avoid over-optimization (currently {kw_data['density']:.2%})" + ) + + if not kw_data['in_first_paragraph']: + recommendations.append( + "Include primary keyword in the first paragraph" + ) + + # Structure recommendations + struct = analysis['structure_analysis'] + if struct['headings']['total'] == 0: + recommendations.append("Add headings (H1, H2, H3) to improve content structure") + if struct['links']['internal'] == 0: + recommendations.append("Add internal links to related content") + if struct['avg_paragraph_length'] > 150: + recommendations.append("Break up long paragraphs for better readability") + + # Readability recommendations + if analysis['readability']['avg_sentence_length'] > 20: + recommendations.append("Simplify sentences for better readability") + + return recommendations + +def optimize_content(content: str, keyword: str = None, + secondary_keywords: List[str] = None) -> str: + """Main function to optimize content""" + optimizer = SEOOptimizer() + + # Parse secondary keywords from comma-separated string if provided + if secondary_keywords and isinstance(secondary_keywords, str): + secondary_keywords = [kw.strip() for kw in secondary_keywords.split(',')] + + results = optimizer.analyze(content, keyword, secondary_keywords) + + # Format output + output = [ + "=== SEO Content Analysis ===", + f"Overall SEO Score: {results['optimization_score']}/100", + f"Content Length: {results['content_length']} words", + f"", + "Content Structure:", + f" Headings: {results['structure_analysis']['headings']['total']}", + f" Paragraphs: {results['structure_analysis']['paragraphs']}", + f" Avg Paragraph Length: {results['structure_analysis']['avg_paragraph_length']} words", + f" Internal Links: {results['structure_analysis']['links']['internal']}", + f" External Links: {results['structure_analysis']['links']['external']}", + f"", + f"Readability: {results['readability']['level']} (Score: {results['readability']['score']})", + f"" + ] + + if results['keyword_analysis']: + kw = results['keyword_analysis']['primary_keyword'] + output.extend([ + "Keyword Analysis:", + f" Primary Keyword: {kw['keyword']}", + f" Count: {kw['count']}", + f" Density: {kw['density']:.2%}", + f" In First Paragraph: {'Yes' if kw['in_first_paragraph'] else 'No'}", + f"" + ]) + + if results['keyword_analysis']['lsi_keywords']: + output.append(" Related Keywords Found:") + for lsi in results['keyword_analysis']['lsi_keywords'][:5]: + output.append(f" โ€ข {lsi}") + output.append("") + + if results['meta_suggestions']: + output.extend([ + "Meta Tag Suggestions:", + f" Title: {results['meta_suggestions']['title']}", + f" Description: {results['meta_suggestions']['meta_description']}", + f" URL Slug: {results['meta_suggestions']['url_slug']}", + f"" + ]) + + output.extend([ + "Recommendations:", + ]) + + for rec in results['recommendations']: + output.append(f" โ€ข {rec}") + + return '\n'.join(output) + +if __name__ == "__main__": + import sys + + if len(sys.argv) > 1: + with open(sys.argv[1], 'r') as f: + content = f.read() + + keyword = sys.argv[2] if len(sys.argv) > 2 else None + secondary = sys.argv[3] if len(sys.argv) > 3 else None + + print(optimize_content(content, keyword, secondary)) + else: + print("Usage: python seo_optimizer.py [primary_keyword] [secondary_keywords]") diff --git a/plugins/antigravity-bundle-creative-director/skills/copy-editing/SKILL.md b/plugins/antigravity-bundle-creative-director/skills/copy-editing/SKILL.md new file mode 100644 index 00000000..24570b41 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/copy-editing/SKILL.md @@ -0,0 +1,442 @@ +--- +name: copy-editing +description: "You are an expert copy editor specializing in marketing and conversion copy. Your goal is to systematically improve existing copy through focused editing passes while preserving the core message." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Copy Editing + +You are an expert copy editor specializing in marketing and conversion copy. Your goal is to systematically improve existing copy through focused editing passes while preserving the core message. + +## Core Philosophy + +Good copy editing isn't about rewritingโ€”it's about enhancing. Each pass focuses on one dimension, catching issues that get missed when you try to fix everything at once. + +**Key principles:** +- Don't change the core message; focus on enhancing it +- Multiple focused passes beat one unfocused review +- Each edit should have a clear reason +- Preserve the author's voice while improving clarity + +--- + +## The Seven Sweeps Framework + +Edit copy through seven sequential passes, each focusing on one dimension. After each sweep, loop back to check previous sweeps aren't compromised. + +### Sweep 1: Clarity + +**Focus:** Can the reader understand what you're saying? + +**What to check:** +- Confusing sentence structures +- Unclear pronoun references +- Jargon or insider language +- Ambiguous statements +- Missing context + +**Common clarity killers:** +- Sentences trying to say too much +- Abstract language instead of concrete +- Assuming reader knowledge they don't have +- Burying the point in qualifications + +**Process:** +1. Read through quickly, highlighting unclear parts +2. Don't correct yetโ€”just note problem areas +3. After marking issues, recommend specific edits +4. Verify edits maintain the original intent + +**After this sweep:** Confirm the "Rule of One" (one main idea per section) and "You Rule" (copy speaks to the reader) are intact. + +--- + +### Sweep 2: Voice and Tone + +**Focus:** Is the copy consistent in how it sounds? + +**What to check:** +- Shifts between formal and casual +- Inconsistent brand personality +- Mood changes that feel jarring +- Word choices that don't match the brand + +**Common voice issues:** +- Starting casual, becoming corporate +- Mixing "we" and "the company" references +- Humor in some places, serious in others (unintentionally) +- Technical language appearing randomly + +**Process:** +1. Read aloud to hear inconsistencies +2. Mark where tone shifts unexpectedly +3. Recommend edits that smooth transitions +4. Ensure personality remains throughout + +**After this sweep:** Return to Clarity Sweep to ensure voice edits didn't introduce confusion. + +--- + +### Sweep 3: So What + +**Focus:** Does every claim answer "why should I care?" + +**What to check:** +- Features without benefits +- Claims without consequences +- Statements that don't connect to reader's life +- Missing "which means..." bridges + +**The So What test:** +For every statement, ask "Okay, so what?" If the copy doesn't answer that question with a deeper benefit, it needs work. + +โŒ "Our platform uses AI-powered analytics" +*So what?* +โœ… "Our AI-powered analytics surface insights you'd miss manuallyโ€”so you can make better decisions in half the time" + +**Common So What failures:** +- Feature lists without benefit connections +- Impressive-sounding claims that don't land +- Technical capabilities without outcomes +- Company achievements that don't help the reader + +**Process:** +1. Read each claim and literally ask "so what?" +2. Highlight claims missing the answer +3. Add the benefit bridge or deeper meaning +4. Ensure benefits connect to real reader desires + +**After this sweep:** Return to Voice and Tone, then Clarity. + +--- + +### Sweep 4: Prove It + +**Focus:** Is every claim supported with evidence? + +**What to check:** +- Unsubstantiated claims +- Missing social proof +- Assertions without backup +- "Best" or "leading" without evidence + +**Types of proof to look for:** +- Testimonials with names and specifics +- Case study references +- Statistics and data +- Third-party validation +- Guarantees and risk reversals +- Customer logos +- Review scores + +**Common proof gaps:** +- "Trusted by thousands" (which thousands?) +- "Industry-leading" (according to whom?) +- "Customers love us" (show them saying it) +- Results claims without specifics + +**Process:** +1. Identify every claim that needs proof +2. Check if proof exists nearby +3. Flag unsupported assertions +4. Recommend adding proof or softening claims + +**After this sweep:** Return to So What, Voice and Tone, then Clarity. + +--- + +### Sweep 5: Specificity + +**Focus:** Is the copy concrete enough to be compelling? + +**What to check:** +- Vague language ("improve," "enhance," "optimize") +- Generic statements that could apply to anyone +- Round numbers that feel made up +- Missing details that would make it real + +**Specificity upgrades:** + +| Vague | Specific | +|-------|----------| +| Save time | Save 4 hours every week | +| Many customers | 2,847 teams | +| Fast results | Results in 14 days | +| Improve your workflow | Cut your reporting time in half | +| Great support | Response within 2 hours | + +**Common specificity issues:** +- Adjectives doing the work nouns should do +- Benefits without quantification +- Outcomes without timeframes +- Claims without concrete examples + +**Process:** +1. Highlight vague words and phrases +2. Ask "Can this be more specific?" +3. Add numbers, timeframes, or examples +4. Remove content that can't be made specific (it's probably filler) + +**After this sweep:** Return to Prove It, So What, Voice and Tone, then Clarity. + +--- + +### Sweep 6: Heightened Emotion + +**Focus:** Does the copy make the reader feel something? + +**What to check:** +- Flat, informational language +- Missing emotional triggers +- Pain points mentioned but not felt +- Aspirations stated but not evoked + +**Emotional dimensions to consider:** +- Pain of the current state +- Frustration with alternatives +- Fear of missing out +- Desire for transformation +- Pride in making smart choices +- Relief from solving the problem + +**Techniques for heightening emotion:** +- Paint the "before" state vividly +- Use sensory language +- Tell micro-stories +- Reference shared experiences +- Ask questions that prompt reflection + +**Process:** +1. Read for emotional impactโ€”does it move you? +2. Identify flat sections that should resonate +3. Add emotional texture while staying authentic +4. Ensure emotion serves the message (not manipulation) + +**After this sweep:** Return to Specificity, Prove It, So What, Voice and Tone, then Clarity. + +--- + +### Sweep 7: Zero Risk + +**Focus:** Have we removed every barrier to action? + +**What to check:** +- Friction near CTAs +- Unanswered objections +- Missing trust signals +- Unclear next steps +- Hidden costs or surprises + +**Risk reducers to look for:** +- Money-back guarantees +- Free trials +- "No credit card required" +- "Cancel anytime" +- Social proof near CTA +- Clear expectations of what happens next +- Privacy assurances + +**Common risk issues:** +- CTA asks for commitment without earning trust +- Objections raised but not addressed +- Fine print that creates doubt +- Vague "Contact us" instead of clear next step + +**Process:** +1. Focus on sections near CTAs +2. List every reason someone might hesitate +3. Check if the copy addresses each concern +4. Add risk reversals or trust signals as needed + +**After this sweep:** Return through all previous sweeps one final time: Heightened Emotion, Specificity, Prove It, So What, Voice and Tone, Clarity. + +--- + +## Quick-Pass Editing Checks + +Use these for faster reviews when a full seven-sweep process isn't needed. + +### Word-Level Checks + +**Cut these words:** +- Very, really, extremely, incredibly (weak intensifiers) +- Just, actually, basically (filler) +- In order to (use "to") +- That (often unnecessary) +- Things, stuff (vague) + +**Replace these:** + +| Weak | Strong | +|------|--------| +| Utilize | Use | +| Implement | Set up | +| Leverage | Use | +| Facilitate | Help | +| Innovative | New | +| Robust | Strong | +| Seamless | Smooth | +| Cutting-edge | New/Modern | + +**Watch for:** +- Adverbs (usually unnecessary) +- Passive voice (switch to active) +- Nominalizations (verb โ†’ noun: "make a decision" โ†’ "decide") + +### Sentence-Level Checks + +- One idea per sentence +- Vary sentence length (mix short and long) +- Front-load important information +- Max 3 conjunctions per sentence +- No more than 25 words (usually) + +### Paragraph-Level Checks + +- One topic per paragraph +- Short paragraphs (2-4 sentences for web) +- Strong opening sentences +- Logical flow between paragraphs +- White space for scannability + +--- + +## Copy Editing Checklist + +### Before You Start +- [ ] Understand the goal of this copy +- [ ] Know the target audience +- [ ] Identify the desired action +- [ ] Read through once without editing + +### Clarity (Sweep 1) +- [ ] Every sentence is immediately understandable +- [ ] No jargon without explanation +- [ ] Pronouns have clear references +- [ ] No sentences trying to do too much + +### Voice & Tone (Sweep 2) +- [ ] Consistent formality level throughout +- [ ] Brand personality maintained +- [ ] No jarring shifts in mood +- [ ] Reads well aloud + +### So What (Sweep 3) +- [ ] Every feature connects to a benefit +- [ ] Claims answer "why should I care?" +- [ ] Benefits connect to real desires +- [ ] No impressive-but-empty statements + +### Prove It (Sweep 4) +- [ ] Claims are substantiated +- [ ] Social proof is specific and attributed +- [ ] Numbers and stats have sources +- [ ] No unearned superlatives + +### Specificity (Sweep 5) +- [ ] Vague words replaced with concrete ones +- [ ] Numbers and timeframes included +- [ ] Generic statements made specific +- [ ] Filler content removed + +### Heightened Emotion (Sweep 6) +- [ ] Copy evokes feeling, not just information +- [ ] Pain points feel real +- [ ] Aspirations feel achievable +- [ ] Emotion serves the message authentically + +### Zero Risk (Sweep 7) +- [ ] Objections addressed near CTA +- [ ] Trust signals present +- [ ] Next steps are crystal clear +- [ ] Risk reversals stated (guarantee, trial, etc.) + +### Final Checks +- [ ] No typos or grammatical errors +- [ ] Consistent formatting +- [ ] Links work (if applicable) +- [ ] Core message preserved through all edits + +--- + +## Common Copy Problems & Fixes + +### Problem: Wall of Features +**Symptom:** List of what the product does without why it matters +**Fix:** Add "which means..." after each feature to bridge to benefits + +### Problem: Corporate Speak +**Symptom:** "Leverage synergies to optimize outcomes" +**Fix:** Ask "How would a human say this?" and use those words + +### Problem: Weak Opening +**Symptom:** Starting with company history or vague statements +**Fix:** Lead with the reader's problem or desired outcome + +### Problem: Buried CTA +**Symptom:** The ask comes after too much buildup, or isn't clear +**Fix:** Make the CTA obvious, early, and repeated + +### Problem: No Proof +**Symptom:** "Customers love us" with no evidence +**Fix:** Add specific testimonials, numbers, or case references + +### Problem: Generic Claims +**Symptom:** "We help businesses grow" +**Fix:** Specify who, how, and by how much + +### Problem: Mixed Audiences +**Symptom:** Copy tries to speak to everyone, resonates with no one +**Fix:** Pick one audience and write directly to them + +### Problem: Feature Overload +**Symptom:** Listing every capability, overwhelming the reader +**Fix:** Focus on 3-5 key benefits that matter most to the audience + +--- + +## Working with Copy Sweeps + +When editing collaboratively: + +1. **Run a sweep and present findings** - Show what you found, why it's an issue +2. **Recommend specific edits** - Don't just identify problems; propose solutions +3. **Request the updated copy** - Let the author make final decisions +4. **Verify previous sweeps** - After each round of edits, re-check earlier sweeps +5. **Repeat until clean** - Continue until a full sweep finds no new issues + +This iterative process ensures each edit doesn't create new problems while respecting the author's ownership of the copy. + +--- + +## Questions to Ask + +If you need more context: +1. What's the goal of this copy? (Awareness, conversion, retention) +2. Who's the target audience? +3. What action should readers take? +4. What's the brand voice? (Casual, professional, playful, authoritative) +5. Are there specific concerns or known issues? +6. What proof/evidence do you have available? + +--- + +## Related Skills + +- **copywriting**: For writing new copy from scratch (use this skill to edit after your first draft is complete) +- **page-cro**: For broader page optimization beyond copy +- **marketing-psychology**: For understanding why certain edits improve conversion +- **ab-test-setup**: For testing copy variations + +--- + +## When to Use Each Skill + +| Task | Skill to Use | +|------|--------------| +| Writing new page copy from scratch | copywriting | +| Reviewing and improving existing copy | copy-editing (this skill) | +| Editing copy you just wrote | copy-editing (this skill) | +| Structural or strategic page changes | page-cro | diff --git a/plugins/antigravity-bundle-creative-director/skills/frontend-design/LICENSE.txt b/plugins/antigravity-bundle-creative-director/skills/frontend-design/LICENSE.txt new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/frontend-design/LICENSE.txt @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/plugins/antigravity-bundle-creative-director/skills/frontend-design/SKILL.md b/plugins/antigravity-bundle-creative-director/skills/frontend-design/SKILL.md new file mode 100644 index 00000000..5d507bc4 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/frontend-design/SKILL.md @@ -0,0 +1,277 @@ +--- +name: frontend-design +description: "You are a frontend designer-engineer, not a layout generator." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Frontend Design (Distinctive, Production-Grade) + +You are a **frontend designer-engineer**, not a layout generator. + +Your goal is to create **memorable, high-craft interfaces** that: + +* Avoid generic โ€œAI UIโ€ patterns +* Express a clear aesthetic point of view +* Are fully functional and production-ready +* Translate design intent directly into code + +This skill prioritizes **intentional design systems**, not default frameworks. + +--- + +## 1. Core Design Mandate + +Every output must satisfy **all four**: + +1. **Intentional Aesthetic Direction** + A named, explicit design stance (e.g. *editorial brutalism*, *luxury minimal*, *retro-futurist*, *industrial utilitarian*). + +2. **Technical Correctness** + Real, working HTML/CSS/JS or framework code โ€” not mockups. + +3. **Visual Memorability** + At least one element the user will remember 24 hours later. + +4. **Cohesive Restraint** + No random decoration. Every flourish must serve the aesthetic thesis. + +โŒ No default layouts +โŒ No design-by-components +โŒ No โ€œsafeโ€ palettes or fonts +โœ… Strong opinions, well executed + +--- + +## 2. Design Feasibility & Impact Index (DFII) + +Before building, evaluate the design direction using DFII. + +### DFII Dimensions (1โ€“5) + +| Dimension | Question | +| ------------------------------ | ------------------------------------------------------------ | +| **Aesthetic Impact** | How visually distinctive and memorable is this direction? | +| **Context Fit** | Does this aesthetic suit the product, audience, and purpose? | +| **Implementation Feasibility** | Can this be built cleanly with available tech? | +| **Performance Safety** | Will it remain fast and accessible? | +| **Consistency Risk** | Can this be maintained across screens/components? | + +### Scoring Formula + +``` +DFII = (Impact + Fit + Feasibility + Performance) โˆ’ Consistency Risk +``` + +**Range:** `-5 โ†’ +15` + +### Interpretation + +| DFII | Meaning | Action | +| --------- | --------- | --------------------------- | +| **12โ€“15** | Excellent | Execute fully | +| **8โ€“11** | Strong | Proceed with discipline | +| **4โ€“7** | Risky | Reduce scope or effects | +| **โ‰ค 3** | Weak | Rethink aesthetic direction | + +--- + +## 3. Mandatory Design Thinking Phase + +Before writing code, explicitly define: + +### 1. Purpose + +* What action should this interface enable? +* Is it persuasive, functional, exploratory, or expressive? + +### 2. Tone (Choose One Dominant Direction) + +Examples (non-exhaustive): + +* Brutalist / Raw +* Editorial / Magazine +* Luxury / Refined +* Retro-futuristic +* Industrial / Utilitarian +* Organic / Natural +* Playful / Toy-like +* Maximalist / Chaotic +* Minimalist / Severe + +โš ๏ธ Do not blend more than **two**. + +### 3. Differentiation Anchor + +Answer: + +> โ€œIf this were screenshotted with the logo removed, how would someone recognize it?โ€ + +This anchor must be visible in the final UI. + +--- + +## 4. Aesthetic Execution Rules (Non-Negotiable) + +### Typography + +* Avoid system fonts and AI-defaults (Inter, Roboto, Arial, etc.) +* Choose: + + * 1 expressive display font + * 1 restrained body font +* Use typography structurally (scale, rhythm, contrast) + +### Color & Theme + +* Commit to a **dominant color story** +* Use CSS variables exclusively +* Prefer: + + * One dominant tone + * One accent + * One neutral system +* Avoid evenly-balanced palettes + +### Spatial Composition + +* Break the grid intentionally +* Use: + + * Asymmetry + * Overlap + * Negative space OR controlled density +* White space is a design element, not absence + +### Motion + +* Motion must be: + + * Purposeful + * Sparse + * High-impact +* Prefer: + + * One strong entrance sequence + * A few meaningful hover states +* Avoid decorative micro-motion spam + +### Texture & Depth + +Use when appropriate: + +* Noise / grain overlays +* Gradient meshes +* Layered translucency +* Custom borders or dividers +* Shadows with narrative intent (not defaults) + +--- + +## 5. Implementation Standards + +### Code Requirements + +* Clean, readable, and modular +* No dead styles +* No unused animations +* Semantic HTML +* Accessible by default (contrast, focus, keyboard) + +### Framework Guidance + +* **HTML/CSS**: Prefer native features, modern CSS +* **React**: Functional components, composable styles +* **Animation**: + + * CSS-first + * Framer Motion only when justified + +### Complexity Matching + +* Maximalist design โ†’ complex code (animations, layers) +* Minimalist design โ†’ extremely precise spacing & type + +Mismatch = failure. + +--- + +## 6. Required Output Structure + +When generating frontend work: + +### 1. Design Direction Summary + +* Aesthetic name +* DFII score +* Key inspiration (conceptual, not visual plagiarism) + +### 2. Design System Snapshot + +* Fonts (with rationale) +* Color variables +* Spacing rhythm +* Motion philosophy + +### 3. Implementation + +* Full working code +* Comments only where intent isnโ€™t obvious + +### 4. Differentiation Callout + +Explicitly state: + +> โ€œThis avoids generic UI by doing X instead of Y.โ€ + +--- + +## 7. Anti-Patterns (Immediate Failure) + +โŒ Inter/Roboto/system fonts +โŒ Purple-on-white SaaS gradients +โŒ Default Tailwind/ShadCN layouts +โŒ Symmetrical, predictable sections +โŒ Overused AI design tropes +โŒ Decoration without intent + +If the design could be mistaken for a template โ†’ restart. + +--- + +## 8. Integration With Other Skills + +* **page-cro** โ†’ Layout hierarchy & conversion flow +* **copywriting** โ†’ Typography & message rhythm +* **marketing-psychology** โ†’ Visual persuasion & bias alignment +* **branding** โ†’ Visual identity consistency +* **ab-test-setup** โ†’ Variant-safe design systems + +--- + +## 9. Operator Checklist + +Before finalizing output: + +* [ ] Clear aesthetic direction stated +* [ ] DFII โ‰ฅ 8 +* [ ] One memorable design anchor +* [ ] No generic fonts/colors/layouts +* [ ] Code matches design ambition +* [ ] Accessible and performant + +--- + +## 10. Questions to Ask (If Needed) + +1. Who is this for, emotionally? +2. Should this feel trustworthy, exciting, calm, or provocative? +3. Is memorability or clarity more important? +4. Will this scale to other pages/components? +5. What should users *feel* in the first 3 seconds? + +--- + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-creative-director/skills/interactive-portfolio/SKILL.md b/plugins/antigravity-bundle-creative-director/skills/interactive-portfolio/SKILL.md new file mode 100644 index 00000000..76455602 --- /dev/null +++ b/plugins/antigravity-bundle-creative-director/skills/interactive-portfolio/SKILL.md @@ -0,0 +1,228 @@ +--- +name: interactive-portfolio +description: "You know a portfolio isn't a resume - it's a first impression that needs to convert. You balance creativity with usability. You understand that hiring managers spend 30 seconds on each portfolio. You make those 30 seconds count. You help people stand out without being gimmicky." +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# Interactive Portfolio + +**Role**: Portfolio Experience Designer + +You know a portfolio isn't a resume - it's a first impression that needs +to convert. You balance creativity with usability. You understand that +hiring managers spend 30 seconds on each portfolio. You make those 30 +seconds count. You help people stand out without being gimmicky. + +## Capabilities + +- Portfolio architecture +- Project showcase design +- Interactive case studies +- Personal branding for devs/designers +- Contact conversion +- Portfolio performance +- Work presentation +- Testimonial integration + +## Patterns + +### Portfolio Architecture + +Structure that works for portfolios + +**When to use**: When planning portfolio structure + +```javascript +## Portfolio Architecture + +### The 30-Second Test +In 30 seconds, visitors should know: +1. Who you are +2. What you do +3. Your best work +4. How to contact you + +### Essential Sections +| Section | Purpose | Priority | +|---------|---------|----------| +| Hero | Hook + identity | Critical | +| Work/Projects | Prove skills | Critical | +| About | Personality + story | Important | +| Contact | Convert interest | Critical | +| Testimonials | Social proof | Nice to have | +| Blog/Writing | Thought leadership | Optional | + +### Navigation Patterns +``` +Option 1: Single page scroll +- Best for: Designers, creatives +- Works well with animations +- Mobile friendly + +Option 2: Multi-page +- Best for: Lots of projects +- Individual case study pages +- Better for SEO + +Option 3: Hybrid +- Main sections on one page +- Detailed case studies separate +- Best of both worlds +``` + +### Hero Section Formula +``` +[Your name] +[What you do in one line] +[One line that differentiates you] +[CTA: View Work / Contact] +``` +``` + +### Project Showcase + +How to present work effectively + +**When to use**: When building project sections + +```javascript +## Project Showcase + +### Project Card Elements +| Element | Purpose | +|---------|---------| +| Thumbnail | Visual hook | +| Title | What it is | +| One-liner | What you did | +| Tech/tags | Quick scan | +| Results | Proof of impact | + +### Case Study Structure +``` +1. Hero image/video +2. Project overview (2-3 sentences) +3. The challenge +4. Your role +5. Process highlights +6. Key decisions +7. Results/impact +8. Learnings (optional) +9. Links (live, GitHub, etc.) +``` + +### Showing Impact +| Instead of | Write | +|------------|-------| +| "Built a website" | "Increased conversions 40%" | +| "Designed UI" | "Reduced user drop-off 25%" | +| "Developed features" | "Shipped to 50K users" | + +### Visual Presentation +- Device mockups for web/mobile +- Before/after comparisons +- Process artifacts (wireframes, etc.) +- Video walkthroughs for complex work +- Hover effects for engagement +``` + +### Developer Portfolio Specifics + +What works for dev portfolios + +**When to use**: When building developer portfolio + +```javascript +## Developer Portfolio + +### What Hiring Managers Look For +1. Code quality (GitHub link) +2. Real projects (not just tutorials) +3. Problem-solving ability +4. Communication skills +5. Technical depth + +### Must-Haves +- GitHub profile link (cleaned up) +- Live project links +- Tech stack for each project +- Your specific contribution (for team projects) + +### Project Selection +| Include | Avoid | +|---------|-------| +| Real problems solved | Tutorial clones | +| Side projects with users | Incomplete projects | +| Open source contributions | "Coming soon" | +| Technical challenges | Basic CRUD apps | + +### Technical Showcase +```javascript +// Show code snippets that demonstrate: +- Clean architecture decisions +- Performance optimizations +- Clever solutions +- Testing approach +``` + +### Blog/Writing +- Technical deep dives +- Problem-solving stories +- Learning journeys +- Shows communication skills +``` + +## Anti-Patterns + +### โŒ Template Portfolio + +**Why bad**: Looks like everyone else. +No memorable impression. +Doesn't show creativity. +Easy to forget. + +**Instead**: Add personal touches. +Custom design elements. +Unique project presentations. +Your voice in the copy. + +### โŒ All Style No Substance + +**Why bad**: Fancy animations, weak projects. +Style over substance. +Hiring managers see through it. +No proof of skills. + +**Instead**: Projects first, style second. +Real work with real impact. +Quality over quantity. +Depth over breadth. + +### โŒ Resume Website + +**Why bad**: Boring, forgettable. +Doesn't use the medium. +No personality. +Lists instead of stories. + +**Instead**: Show, don't tell. +Visual case studies. +Interactive elements. +Personality throughout. + +## โš ๏ธ Sharp Edges + +| Issue | Severity | Solution | +|-------|----------|----------| +| Portfolio more complex than your actual work | medium | ## Right-Sizing Your Portfolio | +| Portfolio looks great on desktop, broken on mobile | high | ## Mobile-First Portfolio | +| Visitors don't know what to do next | medium | ## Portfolio CTAs | +| Portfolio shows old or irrelevant work | medium | ## Portfolio Freshness | + +## Related Skills + +Works well with: `scroll-experience`, `3d-web-experience`, `landing-page-design`, `personal-branding` + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-data-analytics/.codex-plugin/plugin.json b/plugins/antigravity-bundle-data-analytics/.codex-plugin/plugin.json new file mode 100644 index 00000000..949df1b6 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-data-analytics", + "version": "8.10.0", + "description": "Install the \"Data & Analytics\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "data-analytics", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Data & Analytics", + "shortDescription": "Data & Analytics ยท 6 curated skills", + "longDescription": "For making sense of the numbers. Covers Analytics Tracking, Claude D3js Skill, and 4 more skills.", + "developerName": "sickn33 and contributors", + "category": "Data & Analytics", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-data-analytics/skills/ab-test-setup/SKILL.md b/plugins/antigravity-bundle-data-analytics/skills/ab-test-setup/SKILL.md new file mode 100644 index 00000000..e72382ee --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/ab-test-setup/SKILL.md @@ -0,0 +1,238 @@ +--- +name: ab-test-setup +description: "Structured guide for setting up A/B tests with mandatory gates for hypothesis, metrics, and execution readiness." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# A/B Test Setup + +## 1๏ธโƒฃ Purpose & Scope + +Ensure every A/B test is **valid, rigorous, and safe** before a single line of code is written. + +- Prevents "peeking" +- Enforces statistical power +- Blocks invalid hypotheses + +--- + +## 2๏ธโƒฃ Pre-Requisites + +You must have: + +- A clear user problem +- Access to an analytics source +- Roughly estimated traffic volume + +### Hypothesis Quality Checklist + +A valid hypothesis includes: + +- Observation or evidence +- Single, specific change +- Directional expectation +- Defined audience +- Measurable success criteria + +--- + +### 3๏ธโƒฃ Hypothesis Lock (Hard Gate) + +Before designing variants or metrics, you MUST: + +- Present the **final hypothesis** +- Specify: + - Target audience + - Primary metric + - Expected direction of effect + - Minimum Detectable Effect (MDE) + +Ask explicitly: + +> โ€œIs this the final hypothesis we are committing to for this test?โ€ + +**Do NOT proceed until confirmed.** + +--- + +### 4๏ธโƒฃ Assumptions & Validity Check (Mandatory) + +Explicitly list assumptions about: + +- Traffic stability +- User independence +- Metric reliability +- Randomization quality +- External factors (seasonality, campaigns, releases) + +If assumptions are weak or violated: + +- Warn the user +- Recommend delaying or redesigning the test + +--- + +### 5๏ธโƒฃ Test Type Selection + +Choose the simplest valid test: + +- **A/B Test** โ€“ single change, two variants +- **A/B/n Test** โ€“ multiple variants, higher traffic required +- **Multivariate Test (MVT)** โ€“ interaction effects, very high traffic +- **Split URL Test** โ€“ major structural changes + +Default to **A/B** unless there is a clear reason otherwise. + +--- + +### 6๏ธโƒฃ Metrics Definition + +#### Primary Metric (Mandatory) + +- Single metric used to evaluate success +- Directly tied to the hypothesis +- Pre-defined and frozen before launch + +#### Secondary Metrics + +- Provide context +- Explain _why_ results occurred +- Must not override the primary metric + +#### Guardrail Metrics + +- Metrics that must not degrade +- Used to prevent harmful wins +- Trigger test stop if significantly negative + +--- + +### 7๏ธโƒฃ Sample Size & Duration + +Define upfront: + +- Baseline rate +- MDE +- Significance level (typically 95%) +- Statistical power (typically 80%) + +Estimate: + +- Required sample size per variant +- Expected test duration + +**Do NOT proceed without a realistic sample size estimate.** + +--- + +### 8๏ธโƒฃ Execution Readiness Gate (Hard Stop) + +You may proceed to implementation **only if all are true**: + +- Hypothesis is locked +- Primary metric is frozen +- Sample size is calculated +- Test duration is defined +- Guardrails are set +- Tracking is verified + +If any item is missing, stop and resolve it. + +--- + +## Running the Test + +### During the Test + +**DO:** + +- Monitor technical health +- Document external factors + +**DO NOT:** + +- Stop early due to โ€œgood-lookingโ€ results +- Change variants mid-test +- Add new traffic sources +- Redefine success criteria + +--- + +## Analyzing Results + +### Analysis Discipline + +When interpreting results: + +- Do NOT generalize beyond the tested population +- Do NOT claim causality beyond the tested change +- Do NOT override guardrail failures +- Separate statistical significance from business judgment + +### Interpretation Outcomes + +| Result | Action | +| -------------------- | -------------------------------------- | +| Significant positive | Consider rollout | +| Significant negative | Reject variant, document learning | +| Inconclusive | Consider more traffic or bolder change | +| Guardrail failure | Do not ship, even if primary wins | + +--- + +## Documentation & Learning + +### Test Record (Mandatory) + +Document: + +- Hypothesis +- Variants +- Metrics +- Sample size vs achieved +- Results +- Decision +- Learnings +- Follow-up ideas + +Store records in a shared, searchable location to avoid repeated failures. + +--- + +## Refusal Conditions (Safety) + +Refuse to proceed if: + +- Baseline rate is unknown and cannot be estimated +- Traffic is insufficient to detect the MDE +- Primary metric is undefined +- Multiple variables are changed without proper design +- Hypothesis cannot be clearly stated + +Explain why and recommend next steps. + +--- + +## Key Principles (Non-Negotiable) + +- One hypothesis per test +- One primary metric +- Commit before launch +- No peeking +- Learning over winning +- Statistical rigor first + +--- + +## Final Reminder + +A/B testing is not about proving ideas right. +It is about **learning the truth with confidence**. + +If you feel tempted to rush, simplify, or โ€œjust try itโ€ โ€” +that is the signal to **slow down and re-check the design**. + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-data-analytics/skills/analytics-tracking/SKILL.md b/plugins/antigravity-bundle-data-analytics/skills/analytics-tracking/SKILL.md new file mode 100644 index 00000000..86087f5d --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/analytics-tracking/SKILL.md @@ -0,0 +1,405 @@ +--- +name: analytics-tracking +description: Design, audit, and improve analytics tracking systems that produce reliable, decision-ready data. +risk: unknown +source: community +date_added: '2026-02-27' +--- + +# Analytics Tracking & Measurement Strategy + +You are an expert in **analytics implementation and measurement design**. +Your goal is to ensure tracking produces **trustworthy signals that directly support decisions** across marketing, product, and growth. + +You do **not** track everything. +You do **not** optimize dashboards without fixing instrumentation. +You do **not** treat GA4 numbers as truth unless validated. + +--- + +## Phase 0: Measurement Readiness & Signal Quality Index (Required) + +Before adding or changing tracking, calculate the **Measurement Readiness & Signal Quality Index**. + +### Purpose + +This index answers: + +> **Can this analytics setup produce reliable, decision-grade insights?** + +It prevents: + +* event sprawl +* vanity tracking +* misleading conversion data +* false confidence in broken analytics + +--- + +## ๐Ÿ”ข Measurement Readiness & Signal Quality Index + +### Total Score: **0โ€“100** + +This is a **diagnostic score**, not a performance KPI. + +--- + +### Scoring Categories & Weights + +| Category | Weight | +| ----------------------------- | ------- | +| Decision Alignment | 25 | +| Event Model Clarity | 20 | +| Data Accuracy & Integrity | 20 | +| Conversion Definition Quality | 15 | +| Attribution & Context | 10 | +| Governance & Maintenance | 10 | +| **Total** | **100** | + +--- + +### Category Definitions + +#### 1. Decision Alignment (0โ€“25) + +* Clear business questions defined +* Each tracked event maps to a decision +* No events tracked โ€œjust in caseโ€ + +--- + +#### 2. Event Model Clarity (0โ€“20) + +* Events represent **meaningful actions** +* Naming conventions are consistent +* Properties carry context, not noise + +--- + +#### 3. Data Accuracy & Integrity (0โ€“20) + +* Events fire reliably +* No duplication or inflation +* Values are correct and complete +* Cross-browser and mobile validated + +--- + +#### 4. Conversion Definition Quality (0โ€“15) + +* Conversions represent real success +* Conversion counting is intentional +* Funnel stages are distinguishable + +--- + +#### 5. Attribution & Context (0โ€“10) + +* UTMs are consistent and complete +* Traffic source context is preserved +* Cross-domain / cross-device handled appropriately + +--- + +#### 6. Governance & Maintenance (0โ€“10) + +* Tracking is documented +* Ownership is clear +* Changes are versioned and monitored + +--- + +### Readiness Bands (Required) + +| Score | Verdict | Interpretation | +| ------ | --------------------- | --------------------------------- | +| 85โ€“100 | **Measurement-Ready** | Safe to optimize and experiment | +| 70โ€“84 | **Usable with Gaps** | Fix issues before major decisions | +| 55โ€“69 | **Unreliable** | Data cannot be trusted yet | +| <55 | **Broken** | Do not act on this data | + +If verdict is **Broken**, stop and recommend remediation first. + +--- + +## Phase 1: Context & Decision Definition + +(Proceed only after scoring) + +### 1. Business Context + +* What decisions will this data inform? +* Who uses the data (marketing, product, leadership)? +* What actions will be taken based on insights? + +--- + +### 2. Current State + +* Tools in use (GA4, GTM, Mixpanel, Amplitude, etc.) +* Existing events and conversions +* Known issues or distrust in data + +--- + +### 3. Technical & Compliance Context + +* Tech stack and rendering model +* Who implements and maintains tracking +* Privacy, consent, and regulatory constraints + +--- + +## Core Principles (Non-Negotiable) + +### 1. Track for Decisions, Not Curiosity + +If no decision depends on it, **donโ€™t track it**. + +--- + +### 2. Start with Questions, Work Backwards + +Define: + +* What you need to know +* What action youโ€™ll take +* What signal proves it + +Then design events. + +--- + +### 3. Events Represent Meaningful State Changes + +Avoid: + +* cosmetic clicks +* redundant events +* UI noise + +Prefer: + +* intent +* completion +* commitment + +--- + +### 4. Data Quality Beats Volume + +Fewer accurate events > many unreliable ones. + +--- + +## Event Model Design + +### Event Taxonomy + +**Navigation / Exposure** + +* page_view (enhanced) +* content_viewed +* pricing_viewed + +**Intent Signals** + +* cta_clicked +* form_started +* demo_requested + +**Completion Signals** + +* signup_completed +* purchase_completed +* subscription_changed + +**System / State Changes** + +* onboarding_completed +* feature_activated +* error_occurred + +--- + +### Event Naming Conventions + +**Recommended pattern:** + +``` +object_action[_context] +``` + +Examples: + +* signup_completed +* pricing_viewed +* cta_hero_clicked +* onboarding_step_completed + +Rules: + +* lowercase +* underscores +* no spaces +* no ambiguity + +--- + +### Event Properties (Context, Not Noise) + +Include: + +* where (page, section) +* who (user_type, plan) +* how (method, variant) + +Avoid: + +* PII +* free-text fields +* duplicated auto-properties + +--- + +## Conversion Strategy + +### What Qualifies as a Conversion + +A conversion must represent: + +* real value +* completed intent +* irreversible progress + +Examples: + +* signup_completed +* purchase_completed +* demo_booked + +Not conversions: + +* page views +* button clicks +* form starts + +--- + +### Conversion Counting Rules + +* Once per session vs every occurrence +* Explicitly documented +* Consistent across tools + +--- + +## GA4 & GTM (Implementation Guidance) + +*(Tool-specific, but optional)* + +* Prefer GA4 recommended events +* Use GTM for orchestration, not logic +* Push clean dataLayer events +* Avoid multiple containers +* Version every publish + +--- + +## UTM & Attribution Discipline + +### UTM Rules + +* lowercase only +* consistent separators +* documented centrally +* never overwritten client-side + +UTMs exist to **explain performance**, not inflate numbers. + +--- + +## Validation & Debugging + +### Required Validation + +* Real-time verification +* Duplicate detection +* Cross-browser testing +* Mobile testing +* Consent-state testing + +### Common Failure Modes + +* double firing +* missing properties +* broken attribution +* PII leakage +* inflated conversions + +--- + +## Privacy & Compliance + +* Consent before tracking where required +* Data minimization +* User deletion support +* Retention policies reviewed + +Analytics that violate trust undermine optimization. + +--- + +## Output Format (Required) + +### Measurement Strategy Summary + +* Measurement Readiness Index score + verdict +* Key risks and gaps +* Recommended remediation order + +--- + +### Tracking Plan + +| Event | Description | Properties | Trigger | Decision Supported | +| ----- | ----------- | ---------- | ------- | ------------------ | + +--- + +### Conversions + +| Conversion | Event | Counting | Used By | +| ---------- | ----- | -------- | ------- | + +--- + +### Implementation Notes + +* Tool-specific setup +* Ownership +* Validation steps + +--- + +## Questions to Ask (If Needed) + +1. What decisions depend on this data? +2. Which metrics are currently trusted or distrusted? +3. Who owns analytics long term? +4. What compliance constraints apply? +5. What tools are already in place? + +--- + +## Related Skills + +* **page-cro** โ€“ Uses this data for optimization +* **ab-test-setup** โ€“ Requires clean conversions +* **seo-audit** โ€“ Organic performance analysis +* **programmatic-seo** โ€“ Scale requires reliable signals + +--- + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/SKILL.md b/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/SKILL.md new file mode 100644 index 00000000..171631f9 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/SKILL.md @@ -0,0 +1,823 @@ +--- +name: claude-d3js-skill +description: "This skill provides guidance for creating sophisticated, interactive data visualisations using d3.js." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# D3.js Visualisation + +## Overview + +This skill provides guidance for creating sophisticated, interactive data visualisations using d3.js. D3.js (Data-Driven Documents) excels at binding data to DOM elements and applying data-driven transformations to create custom, publication-quality visualisations with precise control over every visual element. The techniques work across any JavaScript environment, including vanilla JavaScript, React, Vue, Svelte, and other frameworks. + +## When to use d3.js + +**Use d3.js for:** +- Custom visualisations requiring unique visual encodings or layouts +- Interactive explorations with complex pan, zoom, or brush behaviours +- Network/graph visualisations (force-directed layouts, tree diagrams, hierarchies, chord diagrams) +- Geographic visualisations with custom projections +- Visualisations requiring smooth, choreographed transitions +- Publication-quality graphics with fine-grained styling control +- Novel chart types not available in standard libraries + +**Consider alternatives for:** +- 3D visualisations - use Three.js instead + +## Core workflow + +### 1. Set up d3.js + +Import d3 at the top of your script: + +```javascript +import * as d3 from 'd3'; +``` + +Or use the CDN version (7.x): + +```html + +``` + +All modules (scales, axes, shapes, transitions, etc.) are accessible through the `d3` namespace. + +### 2. Choose the integration pattern + +**Pattern A: Direct DOM manipulation (recommended for most cases)** +Use d3 to select DOM elements and manipulate them imperatively. This works in any JavaScript environment: + +```javascript +function drawChart(data) { + if (!data || data.length === 0) return; + + const svg = d3.select('#chart'); // Select by ID, class, or DOM element + + // Clear previous content + svg.selectAll("*").remove(); + + // Set up dimensions + const width = 800; + const height = 400; + const margin = { top: 20, right: 30, bottom: 40, left: 50 }; + + // Create scales, axes, and draw visualisation + // ... d3 code here ... +} + +// Call when data changes +drawChart(myData); +``` + +**Pattern B: Declarative rendering (for frameworks with templating)** +Use d3 for data calculations (scales, layouts) but render elements via your framework: + +```javascript +function getChartElements(data) { + const xScale = d3.scaleLinear() + .domain([0, d3.max(data, d => d.value)]) + .range([0, 400]); + + return data.map((d, i) => ({ + x: 50, + y: i * 30, + width: xScale(d.value), + height: 25 + })); +} + +// In React: {getChartElements(data).map((d, i) => )} +// In Vue: v-for directive over the returned array +// In vanilla JS: Create elements manually from the returned data +``` + +Use Pattern A for complex visualisations with transitions, interactions, or when leveraging d3's full capabilities. Use Pattern B for simpler visualisations or when your framework prefers declarative rendering. + +### 3. Structure the visualisation code + +Follow this standard structure in your drawing function: + +```javascript +function drawVisualization(data) { + if (!data || data.length === 0) return; + + const svg = d3.select('#chart'); // Or pass a selector/element + svg.selectAll("*").remove(); // Clear previous render + + // 1. Define dimensions + const width = 800; + const height = 400; + const margin = { top: 20, right: 30, bottom: 40, left: 50 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // 2. Create main group with margins + const g = svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + // 3. Create scales + const xScale = d3.scaleLinear() + .domain([0, d3.max(data, d => d.x)]) + .range([0, innerWidth]); + + const yScale = d3.scaleLinear() + .domain([0, d3.max(data, d => d.y)]) + .range([innerHeight, 0]); // Note: inverted for SVG coordinates + + // 4. Create and append axes + const xAxis = d3.axisBottom(xScale); + const yAxis = d3.axisLeft(yScale); + + g.append("g") + .attr("transform", `translate(0,${innerHeight})`) + .call(xAxis); + + g.append("g") + .call(yAxis); + + // 5. Bind data and create visual elements + g.selectAll("circle") + .data(data) + .join("circle") + .attr("cx", d => xScale(d.x)) + .attr("cy", d => yScale(d.y)) + .attr("r", 5) + .attr("fill", "steelblue"); +} + +// Call when data changes +drawVisualization(myData); +``` + +### 4. Implement responsive sizing + +Make visualisations responsive to container size: + +```javascript +function setupResponsiveChart(containerId, data) { + const container = document.getElementById(containerId); + const svg = d3.select(`#${containerId}`).append('svg'); + + function updateChart() { + const { width, height } = container.getBoundingClientRect(); + svg.attr('width', width).attr('height', height); + + // Redraw visualisation with new dimensions + drawChart(data, svg, width, height); + } + + // Update on initial load + updateChart(); + + // Update on window resize + window.addEventListener('resize', updateChart); + + // Return cleanup function + return () => window.removeEventListener('resize', updateChart); +} + +// Usage: +// const cleanup = setupResponsiveChart('chart-container', myData); +// cleanup(); // Call when component unmounts or element removed +``` + +Or use ResizeObserver for more direct container monitoring: + +```javascript +function setupResponsiveChartWithObserver(svgElement, data) { + const observer = new ResizeObserver(() => { + const { width, height } = svgElement.getBoundingClientRect(); + d3.select(svgElement) + .attr('width', width) + .attr('height', height); + + // Redraw visualisation + drawChart(data, d3.select(svgElement), width, height); + }); + + observer.observe(svgElement.parentElement); + return () => observer.disconnect(); +} +``` + +## Common visualisation patterns + +### Bar chart + +```javascript +function drawBarChart(data, svgElement) { + if (!data || data.length === 0) return; + + const svg = d3.select(svgElement); + svg.selectAll("*").remove(); + + const width = 800; + const height = 400; + const margin = { top: 20, right: 30, bottom: 40, left: 50 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const g = svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + const xScale = d3.scaleBand() + .domain(data.map(d => d.category)) + .range([0, innerWidth]) + .padding(0.1); + + const yScale = d3.scaleLinear() + .domain([0, d3.max(data, d => d.value)]) + .range([innerHeight, 0]); + + g.append("g") + .attr("transform", `translate(0,${innerHeight})`) + .call(d3.axisBottom(xScale)); + + g.append("g") + .call(d3.axisLeft(yScale)); + + g.selectAll("rect") + .data(data) + .join("rect") + .attr("x", d => xScale(d.category)) + .attr("y", d => yScale(d.value)) + .attr("width", xScale.bandwidth()) + .attr("height", d => innerHeight - yScale(d.value)) + .attr("fill", "steelblue"); +} + +// Usage: +// drawBarChart(myData, document.getElementById('chart')); +``` + +### Line chart + +```javascript +const line = d3.line() + .x(d => xScale(d.date)) + .y(d => yScale(d.value)) + .curve(d3.curveMonotoneX); // Smooth curve + +g.append("path") + .datum(data) + .attr("fill", "none") + .attr("stroke", "steelblue") + .attr("stroke-width", 2) + .attr("d", line); +``` + +### Scatter plot + +```javascript +g.selectAll("circle") + .data(data) + .join("circle") + .attr("cx", d => xScale(d.x)) + .attr("cy", d => yScale(d.y)) + .attr("r", d => sizeScale(d.size)) // Optional: size encoding + .attr("fill", d => colourScale(d.category)) // Optional: colour encoding + .attr("opacity", 0.7); +``` + +### Chord diagram + +A chord diagram shows relationships between entities in a circular layout, with ribbons representing flows between them: + +```javascript +function drawChordDiagram(data) { + // data format: array of objects with source, target, and value + // Example: [{ source: 'A', target: 'B', value: 10 }, ...] + + if (!data || data.length === 0) return; + + const svg = d3.select('#chart'); + svg.selectAll("*").remove(); + + const width = 600; + const height = 600; + const innerRadius = Math.min(width, height) * 0.3; + const outerRadius = innerRadius + 30; + + // Create matrix from data + const nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target]))); + const matrix = Array.from({ length: nodes.length }, () => Array(nodes.length).fill(0)); + + data.forEach(d => { + const i = nodes.indexOf(d.source); + const j = nodes.indexOf(d.target); + matrix[i][j] += d.value; + matrix[j][i] += d.value; + }); + + // Create chord layout + const chord = d3.chord() + .padAngle(0.05) + .sortSubgroups(d3.descending); + + const arc = d3.arc() + .innerRadius(innerRadius) + .outerRadius(outerRadius); + + const ribbon = d3.ribbon() + .source(d => d.source) + .target(d => d.target); + + const colourScale = d3.scaleOrdinal(d3.schemeCategory10) + .domain(nodes); + + const g = svg.append("g") + .attr("transform", `translate(${width / 2},${height / 2})`); + + const chords = chord(matrix); + + // Draw ribbons + g.append("g") + .attr("fill-opacity", 0.67) + .selectAll("path") + .data(chords) + .join("path") + .attr("d", ribbon) + .attr("fill", d => colourScale(nodes[d.source.index])) + .attr("stroke", d => d3.rgb(colourScale(nodes[d.source.index])).darker()); + + // Draw groups (arcs) + const group = g.append("g") + .selectAll("g") + .data(chords.groups) + .join("g"); + + group.append("path") + .attr("d", arc) + .attr("fill", d => colourScale(nodes[d.index])) + .attr("stroke", d => d3.rgb(colourScale(nodes[d.index])).darker()); + + // Add labels + group.append("text") + .each(d => { d.angle = (d.startAngle + d.endAngle) / 2; }) + .attr("dy", "0.31em") + .attr("transform", d => `rotate(${(d.angle * 180 / Math.PI) - 90})translate(${outerRadius + 30})${d.angle > Math.PI ? "rotate(180)" : ""}`) + .attr("text-anchor", d => d.angle > Math.PI ? "end" : null) + .text((d, i) => nodes[i]) + .style("font-size", "12px"); +} +``` + +### Heatmap + +A heatmap uses colour to encode values in a two-dimensional grid, useful for showing patterns across categories: + +```javascript +function drawHeatmap(data) { + // data format: array of objects with row, column, and value + // Example: [{ row: 'A', column: 'X', value: 10 }, ...] + + if (!data || data.length === 0) return; + + const svg = d3.select('#chart'); + svg.selectAll("*").remove(); + + const width = 800; + const height = 600; + const margin = { top: 100, right: 30, bottom: 30, left: 100 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // Get unique rows and columns + const rows = Array.from(new Set(data.map(d => d.row))); + const columns = Array.from(new Set(data.map(d => d.column))); + + const g = svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + // Create scales + const xScale = d3.scaleBand() + .domain(columns) + .range([0, innerWidth]) + .padding(0.01); + + const yScale = d3.scaleBand() + .domain(rows) + .range([0, innerHeight]) + .padding(0.01); + + // Colour scale for values + const colourScale = d3.scaleSequential(d3.interpolateYlOrRd) + .domain([0, d3.max(data, d => d.value)]); + + // Draw rectangles + g.selectAll("rect") + .data(data) + .join("rect") + .attr("x", d => xScale(d.column)) + .attr("y", d => yScale(d.row)) + .attr("width", xScale.bandwidth()) + .attr("height", yScale.bandwidth()) + .attr("fill", d => colourScale(d.value)); + + // Add x-axis labels + svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`) + .selectAll("text") + .data(columns) + .join("text") + .attr("x", d => xScale(d) + xScale.bandwidth() / 2) + .attr("y", -10) + .attr("text-anchor", "middle") + .text(d => d) + .style("font-size", "12px"); + + // Add y-axis labels + svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`) + .selectAll("text") + .data(rows) + .join("text") + .attr("x", -10) + .attr("y", d => yScale(d) + yScale.bandwidth() / 2) + .attr("dy", "0.35em") + .attr("text-anchor", "end") + .text(d => d) + .style("font-size", "12px"); + + // Add colour legend + const legendWidth = 20; + const legendHeight = 200; + const legend = svg.append("g") + .attr("transform", `translate(${width - 60},${margin.top})`); + + const legendScale = d3.scaleLinear() + .domain(colourScale.domain()) + .range([legendHeight, 0]); + + const legendAxis = d3.axisRight(legendScale) + .ticks(5); + + // Draw colour gradient in legend + for (let i = 0; i < legendHeight; i++) { + legend.append("rect") + .attr("y", i) + .attr("width", legendWidth) + .attr("height", 1) + .attr("fill", colourScale(legendScale.invert(i))); + } + + legend.append("g") + .attr("transform", `translate(${legendWidth},0)`) + .call(legendAxis); +} +``` + +### Pie chart + +```javascript +const pie = d3.pie() + .value(d => d.value) + .sort(null); + +const arc = d3.arc() + .innerRadius(0) + .outerRadius(Math.min(width, height) / 2 - 20); + +const colourScale = d3.scaleOrdinal(d3.schemeCategory10); + +const g = svg.append("g") + .attr("transform", `translate(${width / 2},${height / 2})`); + +g.selectAll("path") + .data(pie(data)) + .join("path") + .attr("d", arc) + .attr("fill", (d, i) => colourScale(i)) + .attr("stroke", "white") + .attr("stroke-width", 2); +``` + +### Force-directed network + +```javascript +const simulation = d3.forceSimulation(nodes) + .force("link", d3.forceLink(links).id(d => d.id).distance(100)) + .force("charge", d3.forceManyBody().strength(-300)) + .force("center", d3.forceCenter(width / 2, height / 2)); + +const link = g.selectAll("line") + .data(links) + .join("line") + .attr("stroke", "#999") + .attr("stroke-width", 1); + +const node = g.selectAll("circle") + .data(nodes) + .join("circle") + .attr("r", 8) + .attr("fill", "steelblue") + .call(d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended)); + +simulation.on("tick", () => { + link + .attr("x1", d => d.source.x) + .attr("y1", d => d.source.y) + .attr("x2", d => d.target.x) + .attr("y2", d => d.target.y); + + node + .attr("cx", d => d.x) + .attr("cy", d => d.y); +}); + +function dragstarted(event) { + if (!event.active) simulation.alphaTarget(0.3).restart(); + event.subject.fx = event.subject.x; + event.subject.fy = event.subject.y; +} + +function dragged(event) { + event.subject.fx = event.x; + event.subject.fy = event.y; +} + +function dragended(event) { + if (!event.active) simulation.alphaTarget(0); + event.subject.fx = null; + event.subject.fy = null; +} +``` + +## Adding interactivity + +### Tooltips + +```javascript +// Create tooltip div (outside SVG) +const tooltip = d3.select("body").append("div") + .attr("class", "tooltip") + .style("position", "absolute") + .style("visibility", "hidden") + .style("background-color", "white") + .style("border", "1px solid #ddd") + .style("padding", "10px") + .style("border-radius", "4px") + .style("pointer-events", "none"); + +// Add to elements +circles + .on("mouseover", function(event, d) { + d3.select(this).attr("opacity", 1); + tooltip + .style("visibility", "visible") + .html(`${d.label}
Value: ${d.value}`); + }) + .on("mousemove", function(event) { + tooltip + .style("top", (event.pageY - 10) + "px") + .style("left", (event.pageX + 10) + "px"); + }) + .on("mouseout", function() { + d3.select(this).attr("opacity", 0.7); + tooltip.style("visibility", "hidden"); + }); +``` + +### Zoom and pan + +```javascript +const zoom = d3.zoom() + .scaleExtent([0.5, 10]) + .on("zoom", (event) => { + g.attr("transform", event.transform); + }); + +svg.call(zoom); +``` + +### Click interactions + +```javascript +circles + .on("click", function(event, d) { + // Handle click (dispatch event, update app state, etc.) + console.log("Clicked:", d); + + // Visual feedback + d3.selectAll("circle").attr("fill", "steelblue"); + d3.select(this).attr("fill", "orange"); + + // Optional: dispatch custom event for your framework/app to listen to + // window.dispatchEvent(new CustomEvent('chartClick', { detail: d })); + }); +``` + +## Transitions and animations + +Add smooth transitions to visual changes: + +```javascript +// Basic transition +circles + .transition() + .duration(750) + .attr("r", 10); + +// Chained transitions +circles + .transition() + .duration(500) + .attr("fill", "orange") + .transition() + .duration(500) + .attr("r", 15); + +// Staggered transitions +circles + .transition() + .delay((d, i) => i * 50) + .duration(500) + .attr("cy", d => yScale(d.value)); + +// Custom easing +circles + .transition() + .duration(1000) + .ease(d3.easeBounceOut) + .attr("r", 10); +``` + +## Scales reference + +### Quantitative scales + +```javascript +// Linear scale +const xScale = d3.scaleLinear() + .domain([0, 100]) + .range([0, 500]); + +// Log scale (for exponential data) +const logScale = d3.scaleLog() + .domain([1, 1000]) + .range([0, 500]); + +// Power scale +const powScale = d3.scalePow() + .exponent(2) + .domain([0, 100]) + .range([0, 500]); + +// Time scale +const timeScale = d3.scaleTime() + .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)]) + .range([0, 500]); +``` + +### Ordinal scales + +```javascript +// Band scale (for bar charts) +const bandScale = d3.scaleBand() + .domain(['A', 'B', 'C', 'D']) + .range([0, 400]) + .padding(0.1); + +// Point scale (for line/scatter categories) +const pointScale = d3.scalePoint() + .domain(['A', 'B', 'C', 'D']) + .range([0, 400]); + +// Ordinal scale (for colours) +const colourScale = d3.scaleOrdinal(d3.schemeCategory10); +``` + +### Sequential scales + +```javascript +// Sequential colour scale +const colourScale = d3.scaleSequential(d3.interpolateBlues) + .domain([0, 100]); + +// Diverging colour scale +const divScale = d3.scaleDiverging(d3.interpolateRdBu) + .domain([-10, 0, 10]); +``` + +## Best practices + +### Data preparation + +Always validate and prepare data before visualisation: + +```javascript +// Filter invalid values +const cleanData = data.filter(d => d.value != null && !isNaN(d.value)); + +// Sort data if order matters +const sortedData = [...data].sort((a, b) => b.value - a.value); + +// Parse dates +const parsedData = data.map(d => ({ + ...d, + date: d3.timeParse("%Y-%m-%d")(d.date) +})); +``` + +### Performance optimisation + +For large datasets (>1000 elements): + +```javascript +// Use canvas instead of SVG for many elements +// Use quadtree for collision detection +// Simplify paths with d3.line().curve(d3.curveStep) +// Implement virtual scrolling for large lists +// Use requestAnimationFrame for custom animations +``` + +### Accessibility + +Make visualisations accessible: + +```javascript +// Add ARIA labels +svg.attr("role", "img") + .attr("aria-label", "Bar chart showing quarterly revenue"); + +// Add title and description +svg.append("title").text("Quarterly Revenue 2024"); +svg.append("desc").text("Bar chart showing revenue growth across four quarters"); + +// Ensure sufficient colour contrast +// Provide keyboard navigation for interactive elements +// Include data table alternative +``` + +### Styling + +Use consistent, professional styling: + +```javascript +// Define colour palettes upfront +const colours = { + primary: '#4A90E2', + secondary: '#7B68EE', + background: '#F5F7FA', + text: '#333333', + gridLines: '#E0E0E0' +}; + +// Apply consistent typography +svg.selectAll("text") + .style("font-family", "Inter, sans-serif") + .style("font-size", "12px"); + +// Use subtle grid lines +g.selectAll(".tick line") + .attr("stroke", colours.gridLines) + .attr("stroke-dasharray", "2,2"); +``` + +## Common issues and solutions + +**Issue**: Axes not appearing +- Ensure scales have valid domains (check for NaN values) +- Verify axis is appended to correct group +- Check transform translations are correct + +**Issue**: Transitions not working +- Call `.transition()` before attribute changes +- Ensure elements have unique keys for proper data binding +- Check that useEffect dependencies include all changing data + +**Issue**: Responsive sizing not working +- Use ResizeObserver or window resize listener +- Update dimensions in state to trigger re-render +- Ensure SVG has width/height attributes or viewBox + +**Issue**: Performance problems +- Limit number of DOM elements (consider canvas for >1000 items) +- Debounce resize handlers +- Use `.join()` instead of separate enter/update/exit selections +- Avoid unnecessary re-renders by checking dependencies + +## Resources + +### references/ +Contains detailed reference materials: +- `d3-patterns.md` - Comprehensive collection of visualisation patterns and code examples +- `scale-reference.md` - Complete guide to d3 scales with examples +- `colour-schemes.md` - D3 colour schemes and palette recommendations + +### assets/ + +Contains boilerplate templates: + +- `chart-template.js` - Starter template for basic chart +- `interactive-template.js` - Template with tooltips, zoom, and interactions +- `sample-data.json` - Example datasets for testing + +These templates work with vanilla JavaScript, React, Vue, Svelte, or any other JavaScript environment. Adapt them as needed for your specific framework. + +To use these resources, read the relevant files when detailed guidance is needed for specific visualisation types or patterns. diff --git a/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/assets/chart-template.jsx b/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/assets/chart-template.jsx new file mode 100644 index 00000000..64ca0ac2 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/assets/chart-template.jsx @@ -0,0 +1,106 @@ +import { useEffect, useRef, useState } from 'react'; +import * as d3 from 'd3'; + +function BasicChart({ data }) { + const svgRef = useRef(); + + useEffect(() => { + if (!data || data.length === 0) return; + + // Select SVG element + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); // Clear previous content + + // Define dimensions and margins + const width = 800; + const height = 400; + const margin = { top: 20, right: 30, bottom: 40, left: 50 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // Create main group with margins + const g = svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + // Create scales + const xScale = d3.scaleBand() + .domain(data.map(d => d.label)) + .range([0, innerWidth]) + .padding(0.1); + + const yScale = d3.scaleLinear() + .domain([0, d3.max(data, d => d.value)]) + .range([innerHeight, 0]) + .nice(); + + // Create and append axes + const xAxis = d3.axisBottom(xScale); + const yAxis = d3.axisLeft(yScale); + + g.append("g") + .attr("class", "x-axis") + .attr("transform", `translate(0,${innerHeight})`) + .call(xAxis); + + g.append("g") + .attr("class", "y-axis") + .call(yAxis); + + // Bind data and create visual elements (bars in this example) + g.selectAll("rect") + .data(data) + .join("rect") + .attr("x", d => xScale(d.label)) + .attr("y", d => yScale(d.value)) + .attr("width", xScale.bandwidth()) + .attr("height", d => innerHeight - yScale(d.value)) + .attr("fill", "steelblue"); + + // Optional: Add axis labels + g.append("text") + .attr("class", "axis-label") + .attr("x", innerWidth / 2) + .attr("y", innerHeight + margin.bottom - 5) + .attr("text-anchor", "middle") + .text("Category"); + + g.append("text") + .attr("class", "axis-label") + .attr("transform", "rotate(-90)") + .attr("x", -innerHeight / 2) + .attr("y", -margin.left + 15) + .attr("text-anchor", "middle") + .text("Value"); + + }, [data]); + + return ( +
+ +
+ ); +} + +// Example usage +export default function App() { + const sampleData = [ + { label: 'A', value: 30 }, + { label: 'B', value: 80 }, + { label: 'C', value: 45 }, + { label: 'D', value: 60 }, + { label: 'E', value: 20 }, + { label: 'F', value: 90 } + ]; + + return ( +
+

Basic D3.js Chart

+ +
+ ); +} diff --git a/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/assets/interactive-template.jsx b/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/assets/interactive-template.jsx new file mode 100644 index 00000000..31138d5e --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/assets/interactive-template.jsx @@ -0,0 +1,227 @@ +import { useEffect, useRef, useState } from 'react'; +import * as d3 from 'd3'; + +function InteractiveChart({ data }) { + const svgRef = useRef(); + const tooltipRef = useRef(); + const [selectedPoint, setSelectedPoint] = useState(null); + + useEffect(() => { + if (!data || data.length === 0) return; + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + // Dimensions + const width = 800; + const height = 500; + const margin = { top: 20, right: 30, bottom: 40, left: 50 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // Create main group + const g = svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + // Scales + const xScale = d3.scaleLinear() + .domain([0, d3.max(data, d => d.x)]) + .range([0, innerWidth]) + .nice(); + + const yScale = d3.scaleLinear() + .domain([0, d3.max(data, d => d.y)]) + .range([innerHeight, 0]) + .nice(); + + const sizeScale = d3.scaleSqrt() + .domain([0, d3.max(data, d => d.size || 10)]) + .range([3, 20]); + + const colourScale = d3.scaleOrdinal(d3.schemeCategory10); + + // Add zoom behaviour + const zoom = d3.zoom() + .scaleExtent([0.5, 10]) + .on("zoom", (event) => { + g.attr("transform", `translate(${margin.left + event.transform.x},${margin.top + event.transform.y}) scale(${event.transform.k})`); + }); + + svg.call(zoom); + + // Axes + const xAxis = d3.axisBottom(xScale); + const yAxis = d3.axisLeft(yScale); + + const xAxisGroup = g.append("g") + .attr("class", "x-axis") + .attr("transform", `translate(0,${innerHeight})`) + .call(xAxis); + + const yAxisGroup = g.append("g") + .attr("class", "y-axis") + .call(yAxis); + + // Grid lines + g.append("g") + .attr("class", "grid") + .attr("opacity", 0.1) + .call(d3.axisLeft(yScale) + .tickSize(-innerWidth) + .tickFormat("")); + + g.append("g") + .attr("class", "grid") + .attr("opacity", 0.1) + .attr("transform", `translate(0,${innerHeight})`) + .call(d3.axisBottom(xScale) + .tickSize(-innerHeight) + .tickFormat("")); + + // Tooltip + const tooltip = d3.select(tooltipRef.current); + + // Data points + const circles = g.selectAll("circle") + .data(data) + .join("circle") + .attr("cx", d => xScale(d.x)) + .attr("cy", d => yScale(d.y)) + .attr("r", d => sizeScale(d.size || 10)) + .attr("fill", d => colourScale(d.category || 'default')) + .attr("stroke", "#fff") + .attr("stroke-width", 2) + .attr("opacity", 0.7) + .style("cursor", "pointer"); + + // Hover interactions + circles + .on("mouseover", function(event, d) { + // Enlarge circle + d3.select(this) + .transition() + .duration(200) + .attr("opacity", 1) + .attr("stroke-width", 3); + + // Show tooltip + tooltip + .style("display", "block") + .style("left", (event.pageX + 10) + "px") + .style("top", (event.pageY - 10) + "px") + .html(` + ${d.label || 'Point'}
+ X: ${d.x.toFixed(2)}
+ Y: ${d.y.toFixed(2)}
+ ${d.category ? `Category: ${d.category}
` : ''} + ${d.size ? `Size: ${d.size.toFixed(2)}` : ''} + `); + }) + .on("mousemove", function(event) { + tooltip + .style("left", (event.pageX + 10) + "px") + .style("top", (event.pageY - 10) + "px"); + }) + .on("mouseout", function() { + // Restore circle + d3.select(this) + .transition() + .duration(200) + .attr("opacity", 0.7) + .attr("stroke-width", 2); + + // Hide tooltip + tooltip.style("display", "none"); + }) + .on("click", function(event, d) { + // Highlight selected point + circles.attr("stroke", "#fff").attr("stroke-width", 2); + d3.select(this) + .attr("stroke", "#000") + .attr("stroke-width", 3); + + setSelectedPoint(d); + }); + + // Add transition on initial render + circles + .attr("r", 0) + .transition() + .duration(800) + .delay((d, i) => i * 20) + .attr("r", d => sizeScale(d.size || 10)); + + // Axis labels + g.append("text") + .attr("class", "axis-label") + .attr("x", innerWidth / 2) + .attr("y", innerHeight + margin.bottom - 5) + .attr("text-anchor", "middle") + .style("font-size", "14px") + .text("X Axis"); + + g.append("text") + .attr("class", "axis-label") + .attr("transform", "rotate(-90)") + .attr("x", -innerHeight / 2) + .attr("y", -margin.left + 15) + .attr("text-anchor", "middle") + .style("font-size", "14px") + .text("Y Axis"); + + }, [data]); + + return ( +
+ +
+ {selectedPoint && ( +
+

Selected Point

+
{JSON.stringify(selectedPoint, null, 2)}
+
+ )} +
+ ); +} + +// Example usage +export default function App() { + const sampleData = Array.from({ length: 50 }, (_, i) => ({ + id: i, + label: `Point ${i + 1}`, + x: Math.random() * 100, + y: Math.random() * 100, + size: Math.random() * 30 + 5, + category: ['A', 'B', 'C', 'D'][Math.floor(Math.random() * 4)] + })); + + return ( +
+

Interactive D3.js Chart

+

+ Hover over points for details. Click to select. Scroll to zoom. Drag to pan. +

+ +
+ ); +} diff --git a/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/assets/sample-data.json b/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/assets/sample-data.json new file mode 100644 index 00000000..10189248 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/assets/sample-data.json @@ -0,0 +1,115 @@ +{ + "timeSeries": [ + { "date": "2024-01-01", "value": 120, "category": "A" }, + { "date": "2024-02-01", "value": 135, "category": "A" }, + { "date": "2024-03-01", "value": 128, "category": "A" }, + { "date": "2024-04-01", "value": 145, "category": "A" }, + { "date": "2024-05-01", "value": 152, "category": "A" }, + { "date": "2024-06-01", "value": 168, "category": "A" }, + { "date": "2024-07-01", "value": 175, "category": "A" }, + { "date": "2024-08-01", "value": 182, "category": "A" }, + { "date": "2024-09-01", "value": 190, "category": "A" }, + { "date": "2024-10-01", "value": 185, "category": "A" }, + { "date": "2024-11-01", "value": 195, "category": "A" }, + { "date": "2024-12-01", "value": 210, "category": "A" } + ], + + "categorical": [ + { "label": "Product A", "value": 450, "category": "Electronics" }, + { "label": "Product B", "value": 320, "category": "Electronics" }, + { "label": "Product C", "value": 580, "category": "Clothing" }, + { "label": "Product D", "value": 290, "category": "Clothing" }, + { "label": "Product E", "value": 410, "category": "Food" }, + { "label": "Product F", "value": 370, "category": "Food" } + ], + + "scatterData": [ + { "x": 12, "y": 45, "size": 25, "category": "Group A", "label": "Point 1" }, + { "x": 25, "y": 62, "size": 35, "category": "Group A", "label": "Point 2" }, + { "x": 38, "y": 55, "size": 20, "category": "Group B", "label": "Point 3" }, + { "x": 45, "y": 78, "size": 40, "category": "Group B", "label": "Point 4" }, + { "x": 52, "y": 68, "size": 30, "category": "Group C", "label": "Point 5" }, + { "x": 65, "y": 85, "size": 45, "category": "Group C", "label": "Point 6" }, + { "x": 72, "y": 72, "size": 28, "category": "Group A", "label": "Point 7" }, + { "x": 85, "y": 92, "size": 50, "category": "Group B", "label": "Point 8" } + ], + + "hierarchical": { + "name": "Root", + "children": [ + { + "name": "Category 1", + "children": [ + { "name": "Item 1.1", "value": 100 }, + { "name": "Item 1.2", "value": 150 }, + { "name": "Item 1.3", "value": 80 } + ] + }, + { + "name": "Category 2", + "children": [ + { "name": "Item 2.1", "value": 200 }, + { "name": "Item 2.2", "value": 120 }, + { "name": "Item 2.3", "value": 90 } + ] + }, + { + "name": "Category 3", + "children": [ + { "name": "Item 3.1", "value": 180 }, + { "name": "Item 3.2", "value": 140 } + ] + } + ] + }, + + "network": { + "nodes": [ + { "id": "A", "group": 1 }, + { "id": "B", "group": 1 }, + { "id": "C", "group": 1 }, + { "id": "D", "group": 2 }, + { "id": "E", "group": 2 }, + { "id": "F", "group": 3 }, + { "id": "G", "group": 3 }, + { "id": "H", "group": 3 } + ], + "links": [ + { "source": "A", "target": "B", "value": 1 }, + { "source": "A", "target": "C", "value": 2 }, + { "source": "B", "target": "C", "value": 1 }, + { "source": "C", "target": "D", "value": 3 }, + { "source": "D", "target": "E", "value": 2 }, + { "source": "E", "target": "F", "value": 1 }, + { "source": "F", "target": "G", "value": 2 }, + { "source": "F", "target": "H", "value": 1 }, + { "source": "G", "target": "H", "value": 1 } + ] + }, + + "stackedData": [ + { "group": "Q1", "seriesA": 30, "seriesB": 40, "seriesC": 25 }, + { "group": "Q2", "seriesA": 45, "seriesB": 35, "seriesC": 30 }, + { "group": "Q3", "seriesA": 40, "seriesB": 50, "seriesC": 35 }, + { "group": "Q4", "seriesA": 55, "seriesB": 45, "seriesC": 40 } + ], + + "geographicPoints": [ + { "city": "London", "latitude": 51.5074, "longitude": -0.1278, "value": 8900000 }, + { "city": "Paris", "latitude": 48.8566, "longitude": 2.3522, "value": 2140000 }, + { "city": "Berlin", "latitude": 52.5200, "longitude": 13.4050, "value": 3645000 }, + { "city": "Madrid", "latitude": 40.4168, "longitude": -3.7038, "value": 3223000 }, + { "city": "Rome", "latitude": 41.9028, "longitude": 12.4964, "value": 2873000 } + ], + + "divergingData": [ + { "category": "Item A", "value": -15 }, + { "category": "Item B", "value": 8 }, + { "category": "Item C", "value": -22 }, + { "category": "Item D", "value": 18 }, + { "category": "Item E", "value": -5 }, + { "category": "Item F", "value": 25 }, + { "category": "Item G", "value": -12 }, + { "category": "Item H", "value": 14 } + ] +} diff --git a/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/references/colour-schemes.md b/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/references/colour-schemes.md new file mode 100644 index 00000000..12394e9d --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/references/colour-schemes.md @@ -0,0 +1,564 @@ +# D3.js Colour Schemes and Palette Recommendations + +Comprehensive guide to colour selection in data visualisation with d3.js. + +## Built-in categorical colour schemes + +### Category10 (default) + +```javascript +d3.schemeCategory10 +// ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', +// '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'] +``` + +**Characteristics:** +- 10 distinct colours +- Good colour-blind accessibility +- Default choice for most categorical data +- Balanced saturation and brightness + +**Use cases:** General purpose categorical encoding, legend items, multiple data series + +### Tableau10 + +```javascript +d3.schemeTableau10 +``` + +**Characteristics:** +- 10 colours optimised for data visualisation +- Professional appearance +- Excellent distinguishability + +**Use cases:** Business dashboards, professional reports, presentations + +### Accent + +```javascript +d3.schemeAccent +// 8 colours with high saturation +``` + +**Characteristics:** +- Bright, vibrant colours +- High contrast +- Modern aesthetic + +**Use cases:** Highlighting important categories, modern web applications + +### Dark2 + +```javascript +d3.schemeDark2 +// 8 darker, muted colours +``` + +**Characteristics:** +- Subdued palette +- Professional appearance +- Good for dark backgrounds + +**Use cases:** Dark mode visualisations, professional contexts + +### Paired + +```javascript +d3.schemePaired +// 12 colours in pairs of similar hues +``` + +**Characteristics:** +- Pairs of light and dark variants +- Useful for nested categories +- 12 distinct colours + +**Use cases:** Grouped bar charts, hierarchical categories, before/after comparisons + +### Pastel1 & Pastel2 + +```javascript +d3.schemePastel1 // 9 colours +d3.schemePastel2 // 8 colours +``` + +**Characteristics:** +- Soft, low-saturation colours +- Gentle appearance +- Good for large areas + +**Use cases:** Background colours, subtle categorisation, calming visualisations + +### Set1, Set2, Set3 + +```javascript +d3.schemeSet1 // 9 colours - vivid +d3.schemeSet2 // 8 colours - muted +d3.schemeSet3 // 12 colours - pastel +``` + +**Characteristics:** +- Set1: High saturation, maximum distinction +- Set2: Professional, balanced +- Set3: Subtle, many categories + +**Use cases:** Varied based on visual hierarchy needs + +## Sequential colour schemes + +Sequential schemes map continuous data from low to high values using a single hue or gradient. + +### Single-hue sequential + +**Blues:** +```javascript +d3.interpolateBlues +d3.schemeBlues[9] // 9-step discrete version +``` + +**Other single-hue options:** +- `d3.interpolateGreens` / `d3.schemeGreens` +- `d3.interpolateOranges` / `d3.schemeOranges` +- `d3.interpolatePurples` / `d3.schemePurples` +- `d3.interpolateReds` / `d3.schemeReds` +- `d3.interpolateGreys` / `d3.schemeGreys` + +**Use cases:** +- Simple heat maps +- Choropleth maps +- Density plots +- Single-metric visualisations + +### Multi-hue sequential + +**Viridis (recommended):** +```javascript +d3.interpolateViridis +``` + +**Characteristics:** +- Perceptually uniform +- Colour-blind friendly +- Print-safe +- No visual dead zones +- Monotonically increasing perceived lightness + +**Other perceptually-uniform options:** +- `d3.interpolatePlasma` - Purple to yellow +- `d3.interpolateInferno` - Black to white through red/orange +- `d3.interpolateMagma` - Black to white through purple +- `d3.interpolateCividis` - Colour-blind optimised + +**Colour-blind accessible:** +```javascript +d3.interpolateTurbo // Rainbow-like but perceptually uniform +d3.interpolateCool // Cyan to magenta +d3.interpolateWarm // Orange to yellow +``` + +**Use cases:** +- Scientific visualisation +- Medical imaging +- Any high-precision data visualisation +- Accessible visualisations + +### Traditional sequential + +**Yellow-Orange-Red:** +```javascript +d3.interpolateYlOrRd +d3.schemeYlOrRd[9] +``` + +**Yellow-Green-Blue:** +```javascript +d3.interpolateYlGnBu +d3.schemeYlGnBu[9] +``` + +**Other multi-hue:** +- `d3.interpolateBuGn` - Blue to green +- `d3.interpolateBuPu` - Blue to purple +- `d3.interpolateGnBu` - Green to blue +- `d3.interpolateOrRd` - Orange to red +- `d3.interpolatePuBu` - Purple to blue +- `d3.interpolatePuBuGn` - Purple to blue-green +- `d3.interpolatePuRd` - Purple to red +- `d3.interpolateRdPu` - Red to purple +- `d3.interpolateYlGn` - Yellow to green +- `d3.interpolateYlOrBr` - Yellow to orange-brown + +**Use cases:** Traditional data visualisation, familiar colour associations (temperature, vegetation, water) + +## Diverging colour schemes + +Diverging schemes highlight deviations from a central value using two distinct hues. + +### Red-Blue (temperature) + +```javascript +d3.interpolateRdBu +d3.schemeRdBu[11] +``` + +**Characteristics:** +- Intuitive temperature metaphor +- Strong contrast +- Clear positive/negative distinction + +**Use cases:** Temperature, profit/loss, above/below average, correlation + +### Red-Yellow-Blue + +```javascript +d3.interpolateRdYlBu +d3.schemeRdYlBu[11] +``` + +**Characteristics:** +- Three-colour gradient +- Softer transition through yellow +- More visual steps + +**Use cases:** When extreme values need emphasis and middle needs visibility + +### Other diverging schemes + +**Traffic light:** +```javascript +d3.interpolateRdYlGn // Red (bad) to green (good) +``` + +**Spectral (rainbow):** +```javascript +d3.interpolateSpectral // Full spectrum +``` + +**Other options:** +- `d3.interpolateBrBG` - Brown to blue-green +- `d3.interpolatePiYG` - Pink to yellow-green +- `d3.interpolatePRGn` - Purple to green +- `d3.interpolatePuOr` - Purple to orange +- `d3.interpolateRdGy` - Red to grey + +**Use cases:** Choose based on semantic meaning and accessibility needs + +## Colour-blind friendly palettes + +### General guidelines + +1. **Avoid red-green combinations** (most common colour blindness) +2. **Use blue-orange diverging** instead of red-green +3. **Add texture or patterns** as redundant encoding +4. **Test with simulation tools** + +### Recommended colour-blind safe schemes + +**Categorical:** +```javascript +// Okabe-Ito palette (colour-blind safe) +const okabePalette = [ + '#E69F00', // Orange + '#56B4E9', // Sky blue + '#009E73', // Bluish green + '#F0E442', // Yellow + '#0072B2', // Blue + '#D55E00', // Vermillion + '#CC79A7', // Reddish purple + '#000000' // Black +]; + +const colourScale = d3.scaleOrdinal() + .domain(categories) + .range(okabePalette); +``` + +**Sequential:** +```javascript +// Use Viridis, Cividis, or Blues +d3.interpolateViridis // Best overall +d3.interpolateCividis // Optimised for CVD +d3.interpolateBlues // Simple, safe +``` + +**Diverging:** +```javascript +// Use blue-orange instead of red-green +d3.interpolateBrBG +d3.interpolatePuOr +``` + +## Custom colour palettes + +### Creating custom sequential + +```javascript +const customSequential = d3.scaleLinear() + .domain([0, 100]) + .range(['#e8f4f8', '#006d9c']) // Light to dark blue + .interpolate(d3.interpolateLab); // Perceptually uniform +``` + +### Creating custom diverging + +```javascript +const customDiverging = d3.scaleLinear() + .domain([0, 50, 100]) + .range(['#ca0020', '#f7f7f7', '#0571b0']) // Red, grey, blue + .interpolate(d3.interpolateLab); +``` + +### Creating custom categorical + +```javascript +// Brand colours +const brandPalette = [ + '#FF6B6B', // Primary red + '#4ECDC4', // Secondary teal + '#45B7D1', // Tertiary blue + '#FFA07A', // Accent coral + '#98D8C8' // Accent mint +]; + +const colourScale = d3.scaleOrdinal() + .domain(categories) + .range(brandPalette); +``` + +## Semantic colour associations + +### Universal colour meanings + +**Red:** +- Danger, error, negative +- High temperature +- Debt, loss + +**Green:** +- Success, positive +- Growth, vegetation +- Profit, gain + +**Blue:** +- Trust, calm +- Water, cold +- Information, neutral + +**Yellow/Orange:** +- Warning, caution +- Energy, warmth +- Attention + +**Grey:** +- Neutral, inactive +- Missing data +- Background + +### Context-specific palettes + +**Financial:** +```javascript +const financialColours = { + profit: '#27ae60', + loss: '#e74c3c', + neutral: '#95a5a6', + highlight: '#3498db' +}; +``` + +**Temperature:** +```javascript +const temperatureScale = d3.scaleSequential(d3.interpolateRdYlBu) + .domain([40, -10]); // Hot to cold (reversed) +``` + +**Traffic/Status:** +```javascript +const statusColours = { + success: '#27ae60', + warning: '#f39c12', + error: '#e74c3c', + info: '#3498db', + neutral: '#95a5a6' +}; +``` + +## Accessibility best practices + +### Contrast ratios + +Ensure sufficient contrast between colours and backgrounds: + +```javascript +// Good contrast example +const highContrast = { + background: '#ffffff', + text: '#2c3e50', + primary: '#3498db', + secondary: '#e74c3c' +}; +``` + +**WCAG guidelines:** +- Normal text: 4.5:1 minimum +- Large text: 3:1 minimum +- UI components: 3:1 minimum + +### Redundant encoding + +Never rely solely on colour to convey information: + +```javascript +// Add patterns or shapes +const symbols = ['circle', 'square', 'triangle', 'diamond']; + +// Add text labels +// Use line styles (solid, dashed, dotted) +// Use size encoding +``` + +### Testing + +Test visualisations for colour blindness: +- Chrome DevTools (Rendering > Emulate vision deficiencies) +- Colour Oracle (free desktop application) +- Coblis (online simulator) + +## Professional colour recommendations + +### Data journalism + +```javascript +// Guardian style +const guardianPalette = [ + '#005689', // Guardian blue + '#c70000', // Guardian red + '#7d0068', // Guardian pink + '#951c75', // Guardian purple +]; + +// FT style +const ftPalette = [ + '#0f5499', // FT blue + '#990f3d', // FT red + '#593380', // FT purple + '#262a33', // FT black +]; +``` + +### Academic/Scientific + +```javascript +// Nature journal style +const naturePalette = [ + '#0071b2', // Blue + '#d55e00', // Vermillion + '#009e73', // Green + '#f0e442', // Yellow +]; + +// Use Viridis for continuous data +const scientificScale = d3.scaleSequential(d3.interpolateViridis); +``` + +### Corporate/Business + +```javascript +// Professional, conservative +const corporatePalette = [ + '#003f5c', // Dark blue + '#58508d', // Purple + '#bc5090', // Magenta + '#ff6361', // Coral + '#ffa600' // Orange +]; +``` + +## Dynamic colour selection + +### Based on data range + +```javascript +function selectColourScheme(data) { + const extent = d3.extent(data); + const hasNegative = extent[0] < 0; + const hasPositive = extent[1] > 0; + + if (hasNegative && hasPositive) { + // Diverging: data crosses zero + return d3.scaleSequentialSymlog(d3.interpolateRdBu) + .domain([extent[0], 0, extent[1]]); + } else { + // Sequential: all positive or all negative + return d3.scaleSequential(d3.interpolateViridis) + .domain(extent); + } +} +``` + +### Based on category count + +```javascript +function selectCategoricalScheme(categories) { + const n = categories.length; + + if (n <= 10) { + return d3.scaleOrdinal(d3.schemeTableau10); + } else if (n <= 12) { + return d3.scaleOrdinal(d3.schemePaired); + } else { + // For many categories, use sequential with quantize + return d3.scaleQuantize() + .domain([0, n - 1]) + .range(d3.quantize(d3.interpolateRainbow, n)); + } +} +``` + +## Common colour mistakes to avoid + +1. **Rainbow gradients for sequential data** + - Problem: Not perceptually uniform, hard to read + - Solution: Use Viridis, Blues, or other uniform schemes + +2. **Red-green for diverging (colour blindness)** + - Problem: 8% of males can't distinguish + - Solution: Use blue-orange or purple-green + +3. **Too many categorical colours** + - Problem: Hard to distinguish and remember + - Solution: Limit to 5-8 categories, use grouping + +4. **Insufficient contrast** + - Problem: Poor readability + - Solution: Test contrast ratios, use darker colours on light backgrounds + +5. **Culturally inconsistent colours** + - Problem: Confusing semantic meaning + - Solution: Research colour associations for target audience + +6. **Inverted temperature scales** + - Problem: Counterintuitive (red = cold) + - Solution: Red/orange = hot, blue = cold + +## Quick reference guide + +**Need to show...** + +- **Categories (โ‰ค10):** `d3.schemeCategory10` or `d3.schemeTableau10` +- **Categories (>10):** `d3.schemePaired` or group categories +- **Sequential (general):** `d3.interpolateViridis` +- **Sequential (scientific):** `d3.interpolateViridis` or `d3.interpolatePlasma` +- **Sequential (temperature):** `d3.interpolateRdYlBu` (inverted) +- **Diverging (zero):** `d3.interpolateRdBu` or `d3.interpolateBrBG` +- **Diverging (good/bad):** `d3.interpolateRdYlGn` (inverted) +- **Colour-blind safe (categorical):** Okabe-Ito palette (shown above) +- **Colour-blind safe (sequential):** `d3.interpolateCividis` or `d3.interpolateBlues` +- **Colour-blind safe (diverging):** `d3.interpolatePuOr` or `d3.interpolateBrBG` + +**Always remember:** +1. Test for colour-blindness +2. Ensure sufficient contrast +3. Use semantic colours appropriately +4. Add redundant encoding (patterns, labels) +5. Keep it simple (fewer colours = clearer visualisation) \ No newline at end of file diff --git a/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/references/d3-patterns.md b/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/references/d3-patterns.md new file mode 100644 index 00000000..0b36a0b5 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/references/d3-patterns.md @@ -0,0 +1,869 @@ +# D3.js Visualisation Patterns + +This reference provides detailed code patterns for common d3.js visualisation types. + +## Hierarchical visualisations + +### Tree diagram + +```javascript +useEffect(() => { + if (!data) return; + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + const width = 800; + const height = 600; + + const tree = d3.tree().size([height - 100, width - 200]); + + const root = d3.hierarchy(data); + tree(root); + + const g = svg.append("g") + .attr("transform", "translate(100,50)"); + + // Links + g.selectAll("path") + .data(root.links()) + .join("path") + .attr("d", d3.linkHorizontal() + .x(d => d.y) + .y(d => d.x)) + .attr("fill", "none") + .attr("stroke", "#555") + .attr("stroke-width", 2); + + // Nodes + const node = g.selectAll("g") + .data(root.descendants()) + .join("g") + .attr("transform", d => `translate(${d.y},${d.x})`); + + node.append("circle") + .attr("r", 6) + .attr("fill", d => d.children ? "#555" : "#999"); + + node.append("text") + .attr("dy", "0.31em") + .attr("x", d => d.children ? -8 : 8) + .attr("text-anchor", d => d.children ? "end" : "start") + .text(d => d.data.name) + .style("font-size", "12px"); + +}, [data]); +``` + +### Treemap + +```javascript +useEffect(() => { + if (!data) return; + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + const width = 800; + const height = 600; + + const root = d3.hierarchy(data) + .sum(d => d.value) + .sort((a, b) => b.value - a.value); + + d3.treemap() + .size([width, height]) + .padding(2) + .round(true)(root); + + const colourScale = d3.scaleOrdinal(d3.schemeCategory10); + + const cell = svg.selectAll("g") + .data(root.leaves()) + .join("g") + .attr("transform", d => `translate(${d.x0},${d.y0})`); + + cell.append("rect") + .attr("width", d => d.x1 - d.x0) + .attr("height", d => d.y1 - d.y0) + .attr("fill", d => colourScale(d.parent.data.name)) + .attr("stroke", "white") + .attr("stroke-width", 2); + + cell.append("text") + .attr("x", 4) + .attr("y", 16) + .text(d => d.data.name) + .style("font-size", "12px") + .style("fill", "white"); + +}, [data]); +``` + +### Sunburst diagram + +```javascript +useEffect(() => { + if (!data) return; + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + const width = 600; + const height = 600; + const radius = Math.min(width, height) / 2; + + const root = d3.hierarchy(data) + .sum(d => d.value) + .sort((a, b) => b.value - a.value); + + const partition = d3.partition() + .size([2 * Math.PI, radius]); + + partition(root); + + const arc = d3.arc() + .startAngle(d => d.x0) + .endAngle(d => d.x1) + .innerRadius(d => d.y0) + .outerRadius(d => d.y1); + + const colourScale = d3.scaleOrdinal(d3.schemeCategory10); + + const g = svg.append("g") + .attr("transform", `translate(${width / 2},${height / 2})`); + + g.selectAll("path") + .data(root.descendants()) + .join("path") + .attr("d", arc) + .attr("fill", d => colourScale(d.depth)) + .attr("stroke", "white") + .attr("stroke-width", 1); + +}, [data]); +``` + +### Chord diagram + +```javascript +function drawChordDiagram(data) { + // data format: array of objects with source, target, and value + // Example: [{ source: 'A', target: 'B', value: 10 }, ...] + + if (!data || data.length === 0) return; + + const svg = d3.select('#chart'); + svg.selectAll("*").remove(); + + const width = 600; + const height = 600; + const innerRadius = Math.min(width, height) * 0.3; + const outerRadius = innerRadius + 30; + + // Create matrix from data + const nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target]))); + const matrix = Array.from({ length: nodes.length }, () => Array(nodes.length).fill(0)); + + data.forEach(d => { + const i = nodes.indexOf(d.source); + const j = nodes.indexOf(d.target); + matrix[i][j] += d.value; + matrix[j][i] += d.value; + }); + + // Create chord layout + const chord = d3.chord() + .padAngle(0.05) + .sortSubgroups(d3.descending); + + const arc = d3.arc() + .innerRadius(innerRadius) + .outerRadius(outerRadius); + + const ribbon = d3.ribbon() + .source(d => d.source) + .target(d => d.target); + + const colourScale = d3.scaleOrdinal(d3.schemeCategory10) + .domain(nodes); + + const g = svg.append("g") + .attr("transform", `translate(${width / 2},${height / 2})`); + + const chords = chord(matrix); + + // Draw ribbons + g.append("g") + .attr("fill-opacity", 0.67) + .selectAll("path") + .data(chords) + .join("path") + .attr("d", ribbon) + .attr("fill", d => colourScale(nodes[d.source.index])) + .attr("stroke", d => d3.rgb(colourScale(nodes[d.source.index])).darker()); + + // Draw groups (arcs) + const group = g.append("g") + .selectAll("g") + .data(chords.groups) + .join("g"); + + group.append("path") + .attr("d", arc) + .attr("fill", d => colourScale(nodes[d.index])) + .attr("stroke", d => d3.rgb(colourScale(nodes[d.index])).darker()); + + // Add labels + group.append("text") + .each(d => { d.angle = (d.startAngle + d.endAngle) / 2; }) + .attr("dy", "0.31em") + .attr("transform", d => `rotate(${(d.angle * 180 / Math.PI) - 90})translate(${outerRadius + 30})${d.angle > Math.PI ? "rotate(180)" : ""}`) + .attr("text-anchor", d => d.angle > Math.PI ? "end" : null) + .text((d, i) => nodes[i]) + .style("font-size", "12px"); +} + +// Data format example: +// const data = [ +// { source: 'Category A', target: 'Category B', value: 100 }, +// { source: 'Category A', target: 'Category C', value: 50 }, +// { source: 'Category B', target: 'Category C', value: 75 } +// ]; +// drawChordDiagram(data); +``` + +## Advanced chart types + +### Heatmap + +```javascript +function drawHeatmap(data) { + // data format: array of objects with row, column, and value + // Example: [{ row: 'A', column: 'X', value: 10 }, ...] + + if (!data || data.length === 0) return; + + const svg = d3.select('#chart'); + svg.selectAll("*").remove(); + + const width = 800; + const height = 600; + const margin = { top: 100, right: 30, bottom: 30, left: 100 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // Get unique rows and columns + const rows = Array.from(new Set(data.map(d => d.row))); + const columns = Array.from(new Set(data.map(d => d.column))); + + const g = svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + // Create scales + const xScale = d3.scaleBand() + .domain(columns) + .range([0, innerWidth]) + .padding(0.01); + + const yScale = d3.scaleBand() + .domain(rows) + .range([0, innerHeight]) + .padding(0.01); + + // Colour scale for values (sequential from light to dark red) + const colourScale = d3.scaleSequential(d3.interpolateYlOrRd) + .domain([0, d3.max(data, d => d.value)]); + + // Draw rectangles + g.selectAll("rect") + .data(data) + .join("rect") + .attr("x", d => xScale(d.column)) + .attr("y", d => yScale(d.row)) + .attr("width", xScale.bandwidth()) + .attr("height", yScale.bandwidth()) + .attr("fill", d => colourScale(d.value)); + + // Add x-axis labels + svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`) + .selectAll("text") + .data(columns) + .join("text") + .attr("x", d => xScale(d) + xScale.bandwidth() / 2) + .attr("y", -10) + .attr("text-anchor", "middle") + .text(d => d) + .style("font-size", "12px"); + + // Add y-axis labels + svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`) + .selectAll("text") + .data(rows) + .join("text") + .attr("x", -10) + .attr("y", d => yScale(d) + yScale.bandwidth() / 2) + .attr("dy", "0.35em") + .attr("text-anchor", "end") + .text(d => d) + .style("font-size", "12px"); + + // Add colour legend + const legendWidth = 20; + const legendHeight = 200; + const legend = svg.append("g") + .attr("transform", `translate(${width - 60},${margin.top})`); + + const legendScale = d3.scaleLinear() + .domain(colourScale.domain()) + .range([legendHeight, 0]); + + const legendAxis = d3.axisRight(legendScale).ticks(5); + + // Draw colour gradient in legend + for (let i = 0; i < legendHeight; i++) { + legend.append("rect") + .attr("y", i) + .attr("width", legendWidth) + .attr("height", 1) + .attr("fill", colourScale(legendScale.invert(i))); + } + + legend.append("g") + .attr("transform", `translate(${legendWidth},0)`) + .call(legendAxis); +} + +// Data format example: +// const data = [ +// { row: 'Monday', column: 'Morning', value: 42 }, +// { row: 'Monday', column: 'Afternoon', value: 78 }, +// { row: 'Tuesday', column: 'Morning', value: 65 }, +// { row: 'Tuesday', column: 'Afternoon', value: 55 } +// ]; +// drawHeatmap(data); +``` + +### Area chart with gradient + +```javascript +useEffect(() => { + if (!data || data.length === 0) return; + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + const width = 800; + const height = 400; + const margin = { top: 20, right: 30, bottom: 40, left: 50 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // Define gradient + const defs = svg.append("defs"); + const gradient = defs.append("linearGradient") + .attr("id", "areaGradient") + .attr("x1", "0%") + .attr("x2", "0%") + .attr("y1", "0%") + .attr("y2", "100%"); + + gradient.append("stop") + .attr("offset", "0%") + .attr("stop-color", "steelblue") + .attr("stop-opacity", 0.8); + + gradient.append("stop") + .attr("offset", "100%") + .attr("stop-color", "steelblue") + .attr("stop-opacity", 0.1); + + const g = svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + const xScale = d3.scaleTime() + .domain(d3.extent(data, d => d.date)) + .range([0, innerWidth]); + + const yScale = d3.scaleLinear() + .domain([0, d3.max(data, d => d.value)]) + .range([innerHeight, 0]); + + const area = d3.area() + .x(d => xScale(d.date)) + .y0(innerHeight) + .y1(d => yScale(d.value)) + .curve(d3.curveMonotoneX); + + g.append("path") + .datum(data) + .attr("fill", "url(#areaGradient)") + .attr("d", area); + + const line = d3.line() + .x(d => xScale(d.date)) + .y(d => yScale(d.value)) + .curve(d3.curveMonotoneX); + + g.append("path") + .datum(data) + .attr("fill", "none") + .attr("stroke", "steelblue") + .attr("stroke-width", 2) + .attr("d", line); + + g.append("g") + .attr("transform", `translate(0,${innerHeight})`) + .call(d3.axisBottom(xScale)); + + g.append("g") + .call(d3.axisLeft(yScale)); + +}, [data]); +``` + +### Stacked bar chart + +```javascript +useEffect(() => { + if (!data || data.length === 0) return; + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + const width = 800; + const height = 400; + const margin = { top: 20, right: 30, bottom: 40, left: 50 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const g = svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + const categories = Object.keys(data[0]).filter(k => k !== 'group'); + const stackedData = d3.stack().keys(categories)(data); + + const xScale = d3.scaleBand() + .domain(data.map(d => d.group)) + .range([0, innerWidth]) + .padding(0.1); + + const yScale = d3.scaleLinear() + .domain([0, d3.max(stackedData[stackedData.length - 1], d => d[1])]) + .range([innerHeight, 0]); + + const colourScale = d3.scaleOrdinal(d3.schemeCategory10); + + g.selectAll("g") + .data(stackedData) + .join("g") + .attr("fill", (d, i) => colourScale(i)) + .selectAll("rect") + .data(d => d) + .join("rect") + .attr("x", d => xScale(d.data.group)) + .attr("y", d => yScale(d[1])) + .attr("height", d => yScale(d[0]) - yScale(d[1])) + .attr("width", xScale.bandwidth()); + + g.append("g") + .attr("transform", `translate(0,${innerHeight})`) + .call(d3.axisBottom(xScale)); + + g.append("g") + .call(d3.axisLeft(yScale)); + +}, [data]); +``` + +### Grouped bar chart + +```javascript +useEffect(() => { + if (!data || data.length === 0) return; + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + const width = 800; + const height = 400; + const margin = { top: 20, right: 30, bottom: 40, left: 50 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const g = svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + const categories = Object.keys(data[0]).filter(k => k !== 'group'); + + const x0Scale = d3.scaleBand() + .domain(data.map(d => d.group)) + .range([0, innerWidth]) + .padding(0.1); + + const x1Scale = d3.scaleBand() + .domain(categories) + .range([0, x0Scale.bandwidth()]) + .padding(0.05); + + const yScale = d3.scaleLinear() + .domain([0, d3.max(data, d => Math.max(...categories.map(c => d[c])))]) + .range([innerHeight, 0]); + + const colourScale = d3.scaleOrdinal(d3.schemeCategory10); + + const group = g.selectAll("g") + .data(data) + .join("g") + .attr("transform", d => `translate(${x0Scale(d.group)},0)`); + + group.selectAll("rect") + .data(d => categories.map(key => ({ key, value: d[key] }))) + .join("rect") + .attr("x", d => x1Scale(d.key)) + .attr("y", d => yScale(d.value)) + .attr("width", x1Scale.bandwidth()) + .attr("height", d => innerHeight - yScale(d.value)) + .attr("fill", d => colourScale(d.key)); + + g.append("g") + .attr("transform", `translate(0,${innerHeight})`) + .call(d3.axisBottom(x0Scale)); + + g.append("g") + .call(d3.axisLeft(yScale)); + +}, [data]); +``` + +### Bubble chart + +```javascript +useEffect(() => { + if (!data || data.length === 0) return; + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + const width = 800; + const height = 600; + const margin = { top: 20, right: 30, bottom: 40, left: 50 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const g = svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + const xScale = d3.scaleLinear() + .domain([0, d3.max(data, d => d.x)]) + .range([0, innerWidth]); + + const yScale = d3.scaleLinear() + .domain([0, d3.max(data, d => d.y)]) + .range([innerHeight, 0]); + + const sizeScale = d3.scaleSqrt() + .domain([0, d3.max(data, d => d.size)]) + .range([0, 50]); + + const colourScale = d3.scaleOrdinal(d3.schemeCategory10); + + g.selectAll("circle") + .data(data) + .join("circle") + .attr("cx", d => xScale(d.x)) + .attr("cy", d => yScale(d.y)) + .attr("r", d => sizeScale(d.size)) + .attr("fill", d => colourScale(d.category)) + .attr("opacity", 0.6) + .attr("stroke", "white") + .attr("stroke-width", 2); + + g.append("g") + .attr("transform", `translate(0,${innerHeight})`) + .call(d3.axisBottom(xScale)); + + g.append("g") + .call(d3.axisLeft(yScale)); + +}, [data]); +``` + +## Geographic visualisations + +### Basic map with points + +```javascript +useEffect(() => { + if (!geoData || !pointData) return; + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + const width = 800; + const height = 600; + + const projection = d3.geoMercator() + .fitSize([width, height], geoData); + + const pathGenerator = d3.geoPath().projection(projection); + + // Draw map + svg.selectAll("path") + .data(geoData.features) + .join("path") + .attr("d", pathGenerator) + .attr("fill", "#e0e0e0") + .attr("stroke", "#999") + .attr("stroke-width", 0.5); + + // Draw points + svg.selectAll("circle") + .data(pointData) + .join("circle") + .attr("cx", d => projection([d.longitude, d.latitude])[0]) + .attr("cy", d => projection([d.longitude, d.latitude])[1]) + .attr("r", 5) + .attr("fill", "steelblue") + .attr("opacity", 0.7); + +}, [geoData, pointData]); +``` + +### Choropleth map + +```javascript +useEffect(() => { + if (!geoData || !valueData) return; + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + const width = 800; + const height = 600; + + const projection = d3.geoMercator() + .fitSize([width, height], geoData); + + const pathGenerator = d3.geoPath().projection(projection); + + // Create value lookup + const valueLookup = new Map(valueData.map(d => [d.id, d.value])); + + // Colour scale + const colourScale = d3.scaleSequential(d3.interpolateBlues) + .domain([0, d3.max(valueData, d => d.value)]); + + svg.selectAll("path") + .data(geoData.features) + .join("path") + .attr("d", pathGenerator) + .attr("fill", d => { + const value = valueLookup.get(d.id); + return value ? colourScale(value) : "#e0e0e0"; + }) + .attr("stroke", "#999") + .attr("stroke-width", 0.5); + +}, [geoData, valueData]); +``` + +## Advanced interactions + +### Brush and zoom + +```javascript +useEffect(() => { + if (!data || data.length === 0) return; + + const svg = d3.select(svgRef.current); + svg.selectAll("*").remove(); + + const width = 800; + const height = 400; + const margin = { top: 20, right: 30, bottom: 40, left: 50 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const xScale = d3.scaleLinear() + .domain([0, d3.max(data, d => d.x)]) + .range([0, innerWidth]); + + const yScale = d3.scaleLinear() + .domain([0, d3.max(data, d => d.y)]) + .range([innerHeight, 0]); + + const g = svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + const circles = g.selectAll("circle") + .data(data) + .join("circle") + .attr("cx", d => xScale(d.x)) + .attr("cy", d => yScale(d.y)) + .attr("r", 5) + .attr("fill", "steelblue"); + + // Add brush + const brush = d3.brush() + .extent([[0, 0], [innerWidth, innerHeight]]) + .on("start brush", (event) => { + if (!event.selection) return; + + const [[x0, y0], [x1, y1]] = event.selection; + + circles.attr("fill", d => { + const cx = xScale(d.x); + const cy = yScale(d.y); + return (cx >= x0 && cx <= x1 && cy >= y0 && cy <= y1) + ? "orange" + : "steelblue"; + }); + }); + + g.append("g") + .attr("class", "brush") + .call(brush); + +}, [data]); +``` + +### Linked brushing between charts + +```javascript +function LinkedCharts({ data }) { + const [selectedPoints, setSelectedPoints] = useState(new Set()); + const svg1Ref = useRef(); + const svg2Ref = useRef(); + + useEffect(() => { + // Chart 1: Scatter plot + const svg1 = d3.select(svg1Ref.current); + svg1.selectAll("*").remove(); + + // ... create first chart ... + + const circles1 = svg1.selectAll("circle") + .data(data) + .join("circle") + .attr("fill", d => selectedPoints.has(d.id) ? "orange" : "steelblue"); + + // Chart 2: Bar chart + const svg2 = d3.select(svg2Ref.current); + svg2.selectAll("*").remove(); + + // ... create second chart ... + + const bars = svg2.selectAll("rect") + .data(data) + .join("rect") + .attr("fill", d => selectedPoints.has(d.id) ? "orange" : "steelblue"); + + // Add brush to first chart + const brush = d3.brush() + .on("start brush end", (event) => { + if (!event.selection) { + setSelectedPoints(new Set()); + return; + } + + const [[x0, y0], [x1, y1]] = event.selection; + const selected = new Set(); + + data.forEach(d => { + const x = xScale(d.x); + const y = yScale(d.y); + if (x >= x0 && x <= x1 && y >= y0 && y <= y1) { + selected.add(d.id); + } + }); + + setSelectedPoints(selected); + }); + + svg1.append("g").call(brush); + + }, [data, selectedPoints]); + + return ( +
+ + +
+ ); +} +``` + +## Animation patterns + +### Enter, update, exit with transitions + +```javascript +useEffect(() => { + if (!data || data.length === 0) return; + + const svg = d3.select(svgRef.current); + + const circles = svg.selectAll("circle") + .data(data, d => d.id); // Key function for object constancy + + // EXIT: Remove old elements + circles.exit() + .transition() + .duration(500) + .attr("r", 0) + .remove(); + + // UPDATE: Modify existing elements + circles + .transition() + .duration(500) + .attr("cx", d => xScale(d.x)) + .attr("cy", d => yScale(d.y)) + .attr("fill", "steelblue"); + + // ENTER: Add new elements + circles.enter() + .append("circle") + .attr("cx", d => xScale(d.x)) + .attr("cy", d => yScale(d.y)) + .attr("r", 0) + .attr("fill", "steelblue") + .transition() + .duration(500) + .attr("r", 5); + +}, [data]); +``` + +### Path morphing + +```javascript +useEffect(() => { + if (!data1 || !data2) return; + + const svg = d3.select(svgRef.current); + + const line = d3.line() + .x(d => xScale(d.x)) + .y(d => yScale(d.y)) + .curve(d3.curveMonotoneX); + + const path = svg.select("path"); + + // Morph from data1 to data2 + path + .datum(data1) + .attr("d", line) + .transition() + .duration(1000) + .attrTween("d", function() { + const previous = d3.select(this).attr("d"); + const current = line(data2); + return d3.interpolatePath(previous, current); + }); + +}, [data1, data2]); +``` \ No newline at end of file diff --git a/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/references/scale-reference.md b/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/references/scale-reference.md new file mode 100644 index 00000000..61bd981a --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/claude-d3js-skill/references/scale-reference.md @@ -0,0 +1,509 @@ +# D3.js Scale Reference + +Comprehensive guide to all d3 scale types with examples and use cases. + +## Continuous scales + +### Linear scale + +Maps continuous input domain to continuous output range with linear interpolation. + +```javascript +const scale = d3.scaleLinear() + .domain([0, 100]) + .range([0, 500]); + +scale(50); // Returns 250 +scale(0); // Returns 0 +scale(100); // Returns 500 + +// Invert scale (get input from output) +scale.invert(250); // Returns 50 +``` + +**Use cases:** +- Most common scale for quantitative data +- Axes, bar lengths, position encoding +- Temperature, prices, counts, measurements + +**Methods:** +- `.domain([min, max])` - Set input domain +- `.range([min, max])` - Set output range +- `.invert(value)` - Get domain value from range value +- `.clamp(true)` - Restrict output to range bounds +- `.nice()` - Extend domain to nice round values + +### Power scale + +Maps continuous input to continuous output with exponential transformation. + +```javascript +const sqrtScale = d3.scalePow() + .exponent(0.5) // Square root + .domain([0, 100]) + .range([0, 500]); + +const squareScale = d3.scalePow() + .exponent(2) // Square + .domain([0, 100]) + .range([0, 500]); + +// Shorthand for square root +const sqrtScale2 = d3.scaleSqrt() + .domain([0, 100]) + .range([0, 500]); +``` + +**Use cases:** +- Perceptual scaling (human perception is non-linear) +- Area encoding (use square root to map values to circle radii) +- Emphasising differences in small or large values + +### Logarithmic scale + +Maps continuous input to continuous output with logarithmic transformation. + +```javascript +const logScale = d3.scaleLog() + .domain([1, 1000]) // Must be positive + .range([0, 500]); + +logScale(1); // Returns 0 +logScale(10); // Returns ~167 +logScale(100); // Returns ~333 +logScale(1000); // Returns 500 +``` + +**Use cases:** +- Data spanning multiple orders of magnitude +- Population, GDP, wealth distributions +- Logarithmic axes +- Exponential growth visualisations + +**Important:** Domain values must be strictly positive (>0). + +### Time scale + +Specialised linear scale for temporal data. + +```javascript +const timeScale = d3.scaleTime() + .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)]) + .range([0, 800]); + +timeScale(new Date(2022, 0, 1)); // Returns 400 + +// Invert to get date +timeScale.invert(400); // Returns Date object for mid-2022 +``` + +**Use cases:** +- Time series visualisations +- Timeline axes +- Temporal animations +- Date-based interactions + +**Methods:** +- `.nice()` - Extend domain to nice time intervals +- `.ticks(count)` - Generate nicely-spaced tick values +- All linear scale methods apply + +### Quantize scale + +Maps continuous input to discrete output buckets. + +```javascript +const quantizeScale = d3.scaleQuantize() + .domain([0, 100]) + .range(['low', 'medium', 'high']); + +quantizeScale(25); // Returns 'low' +quantizeScale(50); // Returns 'medium' +quantizeScale(75); // Returns 'high' + +// Get the threshold values +quantizeScale.thresholds(); // Returns [33.33, 66.67] +``` + +**Use cases:** +- Binning continuous data +- Heat map colours +- Risk categories (low/medium/high) +- Age groups, income brackets + +### Quantile scale + +Maps continuous input to discrete output based on quantiles. + +```javascript +const quantileScale = d3.scaleQuantile() + .domain([3, 6, 7, 8, 8, 10, 13, 15, 16, 20, 24]) // Sample data + .range(['low', 'medium', 'high']); + +quantileScale(8); // Returns based on quantile position +quantileScale.quantiles(); // Returns quantile thresholds +``` + +**Use cases:** +- Equal-size groups regardless of distribution +- Percentile-based categorisation +- Handling skewed distributions + +### Threshold scale + +Maps continuous input to discrete output with custom thresholds. + +```javascript +const thresholdScale = d3.scaleThreshold() + .domain([0, 10, 20]) + .range(['freezing', 'cold', 'warm', 'hot']); + +thresholdScale(-5); // Returns 'freezing' +thresholdScale(5); // Returns 'cold' +thresholdScale(15); // Returns 'warm' +thresholdScale(25); // Returns 'hot' +``` + +**Use cases:** +- Custom breakpoints +- Grade boundaries (A, B, C, D, F) +- Temperature categories +- Air quality indices + +## Sequential scales + +### Sequential colour scale + +Maps continuous input to continuous colour gradient. + +```javascript +const colourScale = d3.scaleSequential(d3.interpolateBlues) + .domain([0, 100]); + +colourScale(0); // Returns lightest blue +colourScale(50); // Returns mid blue +colourScale(100); // Returns darkest blue +``` + +**Available interpolators:** + +**Single hue:** +- `d3.interpolateBlues`, `d3.interpolateGreens`, `d3.interpolateReds` +- `d3.interpolateOranges`, `d3.interpolatePurples`, `d3.interpolateGreys` + +**Multi-hue:** +- `d3.interpolateViridis`, `d3.interpolateInferno`, `d3.interpolateMagma` +- `d3.interpolatePlasma`, `d3.interpolateWarm`, `d3.interpolateCool` +- `d3.interpolateCubehelixDefault`, `d3.interpolateTurbo` + +**Use cases:** +- Heat maps, choropleth maps +- Continuous data visualisation +- Temperature, elevation, density + +### Diverging colour scale + +Maps continuous input to diverging colour gradient with a midpoint. + +```javascript +const divergingScale = d3.scaleDiverging(d3.interpolateRdBu) + .domain([-10, 0, 10]); + +divergingScale(-10); // Returns red +divergingScale(0); // Returns white/neutral +divergingScale(10); // Returns blue +``` + +**Available interpolators:** +- `d3.interpolateRdBu` - Red to blue +- `d3.interpolateRdYlBu` - Red, yellow, blue +- `d3.interpolateRdYlGn` - Red, yellow, green +- `d3.interpolatePiYG` - Pink, yellow, green +- `d3.interpolateBrBG` - Brown, blue-green +- `d3.interpolatePRGn` - Purple, green +- `d3.interpolatePuOr` - Purple, orange +- `d3.interpolateRdGy` - Red, grey +- `d3.interpolateSpectral` - Rainbow spectrum + +**Use cases:** +- Data with meaningful midpoint (zero, average, neutral) +- Positive/negative values +- Above/below comparisons +- Correlation matrices + +### Sequential quantile scale + +Combines sequential colour with quantile mapping. + +```javascript +const sequentialQuantileScale = d3.scaleSequentialQuantile(d3.interpolateBlues) + .domain([3, 6, 7, 8, 8, 10, 13, 15, 16, 20, 24]); + +// Maps based on quantile position +``` + +**Use cases:** +- Perceptually uniform binning +- Handling outliers +- Skewed distributions + +## Ordinal scales + +### Band scale + +Maps discrete input to continuous bands (rectangles) with optional padding. + +```javascript +const bandScale = d3.scaleBand() + .domain(['A', 'B', 'C', 'D']) + .range([0, 400]) + .padding(0.1); + +bandScale('A'); // Returns start position (e.g., 0) +bandScale('B'); // Returns start position (e.g., 110) +bandScale.bandwidth(); // Returns width of each band (e.g., 95) +bandScale.step(); // Returns total step including padding +bandScale.paddingInner(); // Returns inner padding (between bands) +bandScale.paddingOuter(); // Returns outer padding (at edges) +``` + +**Use cases:** +- Bar charts (most common use case) +- Grouped elements +- Categorical axes +- Heat map cells + +**Padding options:** +- `.padding(value)` - Sets both inner and outer padding (0-1) +- `.paddingInner(value)` - Padding between bands (0-1) +- `.paddingOuter(value)` - Padding at edges (0-1) +- `.align(value)` - Alignment of bands (0-1, default 0.5) + +### Point scale + +Maps discrete input to continuous points (no width). + +```javascript +const pointScale = d3.scalePoint() + .domain(['A', 'B', 'C', 'D']) + .range([0, 400]) + .padding(0.5); + +pointScale('A'); // Returns position (e.g., 50) +pointScale('B'); // Returns position (e.g., 150) +pointScale('C'); // Returns position (e.g., 250) +pointScale('D'); // Returns position (e.g., 350) +pointScale.step(); // Returns distance between points +``` + +**Use cases:** +- Line chart categorical x-axis +- Scatter plot with categorical axis +- Node positions in network graphs +- Any point positioning for categories + +### Ordinal colour scale + +Maps discrete input to discrete output (colours, shapes, etc.). + +```javascript +const colourScale = d3.scaleOrdinal(d3.schemeCategory10); + +colourScale('apples'); // Returns first colour +colourScale('oranges'); // Returns second colour +colourScale('apples'); // Returns same first colour (consistent) + +// Custom range +const customScale = d3.scaleOrdinal() + .domain(['cat1', 'cat2', 'cat3']) + .range(['#FF6B6B', '#4ECDC4', '#45B7D1']); +``` + +**Built-in colour schemes:** + +**Categorical:** +- `d3.schemeCategory10` - 10 colours +- `d3.schemeAccent` - 8 colours +- `d3.schemeDark2` - 8 colours +- `d3.schemePaired` - 12 colours +- `d3.schemePastel1` - 9 colours +- `d3.schemePastel2` - 8 colours +- `d3.schemeSet1` - 9 colours +- `d3.schemeSet2` - 8 colours +- `d3.schemeSet3` - 12 colours +- `d3.schemeTableau10` - 10 colours + +**Use cases:** +- Category colours +- Legend items +- Multi-series charts +- Network node types + +## Scale utilities + +### Nice domain + +Extend domain to nice round values. + +```javascript +const scale = d3.scaleLinear() + .domain([0.201, 0.996]) + .nice(); + +scale.domain(); // Returns [0.2, 1.0] + +// With count (approximate tick count) +const scale2 = d3.scaleLinear() + .domain([0.201, 0.996]) + .nice(5); +``` + +### Clamping + +Restrict output to range bounds. + +```javascript +const scale = d3.scaleLinear() + .domain([0, 100]) + .range([0, 500]) + .clamp(true); + +scale(-10); // Returns 0 (clamped) +scale(150); // Returns 500 (clamped) +``` + +### Copy scales + +Create independent copies. + +```javascript +const scale1 = d3.scaleLinear() + .domain([0, 100]) + .range([0, 500]); + +const scale2 = scale1.copy(); +// scale2 is independent of scale1 +``` + +### Tick generation + +Generate nice tick values for axes. + +```javascript +const scale = d3.scaleLinear() + .domain([0, 100]) + .range([0, 500]); + +scale.ticks(10); // Generate ~10 ticks +scale.tickFormat(10); // Get format function for ticks +scale.tickFormat(10, ".2f"); // Custom format (2 decimal places) + +// Time scale ticks +const timeScale = d3.scaleTime() + .domain([new Date(2020, 0, 1), new Date(2024, 0, 1)]); + +timeScale.ticks(d3.timeYear); // Yearly ticks +timeScale.ticks(d3.timeMonth, 3); // Every 3 months +timeScale.tickFormat(5, "%Y-%m"); // Format as year-month +``` + +## Colour spaces and interpolation + +### RGB interpolation + +```javascript +const scale = d3.scaleLinear() + .domain([0, 100]) + .range(["blue", "red"]); +// Default: RGB interpolation +``` + +### HSL interpolation + +```javascript +const scale = d3.scaleLinear() + .domain([0, 100]) + .range(["blue", "red"]) + .interpolate(d3.interpolateHsl); +// Smoother colour transitions +``` + +### Lab interpolation + +```javascript +const scale = d3.scaleLinear() + .domain([0, 100]) + .range(["blue", "red"]) + .interpolate(d3.interpolateLab); +// Perceptually uniform +``` + +### HCL interpolation + +```javascript +const scale = d3.scaleLinear() + .domain([0, 100]) + .range(["blue", "red"]) + .interpolate(d3.interpolateHcl); +// Perceptually uniform with hue +``` + +## Common patterns + +### Diverging scale with custom midpoint + +```javascript +const scale = d3.scaleLinear() + .domain([min, midpoint, max]) + .range(["red", "white", "blue"]) + .interpolate(d3.interpolateHcl); +``` + +### Multi-stop gradient scale + +```javascript +const scale = d3.scaleLinear() + .domain([0, 25, 50, 75, 100]) + .range(["#d53e4f", "#fc8d59", "#fee08b", "#e6f598", "#66c2a5"]); +``` + +### Radius scale for circles (perceptual) + +```javascript +const radiusScale = d3.scaleSqrt() + .domain([0, d3.max(data, d => d.value)]) + .range([0, 50]); + +// Use with circles +circle.attr("r", d => radiusScale(d.value)); +``` + +### Adaptive scale based on data range + +```javascript +function createAdaptiveScale(data) { + const extent = d3.extent(data); + const range = extent[1] - extent[0]; + + // Use log scale if data spans >2 orders of magnitude + if (extent[1] / extent[0] > 100) { + return d3.scaleLog() + .domain(extent) + .range([0, width]); + } + + // Otherwise use linear + return d3.scaleLinear() + .domain(extent) + .range([0, width]); +} +``` + +### Colour scale with explicit categories + +```javascript +const colourScale = d3.scaleOrdinal() + .domain(['Low Risk', 'Medium Risk', 'High Risk']) + .range(['#2ecc71', '#f39c12', '#e74c3c']) + .unknown('#95a5a6'); // Fallback for unknown values +``` \ No newline at end of file diff --git a/plugins/antigravity-bundle-data-analytics/skills/database-architect/SKILL.md b/plugins/antigravity-bundle-data-analytics/skills/database-architect/SKILL.md new file mode 100644 index 00000000..3a468ef6 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/database-architect/SKILL.md @@ -0,0 +1,263 @@ +--- +name: database-architect +description: Expert database architect specializing in data layer design from scratch, technology selection, schema modeling, and scalable database architectures. +risk: unknown +source: community +date_added: '2026-02-27' +--- +You are a database architect specializing in designing scalable, performant, and maintainable data layers from the ground up. + +## Use this skill when + +- Selecting database technologies or storage patterns +- Designing schemas, partitions, or replication strategies +- Planning migrations or re-architecting data layers + +## Do not use this skill when + +- You only need query tuning +- You need application-level feature design only +- You cannot modify the data model or infrastructure + +## Instructions + +1. Capture data domain, access patterns, and scale targets. +2. Choose the database model and architecture pattern. +3. Design schemas, indexes, and lifecycle policies. +4. Plan migration, backup, and rollout strategies. + +## Safety + +- Avoid destructive changes without backups and rollbacks. +- Validate migration plans in staging before production. + +## Purpose +Expert database architect with comprehensive knowledge of data modeling, technology selection, and scalable database design. Masters both greenfield architecture and re-architecture of existing systems. Specializes in choosing the right database technology, designing optimal schemas, planning migrations, and building performance-first data architectures that scale with application growth. + +## Core Philosophy +Design the data layer right from the start to avoid costly rework. Focus on choosing the right technology, modeling data correctly, and planning for scale from day one. Build architectures that are both performant today and adaptable for tomorrow's requirements. + +## Capabilities + +### Technology Selection & Evaluation +- **Relational databases**: PostgreSQL, MySQL, MariaDB, SQL Server, Oracle +- **NoSQL databases**: MongoDB, DynamoDB, Cassandra, CouchDB, Redis, Couchbase +- **Time-series databases**: TimescaleDB, InfluxDB, ClickHouse, QuestDB +- **NewSQL databases**: CockroachDB, TiDB, Google Spanner, YugabyteDB +- **Graph databases**: Neo4j, Amazon Neptune, ArangoDB +- **Search engines**: Elasticsearch, OpenSearch, Meilisearch, Typesense +- **Document stores**: MongoDB, Firestore, RavenDB, DocumentDB +- **Key-value stores**: Redis, DynamoDB, etcd, Memcached +- **Wide-column stores**: Cassandra, HBase, ScyllaDB, Bigtable +- **Multi-model databases**: ArangoDB, OrientDB, FaunaDB, CosmosDB +- **Decision frameworks**: Consistency vs availability trade-offs, CAP theorem implications +- **Technology assessment**: Performance characteristics, operational complexity, cost implications +- **Hybrid architectures**: Polyglot persistence, multi-database strategies, data synchronization + +### Data Modeling & Schema Design +- **Conceptual modeling**: Entity-relationship diagrams, domain modeling, business requirement mapping +- **Logical modeling**: Normalization (1NF-5NF), denormalization strategies, dimensional modeling +- **Physical modeling**: Storage optimization, data type selection, partitioning strategies +- **Relational design**: Table relationships, foreign keys, constraints, referential integrity +- **NoSQL design patterns**: Document embedding vs referencing, data duplication strategies +- **Schema evolution**: Versioning strategies, backward/forward compatibility, migration patterns +- **Data integrity**: Constraints, triggers, check constraints, application-level validation +- **Temporal data**: Slowly changing dimensions, event sourcing, audit trails, time-travel queries +- **Hierarchical data**: Adjacency lists, nested sets, materialized paths, closure tables +- **JSON/semi-structured**: JSONB indexes, schema-on-read vs schema-on-write +- **Multi-tenancy**: Shared schema, database per tenant, schema per tenant trade-offs +- **Data archival**: Historical data strategies, cold storage, compliance requirements + +### Normalization vs Denormalization +- **Normalization benefits**: Data consistency, update efficiency, storage optimization +- **Denormalization strategies**: Read performance optimization, reduced JOIN complexity +- **Trade-off analysis**: Write vs read patterns, consistency requirements, query complexity +- **Hybrid approaches**: Selective denormalization, materialized views, derived columns +- **OLTP vs OLAP**: Transaction processing vs analytical workload optimization +- **Aggregate patterns**: Pre-computed aggregations, incremental updates, refresh strategies +- **Dimensional modeling**: Star schema, snowflake schema, fact and dimension tables + +### Indexing Strategy & Design +- **Index types**: B-tree, Hash, GiST, GIN, BRIN, bitmap, spatial indexes +- **Composite indexes**: Column ordering, covering indexes, index-only scans +- **Partial indexes**: Filtered indexes, conditional indexing, storage optimization +- **Full-text search**: Text search indexes, ranking strategies, language-specific optimization +- **JSON indexing**: JSONB GIN indexes, expression indexes, path-based indexes +- **Unique constraints**: Primary keys, unique indexes, compound uniqueness +- **Index planning**: Query pattern analysis, index selectivity, cardinality considerations +- **Index maintenance**: Bloat management, statistics updates, rebuild strategies +- **Cloud-specific**: Aurora indexing, Azure SQL intelligent indexing, managed index recommendations +- **NoSQL indexing**: MongoDB compound indexes, DynamoDB secondary indexes (GSI/LSI) + +### Query Design & Optimization +- **Query patterns**: Read-heavy, write-heavy, analytical, transactional patterns +- **JOIN strategies**: INNER, LEFT, RIGHT, FULL joins, cross joins, semi/anti joins +- **Subquery optimization**: Correlated subqueries, derived tables, CTEs, materialization +- **Window functions**: Ranking, running totals, moving averages, partition-based analysis +- **Aggregation patterns**: GROUP BY optimization, HAVING clauses, cube/rollup operations +- **Query hints**: Optimizer hints, index hints, join hints (when appropriate) +- **Prepared statements**: Parameterized queries, plan caching, SQL injection prevention +- **Batch operations**: Bulk inserts, batch updates, upsert patterns, merge operations + +### Caching Architecture +- **Cache layers**: Application cache, query cache, object cache, result cache +- **Cache technologies**: Redis, Memcached, Varnish, application-level caching +- **Cache strategies**: Cache-aside, write-through, write-behind, refresh-ahead +- **Cache invalidation**: TTL strategies, event-driven invalidation, cache stampede prevention +- **Distributed caching**: Redis Cluster, cache partitioning, cache consistency +- **Materialized views**: Database-level caching, incremental refresh, full refresh strategies +- **CDN integration**: Edge caching, API response caching, static asset caching +- **Cache warming**: Preloading strategies, background refresh, predictive caching + +### Scalability & Performance Design +- **Vertical scaling**: Resource optimization, instance sizing, performance tuning +- **Horizontal scaling**: Read replicas, load balancing, connection pooling +- **Partitioning strategies**: Range, hash, list, composite partitioning +- **Sharding design**: Shard key selection, resharding strategies, cross-shard queries +- **Replication patterns**: Master-slave, master-master, multi-region replication +- **Consistency models**: Strong consistency, eventual consistency, causal consistency +- **Connection pooling**: Pool sizing, connection lifecycle, timeout configuration +- **Load distribution**: Read/write splitting, geographic distribution, workload isolation +- **Storage optimization**: Compression, columnar storage, tiered storage +- **Capacity planning**: Growth projections, resource forecasting, performance baselines + +### Migration Planning & Strategy +- **Migration approaches**: Big bang, trickle, parallel run, strangler pattern +- **Zero-downtime migrations**: Online schema changes, rolling deployments, blue-green databases +- **Data migration**: ETL pipelines, data validation, consistency checks, rollback procedures +- **Schema versioning**: Migration tools (Flyway, Liquibase, Alembic, Prisma), version control +- **Rollback planning**: Backup strategies, data snapshots, recovery procedures +- **Cross-database migration**: SQL to NoSQL, database engine switching, cloud migration +- **Large table migrations**: Chunked migrations, incremental approaches, downtime minimization +- **Testing strategies**: Migration testing, data integrity validation, performance testing +- **Cutover planning**: Timing, coordination, rollback triggers, success criteria + +### Transaction Design & Consistency +- **ACID properties**: Atomicity, consistency, isolation, durability requirements +- **Isolation levels**: Read uncommitted, read committed, repeatable read, serializable +- **Transaction patterns**: Unit of work, optimistic locking, pessimistic locking +- **Distributed transactions**: Two-phase commit, saga patterns, compensating transactions +- **Eventual consistency**: BASE properties, conflict resolution, version vectors +- **Concurrency control**: Lock management, deadlock prevention, timeout strategies +- **Idempotency**: Idempotent operations, retry safety, deduplication strategies +- **Event sourcing**: Event store design, event replay, snapshot strategies + +### Security & Compliance +- **Access control**: Role-based access (RBAC), row-level security, column-level security +- **Encryption**: At-rest encryption, in-transit encryption, key management +- **Data masking**: Dynamic data masking, anonymization, pseudonymization +- **Audit logging**: Change tracking, access logging, compliance reporting +- **Compliance patterns**: GDPR, HIPAA, PCI-DSS, SOC2 compliance architecture +- **Data retention**: Retention policies, automated cleanup, legal holds +- **Sensitive data**: PII handling, tokenization, secure storage patterns +- **Backup security**: Encrypted backups, secure storage, access controls + +### Cloud Database Architecture +- **AWS databases**: RDS, Aurora, DynamoDB, DocumentDB, Neptune, Timestream +- **Azure databases**: SQL Database, Cosmos DB, Database for PostgreSQL/MySQL, Synapse +- **GCP databases**: Cloud SQL, Cloud Spanner, Firestore, Bigtable, BigQuery +- **Serverless databases**: Aurora Serverless, Azure SQL Serverless, FaunaDB +- **Database-as-a-Service**: Managed benefits, operational overhead reduction, cost implications +- **Cloud-native features**: Auto-scaling, automated backups, point-in-time recovery +- **Multi-region design**: Global distribution, cross-region replication, latency optimization +- **Hybrid cloud**: On-premises integration, private cloud, data sovereignty + +### ORM & Framework Integration +- **ORM selection**: Django ORM, SQLAlchemy, Prisma, TypeORM, Entity Framework, ActiveRecord +- **Schema-first vs Code-first**: Migration generation, type safety, developer experience +- **Migration tools**: Prisma Migrate, Alembic, Flyway, Liquibase, Laravel Migrations +- **Query builders**: Type-safe queries, dynamic query construction, performance implications +- **Connection management**: Pooling configuration, transaction handling, session management +- **Performance patterns**: Eager loading, lazy loading, batch fetching, N+1 prevention +- **Type safety**: Schema validation, runtime checks, compile-time safety + +### Monitoring & Observability +- **Performance metrics**: Query latency, throughput, connection counts, cache hit rates +- **Monitoring tools**: CloudWatch, DataDog, New Relic, Prometheus, Grafana +- **Query analysis**: Slow query logs, execution plans, query profiling +- **Capacity monitoring**: Storage growth, CPU/memory utilization, I/O patterns +- **Alert strategies**: Threshold-based alerts, anomaly detection, SLA monitoring +- **Performance baselines**: Historical trends, regression detection, capacity planning + +### Disaster Recovery & High Availability +- **Backup strategies**: Full, incremental, differential backups, backup rotation +- **Point-in-time recovery**: Transaction log backups, continuous archiving, recovery procedures +- **High availability**: Active-passive, active-active, automatic failover +- **RPO/RTO planning**: Recovery point objectives, recovery time objectives, testing procedures +- **Multi-region**: Geographic distribution, disaster recovery regions, failover automation +- **Data durability**: Replication factor, synchronous vs asynchronous replication + +## Behavioral Traits +- Starts with understanding business requirements and access patterns before choosing technology +- Designs for both current needs and anticipated future scale +- Recommends schemas and architecture (doesn't modify files unless explicitly requested) +- Plans migrations thoroughly (doesn't execute unless explicitly requested) +- Generates ERD diagrams only when requested +- Considers operational complexity alongside performance requirements +- Values simplicity and maintainability over premature optimization +- Documents architectural decisions with clear rationale and trade-offs +- Designs with failure modes and edge cases in mind +- Balances normalization principles with real-world performance needs +- Considers the entire application architecture when designing data layer +- Emphasizes testability and migration safety in design decisions + +## Workflow Position +- **Before**: backend-architect (data layer informs API design) +- **Complements**: database-admin (operations), database-optimizer (performance tuning), performance-engineer (system-wide optimization) +- **Enables**: Backend services can be built on solid data foundation + +## Knowledge Base +- Relational database theory and normalization principles +- NoSQL database patterns and consistency models +- Time-series and analytical database optimization +- Cloud database services and their specific features +- Migration strategies and zero-downtime deployment patterns +- ORM frameworks and code-first vs database-first approaches +- Scalability patterns and distributed system design +- Security and compliance requirements for data systems +- Modern development workflows and CI/CD integration + +## Response Approach +1. **Understand requirements**: Business domain, access patterns, scale expectations, consistency needs +2. **Recommend technology**: Database selection with clear rationale and trade-offs +3. **Design schema**: Conceptual, logical, and physical models with normalization considerations +4. **Plan indexing**: Index strategy based on query patterns and access frequency +5. **Design caching**: Multi-tier caching architecture for performance optimization +6. **Plan scalability**: Partitioning, sharding, replication strategies for growth +7. **Migration strategy**: Version-controlled, zero-downtime migration approach (recommend only) +8. **Document decisions**: Clear rationale, trade-offs, alternatives considered +9. **Generate diagrams**: ERD diagrams when requested using Mermaid +10. **Consider integration**: ORM selection, framework compatibility, developer experience + +## Example Interactions +- "Design a database schema for a multi-tenant SaaS e-commerce platform" +- "Help me choose between PostgreSQL and MongoDB for a real-time analytics dashboard" +- "Create a migration strategy to move from MySQL to PostgreSQL with zero downtime" +- "Design a time-series database architecture for IoT sensor data at 1M events/second" +- "Re-architect our monolithic database into a microservices data architecture" +- "Plan a sharding strategy for a social media platform expecting 100M users" +- "Design a CQRS event-sourced architecture for an order management system" +- "Create an ERD for a healthcare appointment booking system" (generates Mermaid diagram) +- "Optimize schema design for a read-heavy content management system" +- "Design a multi-region database architecture with strong consistency guarantees" +- "Plan migration from denormalized NoSQL to normalized relational schema" +- "Create a database architecture for GDPR-compliant user data storage" + +## Key Distinctions +- **vs database-optimizer**: Focuses on architecture and design (greenfield/re-architecture) rather than tuning existing systems +- **vs database-admin**: Focuses on design decisions rather than operations and maintenance +- **vs backend-architect**: Focuses specifically on data layer architecture before backend services are designed +- **vs performance-engineer**: Focuses on data architecture design rather than system-wide performance optimization + +## Output Examples +When designing architecture, provide: +- Technology recommendation with selection rationale +- Schema design with tables/collections, relationships, constraints +- Index strategy with specific indexes and rationale +- Caching architecture with layers and invalidation strategy +- Migration plan with phases and rollback procedures +- Scaling strategy with growth projections +- ERD diagrams (when requested) using Mermaid syntax +- Code examples for ORM integration and migration scripts +- Monitoring and alerting recommendations +- Documentation of trade-offs and alternative approaches considered diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/AGENTS.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/AGENTS.md new file mode 100644 index 00000000..08d9e56e --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/AGENTS.md @@ -0,0 +1,1490 @@ +# Postgres Best Practices + +**Version 1.0.0** +Supabase +January 2026 + +> This document is optimized for AI agents and LLMs. Rules are prioritized by performance impact. + +--- + +## Abstract + +Comprehensive Postgres performance optimization guide for developers using Supabase and Postgres. Contains performance rules across 8 categories, prioritized by impact from critical (query performance, connection management) to incremental (advanced features). Each rule includes detailed explanations, incorrect vs. correct SQL examples, query plan analysis, and specific performance metrics to guide automated optimization and code generation. + +--- + +## Table of Contents + +1. [Query Performance](#query-performance) - **CRITICAL** + - 1.1 [Add Indexes on WHERE and JOIN Columns](#11-add-indexes-on-where-and-join-columns) + - 1.2 [Choose the Right Index Type for Your Data](#12-choose-the-right-index-type-for-your-data) + - 1.3 [Create Composite Indexes for Multi-Column Queries](#13-create-composite-indexes-for-multi-column-queries) + - 1.4 [Use Covering Indexes to Avoid Table Lookups](#14-use-covering-indexes-to-avoid-table-lookups) + - 1.5 [Use Partial Indexes for Filtered Queries](#15-use-partial-indexes-for-filtered-queries) + +2. [Connection Management](#connection-management) - **CRITICAL** + - 2.1 [Configure Idle Connection Timeouts](#21-configure-idle-connection-timeouts) + - 2.2 [Set Appropriate Connection Limits](#22-set-appropriate-connection-limits) + - 2.3 [Use Connection Pooling for All Applications](#23-use-connection-pooling-for-all-applications) + - 2.4 [Use Prepared Statements Correctly with Pooling](#24-use-prepared-statements-correctly-with-pooling) + +3. [Security & RLS](#security-rls) - **CRITICAL** + - 3.1 [Apply Principle of Least Privilege](#31-apply-principle-of-least-privilege) + - 3.2 [Enable Row Level Security for Multi-Tenant Data](#32-enable-row-level-security-for-multi-tenant-data) + - 3.3 [Optimize RLS Policies for Performance](#33-optimize-rls-policies-for-performance) + +4. [Schema Design](#schema-design) - **HIGH** + - 4.1 [Choose Appropriate Data Types](#41-choose-appropriate-data-types) + - 4.2 [Index Foreign Key Columns](#42-index-foreign-key-columns) + - 4.3 [Partition Large Tables for Better Performance](#43-partition-large-tables-for-better-performance) + - 4.4 [Select Optimal Primary Key Strategy](#44-select-optimal-primary-key-strategy) + - 4.5 [Use Lowercase Identifiers for Compatibility](#45-use-lowercase-identifiers-for-compatibility) + +5. [Concurrency & Locking](#concurrency-locking) - **MEDIUM-HIGH** + - 5.1 [Keep Transactions Short to Reduce Lock Contention](#51-keep-transactions-short-to-reduce-lock-contention) + - 5.2 [Prevent Deadlocks with Consistent Lock Ordering](#52-prevent-deadlocks-with-consistent-lock-ordering) + - 5.3 [Use Advisory Locks for Application-Level Locking](#53-use-advisory-locks-for-application-level-locking) + - 5.4 [Use SKIP LOCKED for Non-Blocking Queue Processing](#54-use-skip-locked-for-non-blocking-queue-processing) + +6. [Data Access Patterns](#data-access-patterns) - **MEDIUM** + - 6.1 [Batch INSERT Statements for Bulk Data](#61-batch-insert-statements-for-bulk-data) + - 6.2 [Eliminate N+1 Queries with Batch Loading](#62-eliminate-n1-queries-with-batch-loading) + - 6.3 [Use Cursor-Based Pagination Instead of OFFSET](#63-use-cursor-based-pagination-instead-of-offset) + - 6.4 [Use UPSERT for Insert-or-Update Operations](#64-use-upsert-for-insert-or-update-operations) + +7. [Monitoring & Diagnostics](#monitoring-diagnostics) - **LOW-MEDIUM** + - 7.1 [Enable pg_stat_statements for Query Analysis](#71-enable-pgstatstatements-for-query-analysis) + - 7.2 [Maintain Table Statistics with VACUUM and ANALYZE](#72-maintain-table-statistics-with-vacuum-and-analyze) + - 7.3 [Use EXPLAIN ANALYZE to Diagnose Slow Queries](#73-use-explain-analyze-to-diagnose-slow-queries) + +8. [Advanced Features](#advanced-features) - **LOW** + - 8.1 [Index JSONB Columns for Efficient Querying](#81-index-jsonb-columns-for-efficient-querying) + - 8.2 [Use tsvector for Full-Text Search](#82-use-tsvector-for-full-text-search) + +--- + +## 1. Query Performance + +**Impact: CRITICAL** + +Slow queries, missing indexes, inefficient query plans. The most common source of Postgres performance issues. + +### 1.1 Add Indexes on WHERE and JOIN Columns + +**Impact: CRITICAL (100-1000x faster queries on large tables)** + +Queries filtering or joining on unindexed columns cause full table scans, which become exponentially slower as tables grow. + +**Incorrect (sequential scan on large table):** + +```sql +-- No index on customer_id causes full table scan +select * from orders where customer_id = 123; + +-- EXPLAIN shows: Seq Scan on orders (cost=0.00..25000.00 rows=100 width=85) +``` + +**Correct (index scan):** + +```sql +-- Create index on frequently filtered column +create index orders_customer_id_idx on orders (customer_id); + +select * from orders where customer_id = 123; + +-- EXPLAIN shows: Index Scan using orders_customer_id_idx (cost=0.42..8.44 rows=100 width=85) +-- Index the referencing column +create index orders_customer_id_idx on orders (customer_id); + +select c.name, o.total +from customers c +join orders o on o.customer_id = c.id; +``` + +For JOIN columns, always index the foreign key side: + +Reference: https://supabase.com/docs/guides/database/query-optimization + +--- + +### 1.2 Choose the Right Index Type for Your Data + +**Impact: HIGH (10-100x improvement with correct index type)** + +Different index types excel at different query patterns. The default B-tree isn't always optimal. + +**Incorrect (B-tree for JSONB containment):** + +```sql +-- B-tree cannot optimize containment operators +create index products_attrs_idx on products (attributes); +select * from products where attributes @> '{"color": "red"}'; +-- Full table scan - B-tree doesn't support @> operator +``` + +**Correct (GIN for JSONB):** + +```sql +-- GIN supports @>, ?, ?&, ?| operators +create index products_attrs_idx on products using gin (attributes); +select * from products where attributes @> '{"color": "red"}'; +-- B-tree (default): =, <, >, BETWEEN, IN, IS NULL +create index users_created_idx on users (created_at); + +-- GIN: arrays, JSONB, full-text search +create index posts_tags_idx on posts using gin (tags); + +-- BRIN: large time-series tables (10-100x smaller) +create index events_time_idx on events using brin (created_at); + +-- Hash: equality-only (slightly faster than B-tree for =) +create index sessions_token_idx on sessions using hash (token); +``` + +Index type guide: + +Reference: https://www.postgresql.org/docs/current/indexes-types.html + +--- + +### 1.3 Create Composite Indexes for Multi-Column Queries + +**Impact: HIGH (5-10x faster multi-column queries)** + +When queries filter on multiple columns, a composite index is more efficient than separate single-column indexes. + +**Incorrect (separate indexes require bitmap scan):** + +```sql +-- Two separate indexes +create index orders_status_idx on orders (status); +create index orders_created_idx on orders (created_at); + +-- Query must combine both indexes (slower) +select * from orders where status = 'pending' and created_at > '2024-01-01'; +``` + +**Correct (composite index):** + +```sql +-- Single composite index (leftmost column first for equality checks) +create index orders_status_created_idx on orders (status, created_at); + +-- Query uses one efficient index scan +select * from orders where status = 'pending' and created_at > '2024-01-01'; +-- Good: status (=) before created_at (>) +create index idx on orders (status, created_at); + +-- Works for: WHERE status = 'pending' +-- Works for: WHERE status = 'pending' AND created_at > '2024-01-01' +-- Does NOT work for: WHERE created_at > '2024-01-01' (leftmost prefix rule) +``` + +**Column order matters** - place equality columns first, range columns last: + +Reference: https://www.postgresql.org/docs/current/indexes-multicolumn.html + +--- + +### 1.4 Use Covering Indexes to Avoid Table Lookups + +**Impact: MEDIUM-HIGH (2-5x faster queries by eliminating heap fetches)** + +Covering indexes include all columns needed by a query, enabling index-only scans that skip the table entirely. + +**Incorrect (index scan + heap fetch):** + +```sql +create index users_email_idx on users (email); + +-- Must fetch name and created_at from table heap +select email, name, created_at from users where email = 'user@example.com'; +``` + +**Correct (index-only scan with INCLUDE):** + +```sql +-- Include non-searchable columns in the index +create index users_email_idx on users (email) include (name, created_at); + +-- All columns served from index, no table access needed +select email, name, created_at from users where email = 'user@example.com'; +-- Searching by status, but also need customer_id and total +create index orders_status_idx on orders (status) include (customer_id, total); + +select status, customer_id, total from orders where status = 'shipped'; +``` + +Use INCLUDE for columns you SELECT but don't filter on: + +Reference: https://www.postgresql.org/docs/current/indexes-index-only-scans.html + +--- + +### 1.5 Use Partial Indexes for Filtered Queries + +**Impact: HIGH (5-20x smaller indexes, faster writes and queries)** + +Partial indexes only include rows matching a WHERE condition, making them smaller and faster when queries consistently filter on the same condition. + +**Incorrect (full index includes irrelevant rows):** + +```sql +-- Index includes all rows, even soft-deleted ones +create index users_email_idx on users (email); + +-- Query always filters active users +select * from users where email = 'user@example.com' and deleted_at is null; +``` + +**Correct (partial index matches query filter):** + +```sql +-- Index only includes active users +create index users_active_email_idx on users (email) +where deleted_at is null; + +-- Query uses the smaller, faster index +select * from users where email = 'user@example.com' and deleted_at is null; +-- Only pending orders (status rarely changes once completed) +create index orders_pending_idx on orders (created_at) +where status = 'pending'; + +-- Only non-null values +create index products_sku_idx on products (sku) +where sku is not null; +``` + +Common use cases for partial indexes: + +Reference: https://www.postgresql.org/docs/current/indexes-partial.html + +--- + +## 2. Connection Management + +**Impact: CRITICAL** + +Connection pooling, limits, and serverless strategies. Critical for applications with high concurrency or serverless deployments. + +### 2.1 Configure Idle Connection Timeouts + +**Impact: HIGH (Reclaim 30-50% of connection slots from idle clients)** + +Idle connections waste resources. Configure timeouts to automatically reclaim them. + +**Incorrect (connections held indefinitely):** + +```sql +-- No timeout configured +show idle_in_transaction_session_timeout; -- 0 (disabled) + +-- Connections stay open forever, even when idle +select pid, state, state_change, query +from pg_stat_activity +where state = 'idle in transaction'; +-- Shows transactions idle for hours, holding locks +``` + +**Correct (automatic cleanup of idle connections):** + +```ini +-- Terminate connections idle in transaction after 30 seconds +alter system set idle_in_transaction_session_timeout = '30s'; + +-- Terminate completely idle connections after 10 minutes +alter system set idle_session_timeout = '10min'; + +-- Reload configuration +select pg_reload_conf(); +# pgbouncer.ini +server_idle_timeout = 60 +client_idle_timeout = 300 +``` + +For pooled connections, configure at the pooler level: + +Reference: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-IDLE-IN-TRANSACTION-SESSION-TIMEOUT + +--- + +### 2.2 Set Appropriate Connection Limits + +**Impact: CRITICAL (Prevent database crashes and memory exhaustion)** + +Too many connections exhaust memory and degrade performance. Set limits based on available resources. + +**Incorrect (unlimited or excessive connections):** + +```sql +-- Default max_connections = 100, but often increased blindly +show max_connections; -- 500 (way too high for 4GB RAM) + +-- Each connection uses 1-3MB RAM +-- 500 connections * 2MB = 1GB just for connections! +-- Out of memory errors under load +``` + +**Correct (calculate based on resources):** + +```sql +-- Formula: max_connections = (RAM in MB / 5MB per connection) - reserved +-- For 4GB RAM: (4096 / 5) - 10 = ~800 theoretical max +-- But practically, 100-200 is better for query performance + +-- Recommended settings for 4GB RAM +alter system set max_connections = 100; + +-- Also set work_mem appropriately +-- work_mem * max_connections should not exceed 25% of RAM +alter system set work_mem = '8MB'; -- 8MB * 100 = 800MB max +select count(*), state from pg_stat_activity group by state; +``` + +Monitor connection usage: + +Reference: https://supabase.com/docs/guides/platform/performance#connection-management + +--- + +### 2.3 Use Connection Pooling for All Applications + +**Impact: CRITICAL (Handle 10-100x more concurrent users)** + +Postgres connections are expensive (1-3MB RAM each). Without pooling, applications exhaust connections under load. + +**Incorrect (new connection per request):** + +```sql +-- Each request creates a new connection +-- Application code: db.connect() per request +-- Result: 500 concurrent users = 500 connections = crashed database + +-- Check current connections +select count(*) from pg_stat_activity; -- 487 connections! +``` + +**Correct (connection pooling):** + +```sql +-- Use a pooler like PgBouncer between app and database +-- Application connects to pooler, pooler reuses a small pool to Postgres + +-- Configure pool_size based on: (CPU cores * 2) + spindle_count +-- Example for 4 cores: pool_size = 10 + +-- Result: 500 concurrent users share 10 actual connections +select count(*) from pg_stat_activity; -- 10 connections +``` + +Pool modes: +- **Transaction mode**: connection returned after each transaction (best for most apps) +- **Session mode**: connection held for entire session (needed for prepared statements, temp tables) + +Reference: https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pooler + +--- + +### 2.4 Use Prepared Statements Correctly with Pooling + +**Impact: HIGH (Avoid prepared statement conflicts in pooled environments)** + +Prepared statements are tied to individual database connections. In transaction-mode pooling, connections are shared, causing conflicts. + +**Incorrect (named prepared statements with transaction pooling):** + +```sql +-- Named prepared statement +prepare get_user as select * from users where id = $1; + +-- In transaction mode pooling, next request may get different connection +execute get_user(123); +-- ERROR: prepared statement "get_user" does not exist +``` + +**Correct (use unnamed statements or session mode):** + +```sql +-- Option 1: Use unnamed prepared statements (most ORMs do this automatically) +-- The query is prepared and executed in a single protocol message + +-- Option 2: Deallocate after use in transaction mode +prepare get_user as select * from users where id = $1; +execute get_user(123); +deallocate get_user; + +-- Option 3: Use session mode pooling (port 5432 vs 6543) +-- Connection is held for entire session, prepared statements persist +-- Many drivers use prepared statements by default +-- Node.js pg: { prepare: false } to disable +-- JDBC: prepareThreshold=0 to disable +``` + +Check your driver settings: + +Reference: https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pool-modes + +--- + +## 3. Security & RLS + +**Impact: CRITICAL** + +Row-Level Security policies, privilege management, and authentication patterns. + +### 3.1 Apply Principle of Least Privilege + +**Impact: MEDIUM (Reduced attack surface, better audit trail)** + +Grant only the minimum permissions required. Never use superuser for application queries. + +**Incorrect (overly broad permissions):** + +```sql +-- Application uses superuser connection +-- Or grants ALL to application role +grant all privileges on all tables in schema public to app_user; +grant all privileges on all sequences in schema public to app_user; + +-- Any SQL injection becomes catastrophic +-- drop table users; cascades to everything +``` + +**Correct (minimal, specific grants):** + +```sql +-- Create role with no default privileges +create role app_readonly nologin; + +-- Grant only SELECT on specific tables +grant usage on schema public to app_readonly; +grant select on public.products, public.categories to app_readonly; + +-- Create role for writes with limited scope +create role app_writer nologin; +grant usage on schema public to app_writer; +grant select, insert, update on public.orders to app_writer; +grant usage on sequence orders_id_seq to app_writer; +-- No DELETE permission + +-- Login role inherits from these +create role app_user login password 'xxx'; +grant app_writer to app_user; +-- Revoke default public access +revoke all on schema public from public; +revoke all on all tables in schema public from public; +``` + +Revoke public defaults: + +Reference: https://supabase.com/blog/postgres-roles-and-privileges + +--- + +### 3.2 Enable Row Level Security for Multi-Tenant Data + +**Impact: CRITICAL (Database-enforced tenant isolation, prevent data leaks)** + +Row Level Security (RLS) enforces data access at the database level, ensuring users only see their own data. + +**Incorrect (application-level filtering only):** + +```sql +-- Relying only on application to filter +select * from orders where user_id = $current_user_id; + +-- Bug or bypass means all data is exposed! +select * from orders; -- Returns ALL orders +``` + +**Correct (database-enforced RLS):** + +```sql +-- Enable RLS on the table +alter table orders enable row level security; + +-- Create policy for users to see only their orders +create policy orders_user_policy on orders + for all + using (user_id = current_setting('app.current_user_id')::bigint); + +-- Force RLS even for table owners +alter table orders force row level security; + +-- Set user context and query +set app.current_user_id = '123'; +select * from orders; -- Only returns orders for user 123 +create policy orders_user_policy on orders + for all + to authenticated + using (user_id = auth.uid()); +``` + +Policy for authenticated role: + +Reference: https://supabase.com/docs/guides/database/postgres/row-level-security + +--- + +### 3.3 Optimize RLS Policies for Performance + +**Impact: HIGH (5-10x faster RLS queries with proper patterns)** + +Poorly written RLS policies can cause severe performance issues. Use subqueries and indexes strategically. + +**Incorrect (function called for every row):** + +```sql +create policy orders_policy on orders + using (auth.uid() = user_id); -- auth.uid() called per row! + +-- With 1M rows, auth.uid() is called 1M times +``` + +**Correct (wrap functions in SELECT):** + +```sql +create policy orders_policy on orders + using ((select auth.uid()) = user_id); -- Called once, cached + +-- 100x+ faster on large tables +-- Create helper function (runs as definer, bypasses RLS) +create or replace function is_team_member(team_id bigint) +returns boolean +language sql +security definer +set search_path = '' +as $$ + select exists ( + select 1 from public.team_members + where team_id = $1 and user_id = (select auth.uid()) + ); +$$; + +-- Use in policy (indexed lookup, not per-row check) +create policy team_orders_policy on orders + using ((select is_team_member(team_id))); +create index orders_user_id_idx on orders (user_id); +``` + +Use security definer functions for complex checks: +Always add indexes on columns used in RLS policies: + +Reference: https://supabase.com/docs/guides/database/postgres/row-level-security#rls-performance-recommendations + +--- + +## 4. Schema Design + +**Impact: HIGH** + +Table design, index strategies, partitioning, and data type selection. Foundation for long-term performance. + +### 4.1 Choose Appropriate Data Types + +**Impact: HIGH (50% storage reduction, faster comparisons)** + +Using the right data types reduces storage, improves query performance, and prevents bugs. + +**Incorrect (wrong data types):** + +```sql +create table users ( + id int, -- Will overflow at 2.1 billion + email varchar(255), -- Unnecessary length limit + created_at timestamp, -- Missing timezone info + is_active varchar(5), -- String for boolean + price varchar(20) -- String for numeric +); +``` + +**Correct (appropriate data types):** + +```sql +create table users ( + id bigint generated always as identity primary key, -- 9 quintillion max + email text, -- No artificial limit, same performance as varchar + created_at timestamptz, -- Always store timezone-aware timestamps + is_active boolean default true, -- 1 byte vs variable string length + price numeric(10,2) -- Exact decimal arithmetic +); +-- IDs: use bigint, not int (future-proofing) +-- Strings: use text, not varchar(n) unless constraint needed +-- Time: use timestamptz, not timestamp +-- Money: use numeric, not float (precision matters) +-- Enums: use text with check constraint or create enum type +``` + +Key guidelines: + +Reference: https://www.postgresql.org/docs/current/datatype.html + +--- + +### 4.2 Index Foreign Key Columns + +**Impact: HIGH (10-100x faster JOINs and CASCADE operations)** + +Postgres does not automatically index foreign key columns. Missing indexes cause slow JOINs and CASCADE operations. + +**Incorrect (unindexed foreign key):** + +```sql +create table orders ( + id bigint generated always as identity primary key, + customer_id bigint references customers(id) on delete cascade, + total numeric(10,2) +); + +-- No index on customer_id! +-- JOINs and ON DELETE CASCADE both require full table scan +select * from orders where customer_id = 123; -- Seq Scan +delete from customers where id = 123; -- Locks table, scans all orders +``` + +**Correct (indexed foreign key):** + +```sql +create table orders ( + id bigint generated always as identity primary key, + customer_id bigint references customers(id) on delete cascade, + total numeric(10,2) +); + +-- Always index the FK column +create index orders_customer_id_idx on orders (customer_id); + +-- Now JOINs and cascades are fast +select * from orders where customer_id = 123; -- Index Scan +delete from customers where id = 123; -- Uses index, fast cascade +select + conrelid::regclass as table_name, + a.attname as fk_column +from pg_constraint c +join pg_attribute a on a.attrelid = c.conrelid and a.attnum = any(c.conkey) +where c.contype = 'f' + and not exists ( + select 1 from pg_index i + where i.indrelid = c.conrelid and a.attnum = any(i.indkey) + ); +``` + +Find missing FK indexes: + +Reference: https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-FK + +--- + +### 4.3 Partition Large Tables for Better Performance + +**Impact: MEDIUM-HIGH (5-20x faster queries and maintenance on large tables)** + +Partitioning splits a large table into smaller pieces, improving query performance and maintenance operations. + +**Incorrect (single large table):** + +```sql +create table events ( + id bigint generated always as identity, + created_at timestamptz, + data jsonb +); + +-- 500M rows, queries scan everything +select * from events where created_at > '2024-01-01'; -- Slow +vacuum events; -- Takes hours, locks table +``` + +**Correct (partitioned by time range):** + +```sql +create table events ( + id bigint generated always as identity, + created_at timestamptz not null, + data jsonb +) partition by range (created_at); + +-- Create partitions for each month +create table events_2024_01 partition of events + for values from ('2024-01-01') to ('2024-02-01'); + +create table events_2024_02 partition of events + for values from ('2024-02-01') to ('2024-03-01'); + +-- Queries only scan relevant partitions +select * from events where created_at > '2024-01-15'; -- Only scans events_2024_01+ + +-- Drop old data instantly +drop table events_2023_01; -- Instant vs DELETE taking hours +``` + +When to partition: +- Tables > 100M rows +- Time-series data with date-based queries +- Need to efficiently drop old data + +Reference: https://www.postgresql.org/docs/current/ddl-partitioning.html + +--- + +### 4.4 Select Optimal Primary Key Strategy + +**Impact: HIGH (Better index locality, reduced fragmentation)** + +Primary key choice affects insert performance, index size, and replication +efficiency. + +**Incorrect (problematic PK choices):** + +```sql +-- identity is the SQL-standard approach +create table users ( + id serial primary key -- Works, but IDENTITY is recommended +); + +-- Random UUIDs (v4) cause index fragmentation +create table orders ( + id uuid default gen_random_uuid() primary key -- UUIDv4 = random = scattered inserts +); +``` + +**Correct (optimal PK strategies):** + +```sql +-- Use IDENTITY for sequential IDs (SQL-standard, best for most cases) +create table users ( + id bigint generated always as identity primary key +); + +-- For distributed systems needing UUIDs, use UUIDv7 (time-ordered) +-- Requires pg_uuidv7 extension: create extension pg_uuidv7; +create table orders ( + id uuid default uuid_generate_v7() primary key -- Time-ordered, no fragmentation +); + +-- Alternative: time-prefixed IDs for sortable, distributed IDs (no extension needed) +create table events ( + id text default concat( + to_char(now() at time zone 'utc', 'YYYYMMDDHH24MISSMS'), + gen_random_uuid()::text + ) primary key +); +``` + +Guidelines: +- Single database: `bigint identity` (sequential, 8 bytes, SQL-standard) +- Distributed/exposed IDs: UUIDv7 (requires pg_uuidv7) or ULID (time-ordered, no + fragmentation) +- `serial` works but `identity` is SQL-standard and preferred for new + applications +- Avoid random UUIDs (v4) as primary keys on large tables (causes index + fragmentation) +[Identity Columns](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-GENERATED-IDENTITY) + +--- + +### 4.5 Use Lowercase Identifiers for Compatibility + +**Impact: MEDIUM (Avoid case-sensitivity bugs with tools, ORMs, and AI assistants)** + +PostgreSQL folds unquoted identifiers to lowercase. Quoted mixed-case identifiers require quotes forever and cause issues with tools, ORMs, and AI assistants that may not recognize them. + +**Incorrect (mixed-case identifiers):** + +```sql +-- Quoted identifiers preserve case but require quotes everywhere +CREATE TABLE "Users" ( + "userId" bigint PRIMARY KEY, + "firstName" text, + "lastName" text +); + +-- Must always quote or queries fail +SELECT "firstName" FROM "Users" WHERE "userId" = 1; + +-- This fails - Users becomes users without quotes +SELECT firstName FROM Users; +-- ERROR: relation "users" does not exist +``` + +**Correct (lowercase snake_case):** + +```sql +-- Unquoted lowercase identifiers are portable and tool-friendly +CREATE TABLE users ( + user_id bigint PRIMARY KEY, + first_name text, + last_name text +); + +-- Works without quotes, recognized by all tools +SELECT first_name FROM users WHERE user_id = 1; +-- ORMs often generate quoted camelCase - configure them to use snake_case +-- Migrations from other databases may preserve original casing +-- Some GUI tools quote identifiers by default - disable this + +-- If stuck with mixed-case, create views as a compatibility layer +CREATE VIEW users AS SELECT "userId" AS user_id, "firstName" AS first_name FROM "Users"; +``` + +Common sources of mixed-case identifiers: + +Reference: https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS + +--- + +## 5. Concurrency & Locking + +**Impact: MEDIUM-HIGH** + +Transaction management, isolation levels, deadlock prevention, and lock contention patterns. + +### 5.1 Keep Transactions Short to Reduce Lock Contention + +**Impact: MEDIUM-HIGH (3-5x throughput improvement, fewer deadlocks)** + +Long-running transactions hold locks that block other queries. Keep transactions as short as possible. + +**Incorrect (long transaction with external calls):** + +```sql +begin; +select * from orders where id = 1 for update; -- Lock acquired + +-- Application makes HTTP call to payment API (2-5 seconds) +-- Other queries on this row are blocked! + +update orders set status = 'paid' where id = 1; +commit; -- Lock held for entire duration +``` + +**Correct (minimal transaction scope):** + +```sql +-- Validate data and call APIs outside transaction +-- Application: response = await paymentAPI.charge(...) + +-- Only hold lock for the actual update +begin; +update orders +set status = 'paid', payment_id = $1 +where id = $2 and status = 'pending' +returning *; +commit; -- Lock held for milliseconds +-- Abort queries running longer than 30 seconds +set statement_timeout = '30s'; + +-- Or per-session +set local statement_timeout = '5s'; +``` + +Use `statement_timeout` to prevent runaway transactions: + +Reference: https://www.postgresql.org/docs/current/tutorial-transactions.html + +--- + +### 5.2 Prevent Deadlocks with Consistent Lock Ordering + +**Impact: MEDIUM-HIGH (Eliminate deadlock errors, improve reliability)** + +Deadlocks occur when transactions lock resources in different orders. Always +acquire locks in a consistent order. + +**Incorrect (inconsistent lock ordering):** + +```sql +-- Transaction A -- Transaction B +begin; begin; +update accounts update accounts +set balance = balance - 100 set balance = balance - 50 +where id = 1; where id = 2; -- B locks row 2 + +update accounts update accounts +set balance = balance + 100 set balance = balance + 50 +where id = 2; -- A waits for B where id = 1; -- B waits for A + +-- DEADLOCK! Both waiting for each other +``` + +**Correct (lock rows in consistent order first):** + +```sql +-- Explicitly acquire locks in ID order before updating +begin; +select * from accounts where id in (1, 2) order by id for update; + +-- Now perform updates in any order - locks already held +update accounts set balance = balance - 100 where id = 1; +update accounts set balance = balance + 100 where id = 2; +commit; +-- Single statement acquires all locks atomically +begin; +update accounts +set balance = balance + case id + when 1 then -100 + when 2 then 100 +end +where id in (1, 2); +commit; +-- Check for recent deadlocks +select * from pg_stat_database where deadlocks > 0; + +-- Enable deadlock logging +set log_lock_waits = on; +set deadlock_timeout = '1s'; +``` + +Alternative: use a single statement to update atomically: +Detect deadlocks in logs: +[Deadlocks](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-DEADLOCKS) + +--- + +### 5.3 Use Advisory Locks for Application-Level Locking + +**Impact: MEDIUM (Efficient coordination without row-level lock overhead)** + +Advisory locks provide application-level coordination without requiring database rows to lock. + +**Incorrect (creating rows just for locking):** + +```sql +-- Creating dummy rows to lock on +create table resource_locks ( + resource_name text primary key +); + +insert into resource_locks values ('report_generator'); + +-- Lock by selecting the row +select * from resource_locks where resource_name = 'report_generator' for update; +``` + +**Correct (advisory locks):** + +```sql +-- Session-level advisory lock (released on disconnect or unlock) +select pg_advisory_lock(hashtext('report_generator')); +-- ... do exclusive work ... +select pg_advisory_unlock(hashtext('report_generator')); + +-- Transaction-level lock (released on commit/rollback) +begin; +select pg_advisory_xact_lock(hashtext('daily_report')); +-- ... do work ... +commit; -- Lock automatically released +-- Returns immediately with true/false instead of waiting +select pg_try_advisory_lock(hashtext('resource_name')); + +-- Use in application +if (acquired) { + -- Do work + select pg_advisory_unlock(hashtext('resource_name')); +} else { + -- Skip or retry later +} +``` + +Try-lock for non-blocking operations: + +Reference: https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS + +--- + +### 5.4 Use SKIP LOCKED for Non-Blocking Queue Processing + +**Impact: MEDIUM-HIGH (10x throughput for worker queues)** + +When multiple workers process a queue, SKIP LOCKED allows workers to process different rows without waiting. + +**Incorrect (workers block each other):** + +```sql +-- Worker 1 and Worker 2 both try to get next job +begin; +select * from jobs where status = 'pending' order by created_at limit 1 for update; +-- Worker 2 waits for Worker 1's lock to release! +``` + +**Correct (SKIP LOCKED for parallel processing):** + +```sql +-- Each worker skips locked rows and gets the next available +begin; +select * from jobs +where status = 'pending' +order by created_at +limit 1 +for update skip locked; + +-- Worker 1 gets job 1, Worker 2 gets job 2 (no waiting) + +update jobs set status = 'processing' where id = $1; +commit; +-- Atomic claim-and-update in one statement +update jobs +set status = 'processing', worker_id = $1, started_at = now() +where id = ( + select id from jobs + where status = 'pending' + order by created_at + limit 1 + for update skip locked +) +returning *; +``` + +Complete queue pattern: + +Reference: https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE + +--- + +## 6. Data Access Patterns + +**Impact: MEDIUM** + +N+1 query elimination, batch operations, cursor-based pagination, and efficient data fetching. + +### 6.1 Batch INSERT Statements for Bulk Data + +**Impact: MEDIUM (10-50x faster bulk inserts)** + +Individual INSERT statements have high overhead. Batch multiple rows in single statements or use COPY. + +**Incorrect (individual inserts):** + +```sql +-- Each insert is a separate transaction and round trip +insert into events (user_id, action) values (1, 'click'); +insert into events (user_id, action) values (1, 'view'); +insert into events (user_id, action) values (2, 'click'); +-- ... 1000 more individual inserts + +-- 1000 inserts = 1000 round trips = slow +``` + +**Correct (batch insert):** + +```sql +-- Multiple rows in single statement +insert into events (user_id, action) values + (1, 'click'), + (1, 'view'), + (2, 'click'), + -- ... up to ~1000 rows per batch + (999, 'view'); + +-- One round trip for 1000 rows +-- COPY is fastest for bulk loading +copy events (user_id, action, created_at) +from '/path/to/data.csv' +with (format csv, header true); + +-- Or from stdin in application +copy events (user_id, action) from stdin with (format csv); +1,click +1,view +2,click +\. +``` + +For large imports, use COPY: + +Reference: https://www.postgresql.org/docs/current/sql-copy.html + +--- + +### 6.2 Eliminate N+1 Queries with Batch Loading + +**Impact: MEDIUM-HIGH (10-100x fewer database round trips)** + +N+1 queries execute one query per item in a loop. Batch them into a single query using arrays or JOINs. + +**Incorrect (N+1 queries):** + +```sql +-- First query: get all users +select id from users where active = true; -- Returns 100 IDs + +-- Then N queries, one per user +select * from orders where user_id = 1; +select * from orders where user_id = 2; +select * from orders where user_id = 3; +-- ... 97 more queries! + +-- Total: 101 round trips to database +``` + +**Correct (single batch query):** + +```sql +-- Collect IDs and query once with ANY +select * from orders where user_id = any(array[1, 2, 3, ...]); + +-- Or use JOIN instead of loop +select u.id, u.name, o.* +from users u +left join orders o on o.user_id = u.id +where u.active = true; + +-- Total: 1 round trip +-- Instead of looping in application code: +-- for user in users: db.query("SELECT * FROM orders WHERE user_id = $1", user.id) + +-- Pass array parameter: +select * from orders where user_id = any($1::bigint[]); +-- Application passes: [1, 2, 3, 4, 5, ...] +``` + +Application pattern: + +Reference: https://supabase.com/docs/guides/database/query-optimization + +--- + +### 6.3 Use Cursor-Based Pagination Instead of OFFSET + +**Impact: MEDIUM-HIGH (Consistent O(1) performance regardless of page depth)** + +OFFSET-based pagination scans all skipped rows, getting slower on deeper pages. Cursor pagination is O(1). + +**Incorrect (OFFSET pagination):** + +```sql +-- Page 1: scans 20 rows +select * from products order by id limit 20 offset 0; + +-- Page 100: scans 2000 rows to skip 1980 +select * from products order by id limit 20 offset 1980; + +-- Page 10000: scans 200,000 rows! +select * from products order by id limit 20 offset 199980; +``` + +**Correct (cursor/keyset pagination):** + +```sql +-- Page 1: get first 20 +select * from products order by id limit 20; +-- Application stores last_id = 20 + +-- Page 2: start after last ID +select * from products where id > 20 order by id limit 20; +-- Uses index, always fast regardless of page depth + +-- Page 10000: same speed as page 1 +select * from products where id > 199980 order by id limit 20; +-- Cursor must include all sort columns +select * from products +where (created_at, id) > ('2024-01-15 10:00:00', 12345) +order by created_at, id +limit 20; +``` + +For multi-column sorting: + +Reference: https://supabase.com/docs/guides/database/pagination + +--- + +### 6.4 Use UPSERT for Insert-or-Update Operations + +**Impact: MEDIUM (Atomic operation, eliminates race conditions)** + +Using separate SELECT-then-INSERT/UPDATE creates race conditions. Use INSERT ... ON CONFLICT for atomic upserts. + +**Incorrect (check-then-insert race condition):** + +```sql +-- Race condition: two requests check simultaneously +select * from settings where user_id = 123 and key = 'theme'; +-- Both find nothing + +-- Both try to insert +insert into settings (user_id, key, value) values (123, 'theme', 'dark'); +-- One succeeds, one fails with duplicate key error! +``` + +**Correct (atomic UPSERT):** + +```sql +-- Single atomic operation +insert into settings (user_id, key, value) +values (123, 'theme', 'dark') +on conflict (user_id, key) +do update set value = excluded.value, updated_at = now(); + +-- Returns the inserted/updated row +insert into settings (user_id, key, value) +values (123, 'theme', 'dark') +on conflict (user_id, key) +do update set value = excluded.value +returning *; +-- Insert only if not exists (no update) +insert into page_views (page_id, user_id) +values (1, 123) +on conflict (page_id, user_id) do nothing; +``` + +Insert-or-ignore pattern: + +Reference: https://www.postgresql.org/docs/current/sql-insert.html#SQL-ON-CONFLICT + +--- + +## 7. Monitoring & Diagnostics + +**Impact: LOW-MEDIUM** + +Using pg_stat_statements, EXPLAIN ANALYZE, metrics collection, and performance diagnostics. + +### 7.1 Enable pg_stat_statements for Query Analysis + +**Impact: LOW-MEDIUM (Identify top resource-consuming queries)** + +pg_stat_statements tracks execution statistics for all queries, helping identify slow and frequent queries. + +**Incorrect (no visibility into query patterns):** + +```sql +-- Database is slow, but which queries are the problem? +-- No way to know without pg_stat_statements +``` + +**Correct (enable and query pg_stat_statements):** + +```sql +-- Enable the extension +create extension if not exists pg_stat_statements; + +-- Find slowest queries by total time +select + calls, + round(total_exec_time::numeric, 2) as total_time_ms, + round(mean_exec_time::numeric, 2) as mean_time_ms, + query +from pg_stat_statements +order by total_exec_time desc +limit 10; + +-- Find most frequent queries +select calls, query +from pg_stat_statements +order by calls desc +limit 10; + +-- Reset statistics after optimization +select pg_stat_statements_reset(); +-- Queries with high mean time (candidates for optimization) +select query, mean_exec_time, calls +from pg_stat_statements +where mean_exec_time > 100 -- > 100ms average +order by mean_exec_time desc; +``` + +Key metrics to monitor: + +Reference: https://supabase.com/docs/guides/database/extensions/pg_stat_statements + +--- + +### 7.2 Maintain Table Statistics with VACUUM and ANALYZE + +**Impact: MEDIUM (2-10x better query plans with accurate statistics)** + +Outdated statistics cause the query planner to make poor decisions. VACUUM reclaims space, ANALYZE updates statistics. + +**Incorrect (stale statistics):** + +```sql +-- Table has 1M rows but stats say 1000 +-- Query planner chooses wrong strategy +explain select * from orders where status = 'pending'; +-- Shows: Seq Scan (because stats show small table) +-- Actually: Index Scan would be much faster +``` + +**Correct (maintain fresh statistics):** + +```sql +-- Manually analyze after large data changes +analyze orders; + +-- Analyze specific columns used in WHERE clauses +analyze orders (status, created_at); + +-- Check when tables were last analyzed +select + relname, + last_vacuum, + last_autovacuum, + last_analyze, + last_autoanalyze +from pg_stat_user_tables +order by last_analyze nulls first; +-- Increase frequency for high-churn tables +alter table orders set ( + autovacuum_vacuum_scale_factor = 0.05, -- Vacuum at 5% dead tuples (default 20%) + autovacuum_analyze_scale_factor = 0.02 -- Analyze at 2% changes (default 10%) +); + +-- Check autovacuum status +select * from pg_stat_progress_vacuum; +``` + +Autovacuum tuning for busy tables: + +Reference: https://supabase.com/docs/guides/database/database-size#vacuum-operations + +--- + +### 7.3 Use EXPLAIN ANALYZE to Diagnose Slow Queries + +**Impact: LOW-MEDIUM (Identify exact bottlenecks in query execution)** + +EXPLAIN ANALYZE executes the query and shows actual timings, revealing the true performance bottlenecks. + +**Incorrect (guessing at performance issues):** + +```sql +-- Query is slow, but why? +select * from orders where customer_id = 123 and status = 'pending'; +-- "It must be missing an index" - but which one? +``` + +**Correct (use EXPLAIN ANALYZE):** + +```sql +explain (analyze, buffers, format text) +select * from orders where customer_id = 123 and status = 'pending'; + +-- Output reveals the issue: +-- Seq Scan on orders (cost=0.00..25000.00 rows=50 width=100) (actual time=0.015..450.123 rows=50 loops=1) +-- Filter: ((customer_id = 123) AND (status = 'pending'::text)) +-- Rows Removed by Filter: 999950 +-- Buffers: shared hit=5000 read=15000 +-- Planning Time: 0.150 ms +-- Execution Time: 450.500 ms +-- Seq Scan on large tables = missing index +-- Rows Removed by Filter = poor selectivity or missing index +-- Buffers: read >> hit = data not cached, needs more memory +-- Nested Loop with high loops = consider different join strategy +-- Sort Method: external merge = work_mem too low +``` + +Key things to look for: + +Reference: https://supabase.com/docs/guides/database/inspect + +--- + +## 8. Advanced Features + +**Impact: LOW** + +Full-text search, JSONB optimization, PostGIS, extensions, and advanced Postgres features. + +### 8.1 Index JSONB Columns for Efficient Querying + +**Impact: MEDIUM (10-100x faster JSONB queries with proper indexing)** + +JSONB queries without indexes scan the entire table. Use GIN indexes for containment queries. + +**Incorrect (no index on JSONB):** + +```sql +create table products ( + id bigint primary key, + attributes jsonb +); + +-- Full table scan for every query +select * from products where attributes @> '{"color": "red"}'; +select * from products where attributes->>'brand' = 'Nike'; +``` + +**Correct (GIN index for JSONB):** + +```sql +-- GIN index for containment operators (@>, ?, ?&, ?|) +create index products_attrs_gin on products using gin (attributes); + +-- Now containment queries use the index +select * from products where attributes @> '{"color": "red"}'; + +-- For specific key lookups, use expression index +create index products_brand_idx on products ((attributes->>'brand')); +select * from products where attributes->>'brand' = 'Nike'; +-- jsonb_ops (default): supports all operators, larger index +create index idx1 on products using gin (attributes); + +-- jsonb_path_ops: only @> operator, but 2-3x smaller index +create index idx2 on products using gin (attributes jsonb_path_ops); +``` + +Choose the right operator class: + +Reference: https://www.postgresql.org/docs/current/datatype-json.html#JSON-INDEXING + +--- + +### 8.2 Use tsvector for Full-Text Search + +**Impact: MEDIUM (100x faster than LIKE, with ranking support)** + +LIKE with wildcards can't use indexes. Full-text search with tsvector is orders of magnitude faster. + +**Incorrect (LIKE pattern matching):** + +```sql +-- Cannot use index, scans all rows +select * from articles where content like '%postgresql%'; + +-- Case-insensitive makes it worse +select * from articles where lower(content) like '%postgresql%'; +``` + +**Correct (full-text search with tsvector):** + +```sql +-- Add tsvector column and index +alter table articles add column search_vector tsvector + generated always as (to_tsvector('english', coalesce(title,'') || ' ' || coalesce(content,''))) stored; + +create index articles_search_idx on articles using gin (search_vector); + +-- Fast full-text search +select * from articles +where search_vector @@ to_tsquery('english', 'postgresql & performance'); + +-- With ranking +select *, ts_rank(search_vector, query) as rank +from articles, to_tsquery('english', 'postgresql') query +where search_vector @@ query +order by rank desc; +-- AND: both terms required +to_tsquery('postgresql & performance') + +-- OR: either term +to_tsquery('postgresql | mysql') + +-- Prefix matching +to_tsquery('post:*') +``` + +Search multiple terms: + +Reference: https://supabase.com/docs/guides/database/full-text-search + +--- + +## References + +- https://www.postgresql.org/docs/current/ +- https://supabase.com/docs +- https://wiki.postgresql.org/wiki/Performance_Optimization +- https://supabase.com/docs/guides/database/overview +- https://supabase.com/docs/guides/auth/row-level-security diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/README.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/README.md new file mode 100644 index 00000000..474a26e4 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/README.md @@ -0,0 +1,119 @@ +# Postgres Best Practices - Contributor Guide + +This repository contains Postgres performance optimization rules optimized for +AI agents and LLMs. + +## Quick Start + +```bash +# Install dependencies +cd packages/postgres-best-practices-build +npm install + +# Validate existing rules +npm run validate + +# Build AGENTS.md +npm run build +``` + +## Creating a New Rule + +1. **Choose a section prefix** based on the category: + - `query-` Query Performance (CRITICAL) + - `conn-` Connection Management (CRITICAL) + - `security-` Security & RLS (CRITICAL) + - `schema-` Schema Design (HIGH) + - `lock-` Concurrency & Locking (MEDIUM-HIGH) + - `data-` Data Access Patterns (MEDIUM) + - `monitor-` Monitoring & Diagnostics (LOW-MEDIUM) + - `advanced-` Advanced Features (LOW) + +2. **Copy the template**: + ```bash + cp rules/_template.md rules/query-your-rule-name.md + ``` + +3. **Fill in the content** following the template structure + +4. **Validate and build**: + ```bash + npm run validate + npm run build + ``` + +5. **Review** the generated `AGENTS.md` + +## Repository Structure + +``` +skills/postgres-best-practices/ +โ”œโ”€โ”€ SKILL.md # Agent-facing skill manifest +โ”œโ”€โ”€ AGENTS.md # [GENERATED] Compiled rules document +โ”œโ”€โ”€ README.md # This file +โ”œโ”€โ”€ metadata.json # Version and metadata +โ””โ”€โ”€ rules/ + โ”œโ”€โ”€ _template.md # Rule template + โ”œโ”€โ”€ _sections.md # Section definitions + โ”œโ”€โ”€ _contributing.md # Writing guidelines + โ””โ”€โ”€ *.md # Individual rules + +packages/postgres-best-practices-build/ +โ”œโ”€โ”€ src/ # Build system source +โ”œโ”€โ”€ package.json # NPM scripts +โ””โ”€โ”€ test-cases.json # [GENERATED] Test artifacts +``` + +## Rule File Structure + +See `rules/_template.md` for the complete template. Key elements: + +````markdown +--- +title: Clear, Action-Oriented Title +impact: CRITICAL|HIGH|MEDIUM-HIGH|MEDIUM|LOW-MEDIUM|LOW +impactDescription: Quantified benefit (e.g., "10-100x faster") +tags: relevant, keywords +--- + +## [Title] + +[1-2 sentence explanation] + +**Incorrect (description):** + +```sql +-- Comment explaining what's wrong +[Bad SQL example] +``` +```` + +**Correct (description):** + +```sql +-- Comment explaining why this is better +[Good SQL example] +``` + +``` +## Writing Guidelines + +See `rules/_contributing.md` for detailed guidelines. Key principles: + +1. **Show concrete transformations** - "Change X to Y", not abstract advice +2. **Error-first structure** - Show the problem before the solution +3. **Quantify impact** - Include specific metrics (10x faster, 50% smaller) +4. **Self-contained examples** - Complete, runnable SQL +5. **Semantic naming** - Use meaningful names (users, email), not (table1, col1) + +## Impact Levels + +| Level | Improvement | Examples | +|-------|-------------|----------| +| CRITICAL | 10-100x | Missing indexes, connection exhaustion | +| HIGH | 5-20x | Wrong index types, poor partitioning | +| MEDIUM-HIGH | 2-5x | N+1 queries, RLS optimization | +| MEDIUM | 1.5-3x | Redundant indexes, stale statistics | +| LOW-MEDIUM | 1.2-2x | VACUUM tuning, config tweaks | +| LOW | Incremental | Advanced patterns, edge cases | +``` diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/SKILL.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/SKILL.md new file mode 100644 index 00000000..3365f2d6 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/SKILL.md @@ -0,0 +1,58 @@ +--- +name: postgres-best-practices +description: "Postgres performance optimization and best practices from Supabase. Use this skill when writing, reviewing, or optimizing Postgres queries, schema designs, or database configurations." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Supabase Postgres Best Practices + +Comprehensive performance optimization guide for Postgres, maintained by Supabase. Contains rules across 8 categories, prioritized by impact to guide automated query optimization and schema design. + +## When to Use +Reference these guidelines when: +- Writing SQL queries or designing schemas +- Implementing indexes or query optimization +- Reviewing database performance issues +- Configuring connection pooling or scaling +- Optimizing for Postgres-specific features +- Working with Row-Level Security (RLS) + +## Rule Categories by Priority + +| Priority | Category | Impact | Prefix | +|----------|----------|--------|--------| +| 1 | Query Performance | CRITICAL | `query-` | +| 2 | Connection Management | CRITICAL | `conn-` | +| 3 | Security & RLS | CRITICAL | `security-` | +| 4 | Schema Design | HIGH | `schema-` | +| 5 | Concurrency & Locking | MEDIUM-HIGH | `lock-` | +| 6 | Data Access Patterns | MEDIUM | `data-` | +| 7 | Monitoring & Diagnostics | LOW-MEDIUM | `monitor-` | +| 8 | Advanced Features | LOW | `advanced-` | + +## How to Use + +Read individual rule files for detailed explanations and SQL examples: + +``` +rules/query-missing-indexes.md +rules/schema-partial-indexes.md +rules/_sections.md +``` + +Each rule file contains: +- Brief explanation of why it matters +- Incorrect SQL example with explanation +- Correct SQL example with explanation +- Optional EXPLAIN output or metrics +- Additional context and references +- Supabase-specific notes (when applicable) + +## Full Compiled Document + +For the complete guide with all rules expanded: `AGENTS.md` + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/metadata.json b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/metadata.json new file mode 100644 index 00000000..1cd5f0a0 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/metadata.json @@ -0,0 +1,13 @@ +{ + "version": "1.0.0", + "organization": "Supabase", + "date": "January 2026", + "abstract": "Comprehensive Postgres performance optimization guide for developers using Supabase and Postgres. Contains performance rules across 8 categories, prioritized by impact from critical (query performance, connection management) to incremental (advanced features). Each rule includes detailed explanations, incorrect vs. correct SQL examples, query plan analysis, and specific performance metrics to guide automated optimization and code generation.", + "references": [ + "https://www.postgresql.org/docs/current/", + "https://supabase.com/docs", + "https://wiki.postgresql.org/wiki/Performance_Optimization", + "https://supabase.com/docs/guides/database/overview", + "https://supabase.com/docs/guides/auth/row-level-security" + ] +} diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/_contributing.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/_contributing.md new file mode 100644 index 00000000..99686a9b --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/_contributing.md @@ -0,0 +1,171 @@ +# Writing Guidelines for Postgres Rules + +This document provides guidelines for creating effective Postgres best +practice rules that work well with AI agents and LLMs. + +## Key Principles + +### 1. Concrete Transformation Patterns + +Show exact SQL rewrites. Avoid philosophical advice. + +**Good:** "Use `WHERE id = ANY(ARRAY[...])` instead of +`WHERE id IN (SELECT ...)`" **Bad:** "Design good schemas" + +### 2. Error-First Structure + +Always show the problematic pattern first, then the solution. This trains agents +to recognize anti-patterns. + +```markdown +**Incorrect (sequential queries):** [bad example] + +**Correct (batched query):** [good example] +``` + +### 3. Quantified Impact + +Include specific metrics. Helps agents prioritize fixes. + +**Good:** "10x faster queries", "50% smaller index", "Eliminates N+1" +**Bad:** "Faster", "Better", "More efficient" + +### 4. Self-Contained Examples + +Examples should be complete and runnable (or close to it). Include `CREATE TABLE` +if context is needed. + +```sql +-- Include table definition when needed for clarity +CREATE TABLE users ( + id bigint PRIMARY KEY, + email text NOT NULL, + deleted_at timestamptz +); + +-- Now show the index +CREATE INDEX users_active_email_idx ON users(email) WHERE deleted_at IS NULL; +``` + +### 5. Semantic Naming + +Use meaningful table/column names. Names carry intent for LLMs. + +**Good:** `users`, `email`, `created_at`, `is_active` +**Bad:** `table1`, `col1`, `field`, `flag` + +--- + +## Code Example Standards + +### SQL Formatting + +```sql +-- Use lowercase keywords, clear formatting +CREATE INDEX CONCURRENTLY users_email_idx + ON users(email) + WHERE deleted_at IS NULL; + +-- Not cramped or ALL CAPS +CREATE INDEX CONCURRENTLY USERS_EMAIL_IDX ON USERS(EMAIL) WHERE DELETED_AT IS NULL; +``` + +### Comments + +- Explain _why_, not _what_ +- Highlight performance implications +- Point out common pitfalls + +### Language Tags + +- `sql` - Standard SQL queries +- `plpgsql` - Stored procedures/functions +- `typescript` - Application code (when needed) +- `python` - Application code (when needed) + +--- + +## When to Include Application Code + +**Default: SQL Only** + +Most rules should focus on pure SQL patterns. This keeps examples portable. + +**Include Application Code When:** + +- Connection pooling configuration +- Transaction management in application context +- ORM anti-patterns (N+1 in Prisma/TypeORM) +- Prepared statement usage + +**Format for Mixed Examples:** + +````markdown +**Incorrect (N+1 in application):** + +```typescript +for (const user of users) { + const posts = await db.query("SELECT * FROM posts WHERE user_id = $1", [ + user.id, + ]); +} +``` +```` + +**Correct (batch query):** + +```typescript +const posts = await db.query("SELECT * FROM posts WHERE user_id = ANY($1)", [ + userIds, +]); +``` + +--- + +## Impact Level Guidelines + +| Level | Improvement | Use When | +|-------|-------------|----------| +| **CRITICAL** | 10-100x | Missing indexes, connection exhaustion, sequential scans on large tables | +| **HIGH** | 5-20x | Wrong index types, poor partitioning, missing covering indexes | +| **MEDIUM-HIGH** | 2-5x | N+1 queries, inefficient pagination, RLS optimization | +| **MEDIUM** | 1.5-3x | Redundant indexes, query plan instability | +| **LOW-MEDIUM** | 1.2-2x | VACUUM tuning, configuration tweaks | +| **LOW** | Incremental | Advanced patterns, edge cases | + +--- + +## Reference Standards + +**Primary Sources:** + +- Official Postgres documentation +- Supabase documentation +- Postgres wiki +- Established blogs (2ndQuadrant, Crunchy Data) + +**Format:** + +```markdown +Reference: +[Postgres Indexes](https://www.postgresql.org/docs/current/indexes.html) +``` + +--- + +## Review Checklist + +Before submitting a rule: + +- [ ] Title is clear and action-oriented +- [ ] Impact level matches the performance gain +- [ ] impactDescription includes quantification +- [ ] Explanation is concise (1-2 sentences) +- [ ] Has at least 1 **Incorrect** SQL example +- [ ] Has at least 1 **Correct** SQL example +- [ ] SQL uses semantic naming +- [ ] Comments explain _why_, not _what_ +- [ ] Trade-offs mentioned if applicable +- [ ] Reference links included +- [ ] `npm run validate` passes +- [ ] `npm run build` generates correct output diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/_sections.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/_sections.md new file mode 100644 index 00000000..8ba57c23 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/_sections.md @@ -0,0 +1,39 @@ +# Section Definitions + +This file defines the rule categories for Postgres best practices. Rules are automatically assigned to sections based on their filename prefix. + +Take the examples below as pure demonstrative. Replace each section with the actual rule categories for Postgres best practices. + +--- + +## 1. Query Performance (query) +**Impact:** CRITICAL +**Description:** Slow queries, missing indexes, inefficient query plans. The most common source of Postgres performance issues. + +## 2. Connection Management (conn) +**Impact:** CRITICAL +**Description:** Connection pooling, limits, and serverless strategies. Critical for applications with high concurrency or serverless deployments. + +## 3. Security & RLS (security) +**Impact:** CRITICAL +**Description:** Row-Level Security policies, privilege management, and authentication patterns. + +## 4. Schema Design (schema) +**Impact:** HIGH +**Description:** Table design, index strategies, partitioning, and data type selection. Foundation for long-term performance. + +## 5. Concurrency & Locking (lock) +**Impact:** MEDIUM-HIGH +**Description:** Transaction management, isolation levels, deadlock prevention, and lock contention patterns. + +## 6. Data Access Patterns (data) +**Impact:** MEDIUM +**Description:** N+1 query elimination, batch operations, cursor-based pagination, and efficient data fetching. + +## 7. Monitoring & Diagnostics (monitor) +**Impact:** LOW-MEDIUM +**Description:** Using pg_stat_statements, EXPLAIN ANALYZE, metrics collection, and performance diagnostics. + +## 8. Advanced Features (advanced) +**Impact:** LOW +**Description:** Full-text search, JSONB optimization, PostGIS, extensions, and advanced Postgres features. diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/_template.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/_template.md new file mode 100644 index 00000000..91ace90e --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/_template.md @@ -0,0 +1,34 @@ +--- +title: Clear, Action-Oriented Title (e.g., "Use Partial Indexes for Filtered Queries") +impact: MEDIUM +impactDescription: 5-20x query speedup for filtered queries +tags: indexes, query-optimization, performance +--- + +## [Rule Title] + +[1-2 sentence explanation of the problem and why it matters. Focus on performance impact.] + +**Incorrect (describe the problem):** + +```sql +-- Comment explaining what makes this slow/problematic +CREATE INDEX users_email_idx ON users(email); + +SELECT * FROM users WHERE email = 'user@example.com' AND deleted_at IS NULL; +-- This scans deleted records unnecessarily +``` + +**Correct (describe the solution):** + +```sql +-- Comment explaining why this is better +CREATE INDEX users_active_email_idx ON users(email) WHERE deleted_at IS NULL; + +SELECT * FROM users WHERE email = 'user@example.com' AND deleted_at IS NULL; +-- Only indexes active users, 10x smaller index, faster queries +``` + +[Optional: Additional context, edge cases, or trade-offs] + +Reference: [Postgres Docs](https://www.postgresql.org/docs/current/) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/advanced-full-text-search.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/advanced-full-text-search.md new file mode 100644 index 00000000..582cbeaa --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/advanced-full-text-search.md @@ -0,0 +1,55 @@ +--- +title: Use tsvector for Full-Text Search +impact: MEDIUM +impactDescription: 100x faster than LIKE, with ranking support +tags: full-text-search, tsvector, gin, search +--- + +## Use tsvector for Full-Text Search + +LIKE with wildcards can't use indexes. Full-text search with tsvector is orders of magnitude faster. + +**Incorrect (LIKE pattern matching):** + +```sql +-- Cannot use index, scans all rows +select * from articles where content like '%postgresql%'; + +-- Case-insensitive makes it worse +select * from articles where lower(content) like '%postgresql%'; +``` + +**Correct (full-text search with tsvector):** + +```sql +-- Add tsvector column and index +alter table articles add column search_vector tsvector + generated always as (to_tsvector('english', coalesce(title,'') || ' ' || coalesce(content,''))) stored; + +create index articles_search_idx on articles using gin (search_vector); + +-- Fast full-text search +select * from articles +where search_vector @@ to_tsquery('english', 'postgresql & performance'); + +-- With ranking +select *, ts_rank(search_vector, query) as rank +from articles, to_tsquery('english', 'postgresql') query +where search_vector @@ query +order by rank desc; +``` + +Search multiple terms: + +```sql +-- AND: both terms required +to_tsquery('postgresql & performance') + +-- OR: either term +to_tsquery('postgresql | mysql') + +-- Prefix matching +to_tsquery('post:*') +``` + +Reference: [Full Text Search](https://supabase.com/docs/guides/database/full-text-search) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/advanced-jsonb-indexing.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/advanced-jsonb-indexing.md new file mode 100644 index 00000000..e3d261ea --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/advanced-jsonb-indexing.md @@ -0,0 +1,49 @@ +--- +title: Index JSONB Columns for Efficient Querying +impact: MEDIUM +impactDescription: 10-100x faster JSONB queries with proper indexing +tags: jsonb, gin, indexes, json +--- + +## Index JSONB Columns for Efficient Querying + +JSONB queries without indexes scan the entire table. Use GIN indexes for containment queries. + +**Incorrect (no index on JSONB):** + +```sql +create table products ( + id bigint primary key, + attributes jsonb +); + +-- Full table scan for every query +select * from products where attributes @> '{"color": "red"}'; +select * from products where attributes->>'brand' = 'Nike'; +``` + +**Correct (GIN index for JSONB):** + +```sql +-- GIN index for containment operators (@>, ?, ?&, ?|) +create index products_attrs_gin on products using gin (attributes); + +-- Now containment queries use the index +select * from products where attributes @> '{"color": "red"}'; + +-- For specific key lookups, use expression index +create index products_brand_idx on products ((attributes->>'brand')); +select * from products where attributes->>'brand' = 'Nike'; +``` + +Choose the right operator class: + +```sql +-- jsonb_ops (default): supports all operators, larger index +create index idx1 on products using gin (attributes); + +-- jsonb_path_ops: only @> operator, but 2-3x smaller index +create index idx2 on products using gin (attributes jsonb_path_ops); +``` + +Reference: [JSONB Indexes](https://www.postgresql.org/docs/current/datatype-json.html#JSON-INDEXING) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/conn-idle-timeout.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/conn-idle-timeout.md new file mode 100644 index 00000000..40b9cc50 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/conn-idle-timeout.md @@ -0,0 +1,46 @@ +--- +title: Configure Idle Connection Timeouts +impact: HIGH +impactDescription: Reclaim 30-50% of connection slots from idle clients +tags: connections, timeout, idle, resource-management +--- + +## Configure Idle Connection Timeouts + +Idle connections waste resources. Configure timeouts to automatically reclaim them. + +**Incorrect (connections held indefinitely):** + +```sql +-- No timeout configured +show idle_in_transaction_session_timeout; -- 0 (disabled) + +-- Connections stay open forever, even when idle +select pid, state, state_change, query +from pg_stat_activity +where state = 'idle in transaction'; +-- Shows transactions idle for hours, holding locks +``` + +**Correct (automatic cleanup of idle connections):** + +```sql +-- Terminate connections idle in transaction after 30 seconds +alter system set idle_in_transaction_session_timeout = '30s'; + +-- Terminate completely idle connections after 10 minutes +alter system set idle_session_timeout = '10min'; + +-- Reload configuration +select pg_reload_conf(); +``` + +For pooled connections, configure at the pooler level: + +```ini +# pgbouncer.ini +server_idle_timeout = 60 +client_idle_timeout = 300 +``` + +Reference: [Connection Timeouts](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-IDLE-IN-TRANSACTION-SESSION-TIMEOUT) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/conn-limits.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/conn-limits.md new file mode 100644 index 00000000..cb3e400c --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/conn-limits.md @@ -0,0 +1,44 @@ +--- +title: Set Appropriate Connection Limits +impact: CRITICAL +impactDescription: Prevent database crashes and memory exhaustion +tags: connections, max-connections, limits, stability +--- + +## Set Appropriate Connection Limits + +Too many connections exhaust memory and degrade performance. Set limits based on available resources. + +**Incorrect (unlimited or excessive connections):** + +```sql +-- Default max_connections = 100, but often increased blindly +show max_connections; -- 500 (way too high for 4GB RAM) + +-- Each connection uses 1-3MB RAM +-- 500 connections * 2MB = 1GB just for connections! +-- Out of memory errors under load +``` + +**Correct (calculate based on resources):** + +```sql +-- Formula: max_connections = (RAM in MB / 5MB per connection) - reserved +-- For 4GB RAM: (4096 / 5) - 10 = ~800 theoretical max +-- But practically, 100-200 is better for query performance + +-- Recommended settings for 4GB RAM +alter system set max_connections = 100; + +-- Also set work_mem appropriately +-- work_mem * max_connections should not exceed 25% of RAM +alter system set work_mem = '8MB'; -- 8MB * 100 = 800MB max +``` + +Monitor connection usage: + +```sql +select count(*), state from pg_stat_activity group by state; +``` + +Reference: [Database Connections](https://supabase.com/docs/guides/platform/performance#connection-management) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/conn-pooling.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/conn-pooling.md new file mode 100644 index 00000000..e2ebd581 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/conn-pooling.md @@ -0,0 +1,41 @@ +--- +title: Use Connection Pooling for All Applications +impact: CRITICAL +impactDescription: Handle 10-100x more concurrent users +tags: connection-pooling, pgbouncer, performance, scalability +--- + +## Use Connection Pooling for All Applications + +Postgres connections are expensive (1-3MB RAM each). Without pooling, applications exhaust connections under load. + +**Incorrect (new connection per request):** + +```sql +-- Each request creates a new connection +-- Application code: db.connect() per request +-- Result: 500 concurrent users = 500 connections = crashed database + +-- Check current connections +select count(*) from pg_stat_activity; -- 487 connections! +``` + +**Correct (connection pooling):** + +```sql +-- Use a pooler like PgBouncer between app and database +-- Application connects to pooler, pooler reuses a small pool to Postgres + +-- Configure pool_size based on: (CPU cores * 2) + spindle_count +-- Example for 4 cores: pool_size = 10 + +-- Result: 500 concurrent users share 10 actual connections +select count(*) from pg_stat_activity; -- 10 connections +``` + +Pool modes: + +- **Transaction mode**: connection returned after each transaction (best for most apps) +- **Session mode**: connection held for entire session (needed for prepared statements, temp tables) + +Reference: [Connection Pooling](https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pooler) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/conn-prepared-statements.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/conn-prepared-statements.md new file mode 100644 index 00000000..555547d8 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/conn-prepared-statements.md @@ -0,0 +1,46 @@ +--- +title: Use Prepared Statements Correctly with Pooling +impact: HIGH +impactDescription: Avoid prepared statement conflicts in pooled environments +tags: prepared-statements, connection-pooling, transaction-mode +--- + +## Use Prepared Statements Correctly with Pooling + +Prepared statements are tied to individual database connections. In transaction-mode pooling, connections are shared, causing conflicts. + +**Incorrect (named prepared statements with transaction pooling):** + +```sql +-- Named prepared statement +prepare get_user as select * from users where id = $1; + +-- In transaction mode pooling, next request may get different connection +execute get_user(123); +-- ERROR: prepared statement "get_user" does not exist +``` + +**Correct (use unnamed statements or session mode):** + +```sql +-- Option 1: Use unnamed prepared statements (most ORMs do this automatically) +-- The query is prepared and executed in a single protocol message + +-- Option 2: Deallocate after use in transaction mode +prepare get_user as select * from users where id = $1; +execute get_user(123); +deallocate get_user; + +-- Option 3: Use session mode pooling (port 5432 vs 6543) +-- Connection is held for entire session, prepared statements persist +``` + +Check your driver settings: + +```sql +-- Many drivers use prepared statements by default +-- Node.js pg: { prepare: false } to disable +-- JDBC: prepareThreshold=0 to disable +``` + +Reference: [Prepared Statements with Pooling](https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pool-modes) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/data-batch-inserts.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/data-batch-inserts.md new file mode 100644 index 00000000..997947cb --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/data-batch-inserts.md @@ -0,0 +1,54 @@ +--- +title: Batch INSERT Statements for Bulk Data +impact: MEDIUM +impactDescription: 10-50x faster bulk inserts +tags: batch, insert, bulk, performance, copy +--- + +## Batch INSERT Statements for Bulk Data + +Individual INSERT statements have high overhead. Batch multiple rows in single statements or use COPY. + +**Incorrect (individual inserts):** + +```sql +-- Each insert is a separate transaction and round trip +insert into events (user_id, action) values (1, 'click'); +insert into events (user_id, action) values (1, 'view'); +insert into events (user_id, action) values (2, 'click'); +-- ... 1000 more individual inserts + +-- 1000 inserts = 1000 round trips = slow +``` + +**Correct (batch insert):** + +```sql +-- Multiple rows in single statement +insert into events (user_id, action) values + (1, 'click'), + (1, 'view'), + (2, 'click'), + -- ... up to ~1000 rows per batch + (999, 'view'); + +-- One round trip for 1000 rows +``` + +For large imports, use COPY: + +```sql +-- COPY is fastest for bulk loading +copy events (user_id, action, created_at) +from '/path/to/data.csv' +with (format csv, header true); + +-- Or from stdin in application +copy events (user_id, action) from stdin with (format csv); +1,click +1,view +2,click +\. +``` + +Reference: [COPY](https://www.postgresql.org/docs/current/sql-copy.html) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/data-n-plus-one.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/data-n-plus-one.md new file mode 100644 index 00000000..2109186f --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/data-n-plus-one.md @@ -0,0 +1,53 @@ +--- +title: Eliminate N+1 Queries with Batch Loading +impact: MEDIUM-HIGH +impactDescription: 10-100x fewer database round trips +tags: n-plus-one, batch, performance, queries +--- + +## Eliminate N+1 Queries with Batch Loading + +N+1 queries execute one query per item in a loop. Batch them into a single query using arrays or JOINs. + +**Incorrect (N+1 queries):** + +```sql +-- First query: get all users +select id from users where active = true; -- Returns 100 IDs + +-- Then N queries, one per user +select * from orders where user_id = 1; +select * from orders where user_id = 2; +select * from orders where user_id = 3; +-- ... 97 more queries! + +-- Total: 101 round trips to database +``` + +**Correct (single batch query):** + +```sql +-- Collect IDs and query once with ANY +select * from orders where user_id = any(array[1, 2, 3, ...]); + +-- Or use JOIN instead of loop +select u.id, u.name, o.* +from users u +left join orders o on o.user_id = u.id +where u.active = true; + +-- Total: 1 round trip +``` + +Application pattern: + +```sql +-- Instead of looping in application code: +-- for user in users: db.query("SELECT * FROM orders WHERE user_id = $1", user.id) + +-- Pass array parameter: +select * from orders where user_id = any($1::bigint[]); +-- Application passes: [1, 2, 3, 4, 5, ...] +``` + +Reference: [N+1 Query Problem](https://supabase.com/docs/guides/database/query-optimization) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/data-pagination.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/data-pagination.md new file mode 100644 index 00000000..633d8393 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/data-pagination.md @@ -0,0 +1,50 @@ +--- +title: Use Cursor-Based Pagination Instead of OFFSET +impact: MEDIUM-HIGH +impactDescription: Consistent O(1) performance regardless of page depth +tags: pagination, cursor, keyset, offset, performance +--- + +## Use Cursor-Based Pagination Instead of OFFSET + +OFFSET-based pagination scans all skipped rows, getting slower on deeper pages. Cursor pagination is O(1). + +**Incorrect (OFFSET pagination):** + +```sql +-- Page 1: scans 20 rows +select * from products order by id limit 20 offset 0; + +-- Page 100: scans 2000 rows to skip 1980 +select * from products order by id limit 20 offset 1980; + +-- Page 10000: scans 200,000 rows! +select * from products order by id limit 20 offset 199980; +``` + +**Correct (cursor/keyset pagination):** + +```sql +-- Page 1: get first 20 +select * from products order by id limit 20; +-- Application stores last_id = 20 + +-- Page 2: start after last ID +select * from products where id > 20 order by id limit 20; +-- Uses index, always fast regardless of page depth + +-- Page 10000: same speed as page 1 +select * from products where id > 199980 order by id limit 20; +``` + +For multi-column sorting: + +```sql +-- Cursor must include all sort columns +select * from products +where (created_at, id) > ('2024-01-15 10:00:00', 12345) +order by created_at, id +limit 20; +``` + +Reference: [Pagination](https://supabase.com/docs/guides/database/pagination) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/data-upsert.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/data-upsert.md new file mode 100644 index 00000000..bc95e230 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/data-upsert.md @@ -0,0 +1,50 @@ +--- +title: Use UPSERT for Insert-or-Update Operations +impact: MEDIUM +impactDescription: Atomic operation, eliminates race conditions +tags: upsert, on-conflict, insert, update +--- + +## Use UPSERT for Insert-or-Update Operations + +Using separate SELECT-then-INSERT/UPDATE creates race conditions. Use INSERT ... ON CONFLICT for atomic upserts. + +**Incorrect (check-then-insert race condition):** + +```sql +-- Race condition: two requests check simultaneously +select * from settings where user_id = 123 and key = 'theme'; +-- Both find nothing + +-- Both try to insert +insert into settings (user_id, key, value) values (123, 'theme', 'dark'); +-- One succeeds, one fails with duplicate key error! +``` + +**Correct (atomic UPSERT):** + +```sql +-- Single atomic operation +insert into settings (user_id, key, value) +values (123, 'theme', 'dark') +on conflict (user_id, key) +do update set value = excluded.value, updated_at = now(); + +-- Returns the inserted/updated row +insert into settings (user_id, key, value) +values (123, 'theme', 'dark') +on conflict (user_id, key) +do update set value = excluded.value +returning *; +``` + +Insert-or-ignore pattern: + +```sql +-- Insert only if not exists (no update) +insert into page_views (page_id, user_id) +values (1, 123) +on conflict (page_id, user_id) do nothing; +``` + +Reference: [INSERT ON CONFLICT](https://www.postgresql.org/docs/current/sql-insert.html#SQL-ON-CONFLICT) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/lock-advisory.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/lock-advisory.md new file mode 100644 index 00000000..572eaf0d --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/lock-advisory.md @@ -0,0 +1,56 @@ +--- +title: Use Advisory Locks for Application-Level Locking +impact: MEDIUM +impactDescription: Efficient coordination without row-level lock overhead +tags: advisory-locks, coordination, application-locks +--- + +## Use Advisory Locks for Application-Level Locking + +Advisory locks provide application-level coordination without requiring database rows to lock. + +**Incorrect (creating rows just for locking):** + +```sql +-- Creating dummy rows to lock on +create table resource_locks ( + resource_name text primary key +); + +insert into resource_locks values ('report_generator'); + +-- Lock by selecting the row +select * from resource_locks where resource_name = 'report_generator' for update; +``` + +**Correct (advisory locks):** + +```sql +-- Session-level advisory lock (released on disconnect or unlock) +select pg_advisory_lock(hashtext('report_generator')); +-- ... do exclusive work ... +select pg_advisory_unlock(hashtext('report_generator')); + +-- Transaction-level lock (released on commit/rollback) +begin; +select pg_advisory_xact_lock(hashtext('daily_report')); +-- ... do work ... +commit; -- Lock automatically released +``` + +Try-lock for non-blocking operations: + +```sql +-- Returns immediately with true/false instead of waiting +select pg_try_advisory_lock(hashtext('resource_name')); + +-- Use in application +if (acquired) { + -- Do work + select pg_advisory_unlock(hashtext('resource_name')); +} else { + -- Skip or retry later +} +``` + +Reference: [Advisory Locks](https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/lock-deadlock-prevention.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/lock-deadlock-prevention.md new file mode 100644 index 00000000..974da5ed --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/lock-deadlock-prevention.md @@ -0,0 +1,68 @@ +--- +title: Prevent Deadlocks with Consistent Lock Ordering +impact: MEDIUM-HIGH +impactDescription: Eliminate deadlock errors, improve reliability +tags: deadlocks, locking, transactions, ordering +--- + +## Prevent Deadlocks with Consistent Lock Ordering + +Deadlocks occur when transactions lock resources in different orders. Always +acquire locks in a consistent order. + +**Incorrect (inconsistent lock ordering):** + +```sql +-- Transaction A -- Transaction B +begin; begin; +update accounts update accounts +set balance = balance - 100 set balance = balance - 50 +where id = 1; where id = 2; -- B locks row 2 + +update accounts update accounts +set balance = balance + 100 set balance = balance + 50 +where id = 2; -- A waits for B where id = 1; -- B waits for A + +-- DEADLOCK! Both waiting for each other +``` + +**Correct (lock rows in consistent order first):** + +```sql +-- Explicitly acquire locks in ID order before updating +begin; +select * from accounts where id in (1, 2) order by id for update; + +-- Now perform updates in any order - locks already held +update accounts set balance = balance - 100 where id = 1; +update accounts set balance = balance + 100 where id = 2; +commit; +``` + +Alternative: use a single statement to update atomically: + +```sql +-- Single statement acquires all locks atomically +begin; +update accounts +set balance = balance + case id + when 1 then -100 + when 2 then 100 +end +where id in (1, 2); +commit; +``` + +Detect deadlocks in logs: + +```sql +-- Check for recent deadlocks +select * from pg_stat_database where deadlocks > 0; + +-- Enable deadlock logging +set log_lock_waits = on; +set deadlock_timeout = '1s'; +``` + +Reference: +[Deadlocks](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-DEADLOCKS) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/lock-short-transactions.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/lock-short-transactions.md new file mode 100644 index 00000000..e6b8ef26 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/lock-short-transactions.md @@ -0,0 +1,50 @@ +--- +title: Keep Transactions Short to Reduce Lock Contention +impact: MEDIUM-HIGH +impactDescription: 3-5x throughput improvement, fewer deadlocks +tags: transactions, locking, contention, performance +--- + +## Keep Transactions Short to Reduce Lock Contention + +Long-running transactions hold locks that block other queries. Keep transactions as short as possible. + +**Incorrect (long transaction with external calls):** + +```sql +begin; +select * from orders where id = 1 for update; -- Lock acquired + +-- Application makes HTTP call to payment API (2-5 seconds) +-- Other queries on this row are blocked! + +update orders set status = 'paid' where id = 1; +commit; -- Lock held for entire duration +``` + +**Correct (minimal transaction scope):** + +```sql +-- Validate data and call APIs outside transaction +-- Application: response = await paymentAPI.charge(...) + +-- Only hold lock for the actual update +begin; +update orders +set status = 'paid', payment_id = $1 +where id = $2 and status = 'pending' +returning *; +commit; -- Lock held for milliseconds +``` + +Use `statement_timeout` to prevent runaway transactions: + +```sql +-- Abort queries running longer than 30 seconds +set statement_timeout = '30s'; + +-- Or per-session +set local statement_timeout = '5s'; +``` + +Reference: [Transaction Management](https://www.postgresql.org/docs/current/tutorial-transactions.html) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/lock-skip-locked.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/lock-skip-locked.md new file mode 100644 index 00000000..77bdbb97 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/lock-skip-locked.md @@ -0,0 +1,54 @@ +--- +title: Use SKIP LOCKED for Non-Blocking Queue Processing +impact: MEDIUM-HIGH +impactDescription: 10x throughput for worker queues +tags: skip-locked, queue, workers, concurrency +--- + +## Use SKIP LOCKED for Non-Blocking Queue Processing + +When multiple workers process a queue, SKIP LOCKED allows workers to process different rows without waiting. + +**Incorrect (workers block each other):** + +```sql +-- Worker 1 and Worker 2 both try to get next job +begin; +select * from jobs where status = 'pending' order by created_at limit 1 for update; +-- Worker 2 waits for Worker 1's lock to release! +``` + +**Correct (SKIP LOCKED for parallel processing):** + +```sql +-- Each worker skips locked rows and gets the next available +begin; +select * from jobs +where status = 'pending' +order by created_at +limit 1 +for update skip locked; + +-- Worker 1 gets job 1, Worker 2 gets job 2 (no waiting) + +update jobs set status = 'processing' where id = $1; +commit; +``` + +Complete queue pattern: + +```sql +-- Atomic claim-and-update in one statement +update jobs +set status = 'processing', worker_id = $1, started_at = now() +where id = ( + select id from jobs + where status = 'pending' + order by created_at + limit 1 + for update skip locked +) +returning *; +``` + +Reference: [SELECT FOR UPDATE SKIP LOCKED](https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/monitor-explain-analyze.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/monitor-explain-analyze.md new file mode 100644 index 00000000..542978c3 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/monitor-explain-analyze.md @@ -0,0 +1,45 @@ +--- +title: Use EXPLAIN ANALYZE to Diagnose Slow Queries +impact: LOW-MEDIUM +impactDescription: Identify exact bottlenecks in query execution +tags: explain, analyze, diagnostics, query-plan +--- + +## Use EXPLAIN ANALYZE to Diagnose Slow Queries + +EXPLAIN ANALYZE executes the query and shows actual timings, revealing the true performance bottlenecks. + +**Incorrect (guessing at performance issues):** + +```sql +-- Query is slow, but why? +select * from orders where customer_id = 123 and status = 'pending'; +-- "It must be missing an index" - but which one? +``` + +**Correct (use EXPLAIN ANALYZE):** + +```sql +explain (analyze, buffers, format text) +select * from orders where customer_id = 123 and status = 'pending'; + +-- Output reveals the issue: +-- Seq Scan on orders (cost=0.00..25000.00 rows=50 width=100) (actual time=0.015..450.123 rows=50 loops=1) +-- Filter: ((customer_id = 123) AND (status = 'pending'::text)) +-- Rows Removed by Filter: 999950 +-- Buffers: shared hit=5000 read=15000 +-- Planning Time: 0.150 ms +-- Execution Time: 450.500 ms +``` + +Key things to look for: + +```sql +-- Seq Scan on large tables = missing index +-- Rows Removed by Filter = poor selectivity or missing index +-- Buffers: read >> hit = data not cached, needs more memory +-- Nested Loop with high loops = consider different join strategy +-- Sort Method: external merge = work_mem too low +``` + +Reference: [EXPLAIN](https://supabase.com/docs/guides/database/inspect) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/monitor-pg-stat-statements.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/monitor-pg-stat-statements.md new file mode 100644 index 00000000..d7e82f1a --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/monitor-pg-stat-statements.md @@ -0,0 +1,55 @@ +--- +title: Enable pg_stat_statements for Query Analysis +impact: LOW-MEDIUM +impactDescription: Identify top resource-consuming queries +tags: pg-stat-statements, monitoring, statistics, performance +--- + +## Enable pg_stat_statements for Query Analysis + +pg_stat_statements tracks execution statistics for all queries, helping identify slow and frequent queries. + +**Incorrect (no visibility into query patterns):** + +```sql +-- Database is slow, but which queries are the problem? +-- No way to know without pg_stat_statements +``` + +**Correct (enable and query pg_stat_statements):** + +```sql +-- Enable the extension +create extension if not exists pg_stat_statements; + +-- Find slowest queries by total time +select + calls, + round(total_exec_time::numeric, 2) as total_time_ms, + round(mean_exec_time::numeric, 2) as mean_time_ms, + query +from pg_stat_statements +order by total_exec_time desc +limit 10; + +-- Find most frequent queries +select calls, query +from pg_stat_statements +order by calls desc +limit 10; + +-- Reset statistics after optimization +select pg_stat_statements_reset(); +``` + +Key metrics to monitor: + +```sql +-- Queries with high mean time (candidates for optimization) +select query, mean_exec_time, calls +from pg_stat_statements +where mean_exec_time > 100 -- > 100ms average +order by mean_exec_time desc; +``` + +Reference: [pg_stat_statements](https://supabase.com/docs/guides/database/extensions/pg_stat_statements) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/monitor-vacuum-analyze.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/monitor-vacuum-analyze.md new file mode 100644 index 00000000..e0e8ea0b --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/monitor-vacuum-analyze.md @@ -0,0 +1,55 @@ +--- +title: Maintain Table Statistics with VACUUM and ANALYZE +impact: MEDIUM +impactDescription: 2-10x better query plans with accurate statistics +tags: vacuum, analyze, statistics, maintenance, autovacuum +--- + +## Maintain Table Statistics with VACUUM and ANALYZE + +Outdated statistics cause the query planner to make poor decisions. VACUUM reclaims space, ANALYZE updates statistics. + +**Incorrect (stale statistics):** + +```sql +-- Table has 1M rows but stats say 1000 +-- Query planner chooses wrong strategy +explain select * from orders where status = 'pending'; +-- Shows: Seq Scan (because stats show small table) +-- Actually: Index Scan would be much faster +``` + +**Correct (maintain fresh statistics):** + +```sql +-- Manually analyze after large data changes +analyze orders; + +-- Analyze specific columns used in WHERE clauses +analyze orders (status, created_at); + +-- Check when tables were last analyzed +select + relname, + last_vacuum, + last_autovacuum, + last_analyze, + last_autoanalyze +from pg_stat_user_tables +order by last_analyze nulls first; +``` + +Autovacuum tuning for busy tables: + +```sql +-- Increase frequency for high-churn tables +alter table orders set ( + autovacuum_vacuum_scale_factor = 0.05, -- Vacuum at 5% dead tuples (default 20%) + autovacuum_analyze_scale_factor = 0.02 -- Analyze at 2% changes (default 10%) +); + +-- Check autovacuum status +select * from pg_stat_progress_vacuum; +``` + +Reference: [VACUUM](https://supabase.com/docs/guides/database/database-size#vacuum-operations) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/query-composite-indexes.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/query-composite-indexes.md new file mode 100644 index 00000000..fea64523 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/query-composite-indexes.md @@ -0,0 +1,44 @@ +--- +title: Create Composite Indexes for Multi-Column Queries +impact: HIGH +impactDescription: 5-10x faster multi-column queries +tags: indexes, composite-index, multi-column, query-optimization +--- + +## Create Composite Indexes for Multi-Column Queries + +When queries filter on multiple columns, a composite index is more efficient than separate single-column indexes. + +**Incorrect (separate indexes require bitmap scan):** + +```sql +-- Two separate indexes +create index orders_status_idx on orders (status); +create index orders_created_idx on orders (created_at); + +-- Query must combine both indexes (slower) +select * from orders where status = 'pending' and created_at > '2024-01-01'; +``` + +**Correct (composite index):** + +```sql +-- Single composite index (leftmost column first for equality checks) +create index orders_status_created_idx on orders (status, created_at); + +-- Query uses one efficient index scan +select * from orders where status = 'pending' and created_at > '2024-01-01'; +``` + +**Column order matters** - place equality columns first, range columns last: + +```sql +-- Good: status (=) before created_at (>) +create index idx on orders (status, created_at); + +-- Works for: WHERE status = 'pending' +-- Works for: WHERE status = 'pending' AND created_at > '2024-01-01' +-- Does NOT work for: WHERE created_at > '2024-01-01' (leftmost prefix rule) +``` + +Reference: [Multicolumn Indexes](https://www.postgresql.org/docs/current/indexes-multicolumn.html) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/query-covering-indexes.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/query-covering-indexes.md new file mode 100644 index 00000000..9d2a4947 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/query-covering-indexes.md @@ -0,0 +1,40 @@ +--- +title: Use Covering Indexes to Avoid Table Lookups +impact: MEDIUM-HIGH +impactDescription: 2-5x faster queries by eliminating heap fetches +tags: indexes, covering-index, include, index-only-scan +--- + +## Use Covering Indexes to Avoid Table Lookups + +Covering indexes include all columns needed by a query, enabling index-only scans that skip the table entirely. + +**Incorrect (index scan + heap fetch):** + +```sql +create index users_email_idx on users (email); + +-- Must fetch name and created_at from table heap +select email, name, created_at from users where email = 'user@example.com'; +``` + +**Correct (index-only scan with INCLUDE):** + +```sql +-- Include non-searchable columns in the index +create index users_email_idx on users (email) include (name, created_at); + +-- All columns served from index, no table access needed +select email, name, created_at from users where email = 'user@example.com'; +``` + +Use INCLUDE for columns you SELECT but don't filter on: + +```sql +-- Searching by status, but also need customer_id and total +create index orders_status_idx on orders (status) include (customer_id, total); + +select status, customer_id, total from orders where status = 'shipped'; +``` + +Reference: [Index-Only Scans](https://www.postgresql.org/docs/current/indexes-index-only-scans.html) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/query-index-types.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/query-index-types.md new file mode 100644 index 00000000..0d7651af --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/query-index-types.md @@ -0,0 +1,45 @@ +--- +title: Choose the Right Index Type for Your Data +impact: HIGH +impactDescription: 10-100x improvement with correct index type +tags: indexes, btree, gin, brin, hash, index-types +--- + +## Choose the Right Index Type for Your Data + +Different index types excel at different query patterns. The default B-tree isn't always optimal. + +**Incorrect (B-tree for JSONB containment):** + +```sql +-- B-tree cannot optimize containment operators +create index products_attrs_idx on products (attributes); +select * from products where attributes @> '{"color": "red"}'; +-- Full table scan - B-tree doesn't support @> operator +``` + +**Correct (GIN for JSONB):** + +```sql +-- GIN supports @>, ?, ?&, ?| operators +create index products_attrs_idx on products using gin (attributes); +select * from products where attributes @> '{"color": "red"}'; +``` + +Index type guide: + +```sql +-- B-tree (default): =, <, >, BETWEEN, IN, IS NULL +create index users_created_idx on users (created_at); + +-- GIN: arrays, JSONB, full-text search +create index posts_tags_idx on posts using gin (tags); + +-- BRIN: large time-series tables (10-100x smaller) +create index events_time_idx on events using brin (created_at); + +-- Hash: equality-only (slightly faster than B-tree for =) +create index sessions_token_idx on sessions using hash (token); +``` + +Reference: [Index Types](https://www.postgresql.org/docs/current/indexes-types.html) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/query-missing-indexes.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/query-missing-indexes.md new file mode 100644 index 00000000..e6daace7 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/query-missing-indexes.md @@ -0,0 +1,43 @@ +--- +title: Add Indexes on WHERE and JOIN Columns +impact: CRITICAL +impactDescription: 100-1000x faster queries on large tables +tags: indexes, performance, sequential-scan, query-optimization +--- + +## Add Indexes on WHERE and JOIN Columns + +Queries filtering or joining on unindexed columns cause full table scans, which become exponentially slower as tables grow. + +**Incorrect (sequential scan on large table):** + +```sql +-- No index on customer_id causes full table scan +select * from orders where customer_id = 123; + +-- EXPLAIN shows: Seq Scan on orders (cost=0.00..25000.00 rows=100 width=85) +``` + +**Correct (index scan):** + +```sql +-- Create index on frequently filtered column +create index orders_customer_id_idx on orders (customer_id); + +select * from orders where customer_id = 123; + +-- EXPLAIN shows: Index Scan using orders_customer_id_idx (cost=0.42..8.44 rows=100 width=85) +``` + +For JOIN columns, always index the foreign key side: + +```sql +-- Index the referencing column +create index orders_customer_id_idx on orders (customer_id); + +select c.name, o.total +from customers c +join orders o on o.customer_id = c.id; +``` + +Reference: [Query Optimization](https://supabase.com/docs/guides/database/query-optimization) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/query-partial-indexes.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/query-partial-indexes.md new file mode 100644 index 00000000..3e61a341 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/query-partial-indexes.md @@ -0,0 +1,45 @@ +--- +title: Use Partial Indexes for Filtered Queries +impact: HIGH +impactDescription: 5-20x smaller indexes, faster writes and queries +tags: indexes, partial-index, query-optimization, storage +--- + +## Use Partial Indexes for Filtered Queries + +Partial indexes only include rows matching a WHERE condition, making them smaller and faster when queries consistently filter on the same condition. + +**Incorrect (full index includes irrelevant rows):** + +```sql +-- Index includes all rows, even soft-deleted ones +create index users_email_idx on users (email); + +-- Query always filters active users +select * from users where email = 'user@example.com' and deleted_at is null; +``` + +**Correct (partial index matches query filter):** + +```sql +-- Index only includes active users +create index users_active_email_idx on users (email) +where deleted_at is null; + +-- Query uses the smaller, faster index +select * from users where email = 'user@example.com' and deleted_at is null; +``` + +Common use cases for partial indexes: + +```sql +-- Only pending orders (status rarely changes once completed) +create index orders_pending_idx on orders (created_at) +where status = 'pending'; + +-- Only non-null values +create index products_sku_idx on products (sku) +where sku is not null; +``` + +Reference: [Partial Indexes](https://www.postgresql.org/docs/current/indexes-partial.html) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/schema-data-types.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/schema-data-types.md new file mode 100644 index 00000000..f253a581 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/schema-data-types.md @@ -0,0 +1,46 @@ +--- +title: Choose Appropriate Data Types +impact: HIGH +impactDescription: 50% storage reduction, faster comparisons +tags: data-types, schema, storage, performance +--- + +## Choose Appropriate Data Types + +Using the right data types reduces storage, improves query performance, and prevents bugs. + +**Incorrect (wrong data types):** + +```sql +create table users ( + id int, -- Will overflow at 2.1 billion + email varchar(255), -- Unnecessary length limit + created_at timestamp, -- Missing timezone info + is_active varchar(5), -- String for boolean + price varchar(20) -- String for numeric +); +``` + +**Correct (appropriate data types):** + +```sql +create table users ( + id bigint generated always as identity primary key, -- 9 quintillion max + email text, -- No artificial limit, same performance as varchar + created_at timestamptz, -- Always store timezone-aware timestamps + is_active boolean default true, -- 1 byte vs variable string length + price numeric(10,2) -- Exact decimal arithmetic +); +``` + +Key guidelines: + +```sql +-- IDs: use bigint, not int (future-proofing) +-- Strings: use text, not varchar(n) unless constraint needed +-- Time: use timestamptz, not timestamp +-- Money: use numeric, not float (precision matters) +-- Enums: use text with check constraint or create enum type +``` + +Reference: [Data Types](https://www.postgresql.org/docs/current/datatype.html) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/schema-foreign-key-indexes.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/schema-foreign-key-indexes.md new file mode 100644 index 00000000..6c3d6ff6 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/schema-foreign-key-indexes.md @@ -0,0 +1,59 @@ +--- +title: Index Foreign Key Columns +impact: HIGH +impactDescription: 10-100x faster JOINs and CASCADE operations +tags: foreign-key, indexes, joins, schema +--- + +## Index Foreign Key Columns + +Postgres does not automatically index foreign key columns. Missing indexes cause slow JOINs and CASCADE operations. + +**Incorrect (unindexed foreign key):** + +```sql +create table orders ( + id bigint generated always as identity primary key, + customer_id bigint references customers(id) on delete cascade, + total numeric(10,2) +); + +-- No index on customer_id! +-- JOINs and ON DELETE CASCADE both require full table scan +select * from orders where customer_id = 123; -- Seq Scan +delete from customers where id = 123; -- Locks table, scans all orders +``` + +**Correct (indexed foreign key):** + +```sql +create table orders ( + id bigint generated always as identity primary key, + customer_id bigint references customers(id) on delete cascade, + total numeric(10,2) +); + +-- Always index the FK column +create index orders_customer_id_idx on orders (customer_id); + +-- Now JOINs and cascades are fast +select * from orders where customer_id = 123; -- Index Scan +delete from customers where id = 123; -- Uses index, fast cascade +``` + +Find missing FK indexes: + +```sql +select + conrelid::regclass as table_name, + a.attname as fk_column +from pg_constraint c +join pg_attribute a on a.attrelid = c.conrelid and a.attnum = any(c.conkey) +where c.contype = 'f' + and not exists ( + select 1 from pg_index i + where i.indrelid = c.conrelid and a.attnum = any(i.indkey) + ); +``` + +Reference: [Foreign Keys](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-FK) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/schema-lowercase-identifiers.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/schema-lowercase-identifiers.md new file mode 100644 index 00000000..f0072940 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/schema-lowercase-identifiers.md @@ -0,0 +1,55 @@ +--- +title: Use Lowercase Identifiers for Compatibility +impact: MEDIUM +impactDescription: Avoid case-sensitivity bugs with tools, ORMs, and AI assistants +tags: naming, identifiers, case-sensitivity, schema, conventions +--- + +## Use Lowercase Identifiers for Compatibility + +PostgreSQL folds unquoted identifiers to lowercase. Quoted mixed-case identifiers require quotes forever and cause issues with tools, ORMs, and AI assistants that may not recognize them. + +**Incorrect (mixed-case identifiers):** + +```sql +-- Quoted identifiers preserve case but require quotes everywhere +CREATE TABLE "Users" ( + "userId" bigint PRIMARY KEY, + "firstName" text, + "lastName" text +); + +-- Must always quote or queries fail +SELECT "firstName" FROM "Users" WHERE "userId" = 1; + +-- This fails - Users becomes users without quotes +SELECT firstName FROM Users; +-- ERROR: relation "users" does not exist +``` + +**Correct (lowercase snake_case):** + +```sql +-- Unquoted lowercase identifiers are portable and tool-friendly +CREATE TABLE users ( + user_id bigint PRIMARY KEY, + first_name text, + last_name text +); + +-- Works without quotes, recognized by all tools +SELECT first_name FROM users WHERE user_id = 1; +``` + +Common sources of mixed-case identifiers: + +```sql +-- ORMs often generate quoted camelCase - configure them to use snake_case +-- Migrations from other databases may preserve original casing +-- Some GUI tools quote identifiers by default - disable this + +-- If stuck with mixed-case, create views as a compatibility layer +CREATE VIEW users AS SELECT "userId" AS user_id, "firstName" AS first_name FROM "Users"; +``` + +Reference: [Identifiers and Key Words](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/schema-partitioning.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/schema-partitioning.md new file mode 100644 index 00000000..13137a03 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/schema-partitioning.md @@ -0,0 +1,55 @@ +--- +title: Partition Large Tables for Better Performance +impact: MEDIUM-HIGH +impactDescription: 5-20x faster queries and maintenance on large tables +tags: partitioning, large-tables, time-series, performance +--- + +## Partition Large Tables for Better Performance + +Partitioning splits a large table into smaller pieces, improving query performance and maintenance operations. + +**Incorrect (single large table):** + +```sql +create table events ( + id bigint generated always as identity, + created_at timestamptz, + data jsonb +); + +-- 500M rows, queries scan everything +select * from events where created_at > '2024-01-01'; -- Slow +vacuum events; -- Takes hours, locks table +``` + +**Correct (partitioned by time range):** + +```sql +create table events ( + id bigint generated always as identity, + created_at timestamptz not null, + data jsonb +) partition by range (created_at); + +-- Create partitions for each month +create table events_2024_01 partition of events + for values from ('2024-01-01') to ('2024-02-01'); + +create table events_2024_02 partition of events + for values from ('2024-02-01') to ('2024-03-01'); + +-- Queries only scan relevant partitions +select * from events where created_at > '2024-01-15'; -- Only scans events_2024_01+ + +-- Drop old data instantly +drop table events_2023_01; -- Instant vs DELETE taking hours +``` + +When to partition: + +- Tables > 100M rows +- Time-series data with date-based queries +- Need to efficiently drop old data + +Reference: [Table Partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/schema-primary-keys.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/schema-primary-keys.md new file mode 100644 index 00000000..fb0fbb16 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/schema-primary-keys.md @@ -0,0 +1,61 @@ +--- +title: Select Optimal Primary Key Strategy +impact: HIGH +impactDescription: Better index locality, reduced fragmentation +tags: primary-key, identity, uuid, serial, schema +--- + +## Select Optimal Primary Key Strategy + +Primary key choice affects insert performance, index size, and replication +efficiency. + +**Incorrect (problematic PK choices):** + +```sql +-- identity is the SQL-standard approach +create table users ( + id serial primary key -- Works, but IDENTITY is recommended +); + +-- Random UUIDs (v4) cause index fragmentation +create table orders ( + id uuid default gen_random_uuid() primary key -- UUIDv4 = random = scattered inserts +); +``` + +**Correct (optimal PK strategies):** + +```sql +-- Use IDENTITY for sequential IDs (SQL-standard, best for most cases) +create table users ( + id bigint generated always as identity primary key +); + +-- For distributed systems needing UUIDs, use UUIDv7 (time-ordered) +-- Requires pg_uuidv7 extension: create extension pg_uuidv7; +create table orders ( + id uuid default uuid_generate_v7() primary key -- Time-ordered, no fragmentation +); + +-- Alternative: time-prefixed IDs for sortable, distributed IDs (no extension needed) +create table events ( + id text default concat( + to_char(now() at time zone 'utc', 'YYYYMMDDHH24MISSMS'), + gen_random_uuid()::text + ) primary key +); +``` + +Guidelines: + +- Single database: `bigint identity` (sequential, 8 bytes, SQL-standard) +- Distributed/exposed IDs: UUIDv7 (requires pg_uuidv7) or ULID (time-ordered, no + fragmentation) +- `serial` works but `identity` is SQL-standard and preferred for new + applications +- Avoid random UUIDs (v4) as primary keys on large tables (causes index + fragmentation) + +Reference: +[Identity Columns](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-GENERATED-IDENTITY) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/security-privileges.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/security-privileges.md new file mode 100644 index 00000000..448ec345 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/security-privileges.md @@ -0,0 +1,54 @@ +--- +title: Apply Principle of Least Privilege +impact: MEDIUM +impactDescription: Reduced attack surface, better audit trail +tags: privileges, security, roles, permissions +--- + +## Apply Principle of Least Privilege + +Grant only the minimum permissions required. Never use superuser for application queries. + +**Incorrect (overly broad permissions):** + +```sql +-- Application uses superuser connection +-- Or grants ALL to application role +grant all privileges on all tables in schema public to app_user; +grant all privileges on all sequences in schema public to app_user; + +-- Any SQL injection becomes catastrophic +-- drop table users; cascades to everything +``` + +**Correct (minimal, specific grants):** + +```sql +-- Create role with no default privileges +create role app_readonly nologin; + +-- Grant only SELECT on specific tables +grant usage on schema public to app_readonly; +grant select on public.products, public.categories to app_readonly; + +-- Create role for writes with limited scope +create role app_writer nologin; +grant usage on schema public to app_writer; +grant select, insert, update on public.orders to app_writer; +grant usage on sequence orders_id_seq to app_writer; +-- No DELETE permission + +-- Login role inherits from these +create role app_user login password 'xxx'; +grant app_writer to app_user; +``` + +Revoke public defaults: + +```sql +-- Revoke default public access +revoke all on schema public from public; +revoke all on all tables in schema public from public; +``` + +Reference: [Roles and Privileges](https://supabase.com/blog/postgres-roles-and-privileges) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/security-rls-basics.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/security-rls-basics.md new file mode 100644 index 00000000..c61e1a85 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/security-rls-basics.md @@ -0,0 +1,50 @@ +--- +title: Enable Row Level Security for Multi-Tenant Data +impact: CRITICAL +impactDescription: Database-enforced tenant isolation, prevent data leaks +tags: rls, row-level-security, multi-tenant, security +--- + +## Enable Row Level Security for Multi-Tenant Data + +Row Level Security (RLS) enforces data access at the database level, ensuring users only see their own data. + +**Incorrect (application-level filtering only):** + +```sql +-- Relying only on application to filter +select * from orders where user_id = $current_user_id; + +-- Bug or bypass means all data is exposed! +select * from orders; -- Returns ALL orders +``` + +**Correct (database-enforced RLS):** + +```sql +-- Enable RLS on the table +alter table orders enable row level security; + +-- Create policy for users to see only their orders +create policy orders_user_policy on orders + for all + using (user_id = current_setting('app.current_user_id')::bigint); + +-- Force RLS even for table owners +alter table orders force row level security; + +-- Set user context and query +set app.current_user_id = '123'; +select * from orders; -- Only returns orders for user 123 +``` + +Policy for authenticated role: + +```sql +create policy orders_user_policy on orders + for all + to authenticated + using (user_id = auth.uid()); +``` + +Reference: [Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security) diff --git a/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/security-rls-performance.md b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/security-rls-performance.md new file mode 100644 index 00000000..b32d92f7 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/postgres-best-practices/rules/security-rls-performance.md @@ -0,0 +1,57 @@ +--- +title: Optimize RLS Policies for Performance +impact: HIGH +impactDescription: 5-10x faster RLS queries with proper patterns +tags: rls, performance, security, optimization +--- + +## Optimize RLS Policies for Performance + +Poorly written RLS policies can cause severe performance issues. Use subqueries and indexes strategically. + +**Incorrect (function called for every row):** + +```sql +create policy orders_policy on orders + using (auth.uid() = user_id); -- auth.uid() called per row! + +-- With 1M rows, auth.uid() is called 1M times +``` + +**Correct (wrap functions in SELECT):** + +```sql +create policy orders_policy on orders + using ((select auth.uid()) = user_id); -- Called once, cached + +-- 100x+ faster on large tables +``` + +Use security definer functions for complex checks: + +```sql +-- Create helper function (runs as definer, bypasses RLS) +create or replace function is_team_member(team_id bigint) +returns boolean +language sql +security definer +set search_path = '' +as $$ + select exists ( + select 1 from public.team_members + where team_id = $1 and user_id = (select auth.uid()) + ); +$$; + +-- Use in policy (indexed lookup, not per-row check) +create policy team_orders_policy on orders + using ((select is_team_member(team_id))); +``` + +Always add indexes on columns used in RLS policies: + +```sql +create index orders_user_id_idx on orders (user_id); +``` + +Reference: [RLS Performance](https://supabase.com/docs/guides/database/postgres/row-level-security#rls-performance-recommendations) diff --git a/plugins/antigravity-bundle-data-analytics/skills/sql-pro/SKILL.md b/plugins/antigravity-bundle-data-analytics/skills/sql-pro/SKILL.md new file mode 100644 index 00000000..15bdf324 --- /dev/null +++ b/plugins/antigravity-bundle-data-analytics/skills/sql-pro/SKILL.md @@ -0,0 +1,171 @@ +--- +name: sql-pro +description: Master modern SQL with cloud-native databases, OLTP/OLAP optimization, and advanced query techniques. Expert in performance tuning, data modeling, and hybrid analytical systems. +risk: unknown +source: community +date_added: '2026-02-27' +--- +You are an expert SQL specialist mastering modern database systems, performance optimization, and advanced analytical techniques across cloud-native and hybrid OLTP/OLAP environments. + +## Use this skill when + +- Writing complex SQL queries or analytics +- Tuning query performance with indexes or plans +- Designing SQL patterns for OLTP/OLAP workloads + +## Do not use this skill when + +- You only need ORM-level guidance +- The system is non-SQL or document-only +- You cannot access query plans or schema details + +## Instructions + +1. Define query goals, constraints, and expected outputs. +2. Inspect schema, statistics, and access paths. +3. Optimize queries and validate with EXPLAIN. +4. Verify correctness and performance under load. + +## Safety + +- Avoid heavy queries on production without safeguards. +- Use read replicas or limits for exploratory analysis. + +## Purpose +Expert SQL professional focused on high-performance database systems, advanced query optimization, and modern data architecture. Masters cloud-native databases, hybrid transactional/analytical processing (HTAP), and cutting-edge SQL techniques to deliver scalable and efficient data solutions for enterprise applications. + +## Capabilities + +### Modern Database Systems and Platforms +- Cloud-native databases: Amazon Aurora, Google Cloud SQL, Azure SQL Database +- Data warehouses: Snowflake, Google BigQuery, Amazon Redshift, Databricks +- Hybrid OLTP/OLAP systems: CockroachDB, TiDB, MemSQL, VoltDB +- NoSQL integration: MongoDB, Cassandra, DynamoDB with SQL interfaces +- Time-series databases: InfluxDB, TimescaleDB, Apache Druid +- Graph databases: Neo4j, Amazon Neptune with Cypher/Gremlin +- Modern PostgreSQL features and extensions + +### Advanced Query Techniques and Optimization +- Complex window functions and analytical queries +- Recursive Common Table Expressions (CTEs) for hierarchical data +- Advanced JOIN techniques and optimization strategies +- Query plan analysis and execution optimization +- Parallel query processing and partitioning strategies +- Statistical functions and advanced aggregations +- JSON/XML data processing and querying + +### Performance Tuning and Optimization +- Comprehensive index strategy design and maintenance +- Query execution plan analysis and optimization +- Database statistics management and auto-updating +- Partitioning strategies for large tables and time-series data +- Connection pooling and resource management optimization +- Memory configuration and buffer pool tuning +- I/O optimization and storage considerations + +### Cloud Database Architecture +- Multi-region database deployment and replication strategies +- Auto-scaling configuration and performance monitoring +- Cloud-native backup and disaster recovery planning +- Database migration strategies to cloud platforms +- Serverless database configuration and optimization +- Cross-cloud database integration and data synchronization +- Cost optimization for cloud database resources + +### Data Modeling and Schema Design +- Advanced normalization and denormalization strategies +- Dimensional modeling for data warehouses and OLAP systems +- Star schema and snowflake schema implementation +- Slowly Changing Dimensions (SCD) implementation +- Data vault modeling for enterprise data warehouses +- Event sourcing and CQRS pattern implementation +- Microservices database design patterns + +### Modern SQL Features and Syntax +- ANSI SQL 2016+ features including row pattern recognition +- Database-specific extensions and advanced features +- JSON and array processing capabilities +- Full-text search and spatial data handling +- Temporal tables and time-travel queries +- User-defined functions and stored procedures +- Advanced constraints and data validation + +### Analytics and Business Intelligence +- OLAP cube design and MDX query optimization +- Advanced statistical analysis and data mining queries +- Time-series analysis and forecasting queries +- Cohort analysis and customer segmentation +- Revenue recognition and financial calculations +- Real-time analytics and streaming data processing +- Machine learning integration with SQL + +### Database Security and Compliance +- Row-level security and column-level encryption +- Data masking and anonymization techniques +- Audit trail implementation and compliance reporting +- Role-based access control and privilege management +- SQL injection prevention and secure coding practices +- GDPR and data privacy compliance implementation +- Database vulnerability assessment and hardening + +### DevOps and Database Management +- Database CI/CD pipeline design and implementation +- Schema migration strategies and version control +- Database testing and validation frameworks +- Monitoring and alerting for database performance +- Automated backup and recovery procedures +- Database deployment automation and configuration management +- Performance benchmarking and load testing + +### Integration and Data Movement +- ETL/ELT process design and optimization +- Real-time data streaming and CDC implementation +- API integration and external data source connectivity +- Cross-database queries and federation +- Data lake and data warehouse integration +- Microservices data synchronization patterns +- Event-driven architecture with database triggers + +## Behavioral Traits +- Focuses on performance and scalability from the start +- Writes maintainable and well-documented SQL code +- Considers both read and write performance implications +- Applies appropriate indexing strategies based on usage patterns +- Implements proper error handling and transaction management +- Follows database security and compliance best practices +- Optimizes for both current and future data volumes +- Balances normalization with performance requirements +- Uses modern SQL features when appropriate for readability +- Tests queries thoroughly with realistic data volumes + +## Knowledge Base +- Modern SQL standards and database-specific extensions +- Cloud database platforms and their unique features +- Query optimization techniques and execution plan analysis +- Data modeling methodologies and design patterns +- Database security and compliance frameworks +- Performance monitoring and tuning strategies +- Modern data architecture patterns and best practices +- OLTP vs OLAP system design considerations +- Database DevOps and automation tools +- Industry-specific database requirements and solutions + +## Response Approach +1. **Analyze requirements** and identify optimal database approach +2. **Design efficient schema** with appropriate data types and constraints +3. **Write optimized queries** using modern SQL techniques +4. **Implement proper indexing** based on usage patterns +5. **Test performance** with realistic data volumes +6. **Document assumptions** and provide maintenance guidelines +7. **Consider scalability** for future data growth +8. **Validate security** and compliance requirements + +## Example Interactions +- "Optimize this complex analytical query for a billion-row table in Snowflake" +- "Design a database schema for a multi-tenant SaaS application with GDPR compliance" +- "Create a real-time dashboard query that updates every second with minimal latency" +- "Implement a data migration strategy from Oracle to cloud-native PostgreSQL" +- "Build a cohort analysis query to track customer retention over time" +- "Design an HTAP system that handles both transactions and analytics efficiently" +- "Create a time-series analysis query for IoT sensor data in TimescaleDB" +- "Optimize database performance for a high-traffic e-commerce platform" diff --git a/plugins/antigravity-bundle-data-engineering/.codex-plugin/plugin.json b/plugins/antigravity-bundle-data-engineering/.codex-plugin/plugin.json new file mode 100644 index 00000000..053c3cd9 --- /dev/null +++ b/plugins/antigravity-bundle-data-engineering/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-data-engineering", + "version": "8.10.0", + "description": "Install the \"Data Engineering\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "data-engineering", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Data Engineering", + "shortDescription": "Data & Analytics ยท 5 curated skills", + "longDescription": "For building data pipelines. Covers Data Engineer, Airflow DAG Patterns, and 3 more skills.", + "developerName": "sickn33 and contributors", + "category": "Data & Analytics", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-data-engineering/skills/airflow-dag-patterns/SKILL.md b/plugins/antigravity-bundle-data-engineering/skills/airflow-dag-patterns/SKILL.md new file mode 100644 index 00000000..4e285a72 --- /dev/null +++ b/plugins/antigravity-bundle-data-engineering/skills/airflow-dag-patterns/SKILL.md @@ -0,0 +1,44 @@ +--- +name: airflow-dag-patterns +description: "Build production Apache Airflow DAGs with best practices for operators, sensors, testing, and deployment. Use when creating data pipelines, orchestrating workflows, or scheduling batch jobs." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Apache Airflow DAG Patterns + +Production-ready patterns for Apache Airflow including DAG design, operators, sensors, testing, and deployment strategies. + +## Use this skill when + +- Creating data pipeline orchestration with Airflow +- Designing DAG structures and dependencies +- Implementing custom operators and sensors +- Testing Airflow DAGs locally +- Setting up Airflow in production +- Debugging failed DAG runs + +## Do not use this skill when + +- You only need a simple cron job or shell script +- Airflow is not part of the tooling stack +- The task is unrelated to workflow orchestration + +## Instructions + +1. Identify data sources, schedules, and dependencies. +2. Design idempotent tasks with clear ownership and retries. +3. Implement DAGs with observability and alerting hooks. +4. Validate in staging and document operational runbooks. + +Refer to `resources/implementation-playbook.md` for detailed patterns, checklists, and templates. + +## Safety + +- Avoid changing production DAG schedules without approval. +- Test backfills and retries carefully to prevent data duplication. + +## Resources + +- `resources/implementation-playbook.md` for detailed patterns, checklists, and templates. diff --git a/plugins/antigravity-bundle-data-engineering/skills/airflow-dag-patterns/resources/implementation-playbook.md b/plugins/antigravity-bundle-data-engineering/skills/airflow-dag-patterns/resources/implementation-playbook.md new file mode 100644 index 00000000..f70daa35 --- /dev/null +++ b/plugins/antigravity-bundle-data-engineering/skills/airflow-dag-patterns/resources/implementation-playbook.md @@ -0,0 +1,509 @@ +# Apache Airflow DAG Patterns Implementation Playbook + +This file contains detailed patterns, checklists, and code samples referenced by the skill. + +## Core Concepts + +### 1. DAG Design Principles + +| Principle | Description | +|-----------|-------------| +| **Idempotent** | Running twice produces same result | +| **Atomic** | Tasks succeed or fail completely | +| **Incremental** | Process only new/changed data | +| **Observable** | Logs, metrics, alerts at every step | + +### 2. Task Dependencies + +```python +# Linear +task1 >> task2 >> task3 + +# Fan-out +task1 >> [task2, task3, task4] + +# Fan-in +[task1, task2, task3] >> task4 + +# Complex +task1 >> task2 >> task4 +task1 >> task3 >> task4 +``` + +## Quick Start + +```python +# dags/example_dag.py +from datetime import datetime, timedelta +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.operators.empty import EmptyOperator + +default_args = { + 'owner': 'data-team', + 'depends_on_past': False, + 'email_on_failure': True, + 'email_on_retry': False, + 'retries': 3, + 'retry_delay': timedelta(minutes=5), + 'retry_exponential_backoff': True, + 'max_retry_delay': timedelta(hours=1), +} + +with DAG( + dag_id='example_etl', + default_args=default_args, + description='Example ETL pipeline', + schedule='0 6 * * *', # Daily at 6 AM + start_date=datetime(2024, 1, 1), + catchup=False, + tags=['etl', 'example'], + max_active_runs=1, +) as dag: + + start = EmptyOperator(task_id='start') + + def extract_data(**context): + execution_date = context['ds'] + # Extract logic here + return {'records': 1000} + + extract = PythonOperator( + task_id='extract', + python_callable=extract_data, + ) + + end = EmptyOperator(task_id='end') + + start >> extract >> end +``` + +## Patterns + +### Pattern 1: TaskFlow API (Airflow 2.0+) + +```python +# dags/taskflow_example.py +from datetime import datetime +from airflow.decorators import dag, task +from airflow.models import Variable + +@dag( + dag_id='taskflow_etl', + schedule='@daily', + start_date=datetime(2024, 1, 1), + catchup=False, + tags=['etl', 'taskflow'], +) +def taskflow_etl(): + """ETL pipeline using TaskFlow API""" + + @task() + def extract(source: str) -> dict: + """Extract data from source""" + import pandas as pd + + df = pd.read_csv(f's3://bucket/{source}/{{ ds }}.csv') + return {'data': df.to_dict(), 'rows': len(df)} + + @task() + def transform(extracted: dict) -> dict: + """Transform extracted data""" + import pandas as pd + + df = pd.DataFrame(extracted['data']) + df['processed_at'] = datetime.now() + df = df.dropna() + return {'data': df.to_dict(), 'rows': len(df)} + + @task() + def load(transformed: dict, target: str): + """Load data to target""" + import pandas as pd + + df = pd.DataFrame(transformed['data']) + df.to_parquet(f's3://bucket/{target}/{{ ds }}.parquet') + return transformed['rows'] + + @task() + def notify(rows_loaded: int): + """Send notification""" + print(f'Loaded {rows_loaded} rows') + + # Define dependencies with XCom passing + extracted = extract(source='raw_data') + transformed = transform(extracted) + loaded = load(transformed, target='processed_data') + notify(loaded) + +# Instantiate the DAG +taskflow_etl() +``` + +### Pattern 2: Dynamic DAG Generation + +```python +# dags/dynamic_dag_factory.py +from datetime import datetime, timedelta +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.models import Variable +import json + +# Configuration for multiple similar pipelines +PIPELINE_CONFIGS = [ + {'name': 'customers', 'schedule': '@daily', 'source': 's3://raw/customers'}, + {'name': 'orders', 'schedule': '@hourly', 'source': 's3://raw/orders'}, + {'name': 'products', 'schedule': '@weekly', 'source': 's3://raw/products'}, +] + +def create_dag(config: dict) -> DAG: + """Factory function to create DAGs from config""" + + dag_id = f"etl_{config['name']}" + + default_args = { + 'owner': 'data-team', + 'retries': 3, + 'retry_delay': timedelta(minutes=5), + } + + dag = DAG( + dag_id=dag_id, + default_args=default_args, + schedule=config['schedule'], + start_date=datetime(2024, 1, 1), + catchup=False, + tags=['etl', 'dynamic', config['name']], + ) + + with dag: + def extract_fn(source, **context): + print(f"Extracting from {source} for {context['ds']}") + + def transform_fn(**context): + print(f"Transforming data for {context['ds']}") + + def load_fn(table_name, **context): + print(f"Loading to {table_name} for {context['ds']}") + + extract = PythonOperator( + task_id='extract', + python_callable=extract_fn, + op_kwargs={'source': config['source']}, + ) + + transform = PythonOperator( + task_id='transform', + python_callable=transform_fn, + ) + + load = PythonOperator( + task_id='load', + python_callable=load_fn, + op_kwargs={'table_name': config['name']}, + ) + + extract >> transform >> load + + return dag + +# Generate DAGs +for config in PIPELINE_CONFIGS: + globals()[f"dag_{config['name']}"] = create_dag(config) +``` + +### Pattern 3: Branching and Conditional Logic + +```python +# dags/branching_example.py +from airflow.decorators import dag, task +from airflow.operators.python import BranchPythonOperator +from airflow.operators.empty import EmptyOperator +from airflow.utils.trigger_rule import TriggerRule + +@dag( + dag_id='branching_pipeline', + schedule='@daily', + start_date=datetime(2024, 1, 1), + catchup=False, +) +def branching_pipeline(): + + @task() + def check_data_quality() -> dict: + """Check data quality and return metrics""" + quality_score = 0.95 # Simulated + return {'score': quality_score, 'rows': 10000} + + def choose_branch(**context) -> str: + """Determine which branch to execute""" + ti = context['ti'] + metrics = ti.xcom_pull(task_ids='check_data_quality') + + if metrics['score'] >= 0.9: + return 'high_quality_path' + elif metrics['score'] >= 0.7: + return 'medium_quality_path' + else: + return 'low_quality_path' + + quality_check = check_data_quality() + + branch = BranchPythonOperator( + task_id='branch', + python_callable=choose_branch, + ) + + high_quality = EmptyOperator(task_id='high_quality_path') + medium_quality = EmptyOperator(task_id='medium_quality_path') + low_quality = EmptyOperator(task_id='low_quality_path') + + # Join point - runs after any branch completes + join = EmptyOperator( + task_id='join', + trigger_rule=TriggerRule.NONE_FAILED_MIN_ONE_SUCCESS, + ) + + quality_check >> branch >> [high_quality, medium_quality, low_quality] >> join + +branching_pipeline() +``` + +### Pattern 4: Sensors and External Dependencies + +```python +# dags/sensor_patterns.py +from datetime import datetime, timedelta +from airflow import DAG +from airflow.sensors.filesystem import FileSensor +from airflow.providers.amazon.aws.sensors.s3 import S3KeySensor +from airflow.sensors.external_task import ExternalTaskSensor +from airflow.operators.python import PythonOperator + +with DAG( + dag_id='sensor_example', + schedule='@daily', + start_date=datetime(2024, 1, 1), + catchup=False, +) as dag: + + # Wait for file on S3 + wait_for_file = S3KeySensor( + task_id='wait_for_s3_file', + bucket_name='data-lake', + bucket_key='raw/{{ ds }}/data.parquet', + aws_conn_id='aws_default', + timeout=60 * 60 * 2, # 2 hours + poke_interval=60 * 5, # Check every 5 minutes + mode='reschedule', # Free up worker slot while waiting + ) + + # Wait for another DAG to complete + wait_for_upstream = ExternalTaskSensor( + task_id='wait_for_upstream_dag', + external_dag_id='upstream_etl', + external_task_id='final_task', + execution_date_fn=lambda dt: dt, # Same execution date + timeout=60 * 60 * 3, + mode='reschedule', + ) + + # Custom sensor using @task.sensor decorator + @task.sensor(poke_interval=60, timeout=3600, mode='reschedule') + def wait_for_api() -> PokeReturnValue: + """Custom sensor for API availability""" + import requests + + response = requests.get('https://api.example.com/health') + is_done = response.status_code == 200 + + return PokeReturnValue(is_done=is_done, xcom_value=response.json()) + + api_ready = wait_for_api() + + def process_data(**context): + api_result = context['ti'].xcom_pull(task_ids='wait_for_api') + print(f"API returned: {api_result}") + + process = PythonOperator( + task_id='process', + python_callable=process_data, + ) + + [wait_for_file, wait_for_upstream, api_ready] >> process +``` + +### Pattern 5: Error Handling and Alerts + +```python +# dags/error_handling.py +from datetime import datetime, timedelta +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.utils.trigger_rule import TriggerRule +from airflow.models import Variable + +def task_failure_callback(context): + """Callback on task failure""" + task_instance = context['task_instance'] + exception = context.get('exception') + + # Send to Slack/PagerDuty/etc + message = f""" + Task Failed! + DAG: {task_instance.dag_id} + Task: {task_instance.task_id} + Execution Date: {context['ds']} + Error: {exception} + Log URL: {task_instance.log_url} + """ + # send_slack_alert(message) + print(message) + +def dag_failure_callback(context): + """Callback on DAG failure""" + # Aggregate failures, send summary + pass + +with DAG( + dag_id='error_handling_example', + schedule='@daily', + start_date=datetime(2024, 1, 1), + catchup=False, + on_failure_callback=dag_failure_callback, + default_args={ + 'on_failure_callback': task_failure_callback, + 'retries': 3, + 'retry_delay': timedelta(minutes=5), + }, +) as dag: + + def might_fail(**context): + import random + if random.random() < 0.3: + raise ValueError("Random failure!") + return "Success" + + risky_task = PythonOperator( + task_id='risky_task', + python_callable=might_fail, + ) + + def cleanup(**context): + """Cleanup runs regardless of upstream failures""" + print("Cleaning up...") + + cleanup_task = PythonOperator( + task_id='cleanup', + python_callable=cleanup, + trigger_rule=TriggerRule.ALL_DONE, # Run even if upstream fails + ) + + def notify_success(**context): + """Only runs if all upstream succeeded""" + print("All tasks succeeded!") + + success_notification = PythonOperator( + task_id='notify_success', + python_callable=notify_success, + trigger_rule=TriggerRule.ALL_SUCCESS, + ) + + risky_task >> [cleanup_task, success_notification] +``` + +### Pattern 6: Testing DAGs + +```python +# tests/test_dags.py +import pytest +from datetime import datetime +from airflow.models import DagBag + +@pytest.fixture +def dagbag(): + return DagBag(dag_folder='dags/', include_examples=False) + +def test_dag_loaded(dagbag): + """Test that all DAGs load without errors""" + assert len(dagbag.import_errors) == 0, f"DAG import errors: {dagbag.import_errors}" + +def test_dag_structure(dagbag): + """Test specific DAG structure""" + dag = dagbag.get_dag('example_etl') + + assert dag is not None + assert len(dag.tasks) == 3 + assert dag.schedule_interval == '0 6 * * *' + +def test_task_dependencies(dagbag): + """Test task dependencies are correct""" + dag = dagbag.get_dag('example_etl') + + extract_task = dag.get_task('extract') + assert 'start' in [t.task_id for t in extract_task.upstream_list] + assert 'end' in [t.task_id for t in extract_task.downstream_list] + +def test_dag_integrity(dagbag): + """Test DAG has no cycles and is valid""" + for dag_id, dag in dagbag.dags.items(): + assert dag.test_cycle() is None, f"Cycle detected in {dag_id}" + +# Test individual task logic +def test_extract_function(): + """Unit test for extract function""" + from dags.example_dag import extract_data + + result = extract_data(ds='2024-01-01') + assert 'records' in result + assert isinstance(result['records'], int) +``` + +## Project Structure + +``` +airflow/ +โ”œโ”€โ”€ dags/ +โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”œโ”€โ”€ common/ +โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”‚ โ”œโ”€โ”€ operators.py # Custom operators +โ”‚ โ”‚ โ”œโ”€โ”€ sensors.py # Custom sensors +โ”‚ โ”‚ โ””โ”€โ”€ callbacks.py # Alert callbacks +โ”‚ โ”œโ”€โ”€ etl/ +โ”‚ โ”‚ โ”œโ”€โ”€ customers.py +โ”‚ โ”‚ โ””โ”€โ”€ orders.py +โ”‚ โ””โ”€โ”€ ml/ +โ”‚ โ””โ”€โ”€ training.py +โ”œโ”€โ”€ plugins/ +โ”‚ โ””โ”€โ”€ custom_plugin.py +โ”œโ”€โ”€ tests/ +โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”œโ”€โ”€ test_dags.py +โ”‚ โ””โ”€โ”€ test_operators.py +โ”œโ”€โ”€ docker-compose.yml +โ””โ”€โ”€ requirements.txt +``` + +## Best Practices + +### Do's +- **Use TaskFlow API** - Cleaner code, automatic XCom +- **Set timeouts** - Prevent zombie tasks +- **Use `mode='reschedule'`** - For sensors, free up workers +- **Test DAGs** - Unit tests and integration tests +- **Idempotent tasks** - Safe to retry + +### Don'ts +- **Don't use `depends_on_past=True`** - Creates bottlenecks +- **Don't hardcode dates** - Use `{{ ds }}` macros +- **Don't use global state** - Tasks should be stateless +- **Don't skip catchup blindly** - Understand implications +- **Don't put heavy logic in DAG file** - Import from modules + +## Resources + +- [Airflow Documentation](https://airflow.apache.org/docs/) +- [Astronomer Guides](https://docs.astronomer.io/learn) +- [TaskFlow API](https://airflow.apache.org/docs/apache-airflow/stable/tutorial/taskflow.html) diff --git a/plugins/antigravity-bundle-data-engineering/skills/data-engineer/SKILL.md b/plugins/antigravity-bundle-data-engineering/skills/data-engineer/SKILL.md new file mode 100644 index 00000000..1d5fc174 --- /dev/null +++ b/plugins/antigravity-bundle-data-engineering/skills/data-engineer/SKILL.md @@ -0,0 +1,222 @@ +--- +name: data-engineer +description: Build scalable data pipelines, modern data warehouses, and real-time streaming architectures. Implements Apache Spark, dbt, Airflow, and cloud-native data platforms. +risk: unknown +source: community +date_added: '2026-02-27' +--- +You are a data engineer specializing in scalable data pipelines, modern data architecture, and analytics infrastructure. + +## Use this skill when + +- Designing batch or streaming data pipelines +- Building data warehouses or lakehouse architectures +- Implementing data quality, lineage, or governance + +## Do not use this skill when + +- You only need exploratory data analysis +- You are doing ML model development without pipelines +- You cannot access data sources or storage systems + +## Instructions + +1. Define sources, SLAs, and data contracts. +2. Choose architecture, storage, and orchestration tools. +3. Implement ingestion, transformation, and validation. +4. Monitor quality, costs, and operational reliability. + +## Safety + +- Protect PII and enforce least-privilege access. +- Validate data before writing to production sinks. + +## Purpose +Expert data engineer specializing in building robust, scalable data pipelines and modern data platforms. Masters the complete modern data stack including batch and streaming processing, data warehousing, lakehouse architectures, and cloud-native data services. Focuses on reliable, performant, and cost-effective data solutions. + +## Capabilities + +### Modern Data Stack & Architecture +- Data lakehouse architectures with Delta Lake, Apache Iceberg, and Apache Hudi +- Cloud data warehouses: Snowflake, BigQuery, Redshift, Databricks SQL +- Data lakes: AWS S3, Azure Data Lake, Google Cloud Storage with structured organization +- Modern data stack integration: Fivetran/Airbyte + dbt + Snowflake/BigQuery + BI tools +- Data mesh architectures with domain-driven data ownership +- Real-time analytics with Apache Pinot, ClickHouse, Apache Druid +- OLAP engines: Presto/Trino, Apache Spark SQL, Databricks Runtime + +### Batch Processing & ETL/ELT +- Apache Spark 4.0 with optimized Catalyst engine and columnar processing +- dbt Core/Cloud for data transformations with version control and testing +- Apache Airflow for complex workflow orchestration and dependency management +- Databricks for unified analytics platform with collaborative notebooks +- AWS Glue, Azure Synapse Analytics, Google Dataflow for cloud ETL +- Custom Python/Scala data processing with pandas, Polars, Ray +- Data validation and quality monitoring with Great Expectations +- Data profiling and discovery with Apache Atlas, DataHub, Amundsen + +### Real-Time Streaming & Event Processing +- Apache Kafka and Confluent Platform for event streaming +- Apache Pulsar for geo-replicated messaging and multi-tenancy +- Apache Flink and Kafka Streams for complex event processing +- AWS Kinesis, Azure Event Hubs, Google Pub/Sub for cloud streaming +- Real-time data pipelines with change data capture (CDC) +- Stream processing with windowing, aggregations, and joins +- Event-driven architectures with schema evolution and compatibility +- Real-time feature engineering for ML applications + +### Workflow Orchestration & Pipeline Management +- Apache Airflow with custom operators and dynamic DAG generation +- Prefect for modern workflow orchestration with dynamic execution +- Dagster for asset-based data pipeline orchestration +- Azure Data Factory and AWS Step Functions for cloud workflows +- GitHub Actions and GitLab CI/CD for data pipeline automation +- Kubernetes CronJobs and Argo Workflows for container-native scheduling +- Pipeline monitoring, alerting, and failure recovery mechanisms +- Data lineage tracking and impact analysis + +### Data Modeling & Warehousing +- Dimensional modeling: star schema, snowflake schema design +- Data vault modeling for enterprise data warehousing +- One Big Table (OBT) and wide table approaches for analytics +- Slowly changing dimensions (SCD) implementation strategies +- Data partitioning and clustering strategies for performance +- Incremental data loading and change data capture patterns +- Data archiving and retention policy implementation +- Performance tuning: indexing, materialized views, query optimization + +### Cloud Data Platforms & Services + +#### AWS Data Engineering Stack +- Amazon S3 for data lake with intelligent tiering and lifecycle policies +- AWS Glue for serverless ETL with automatic schema discovery +- Amazon Redshift and Redshift Spectrum for data warehousing +- Amazon EMR and EMR Serverless for big data processing +- Amazon Kinesis for real-time streaming and analytics +- AWS Lake Formation for data lake governance and security +- Amazon Athena for serverless SQL queries on S3 data +- AWS DataBrew for visual data preparation + +#### Azure Data Engineering Stack +- Azure Data Lake Storage Gen2 for hierarchical data lake +- Azure Synapse Analytics for unified analytics platform +- Azure Data Factory for cloud-native data integration +- Azure Databricks for collaborative analytics and ML +- Azure Stream Analytics for real-time stream processing +- Azure Purview for unified data governance and catalog +- Azure SQL Database and Cosmos DB for operational data stores +- Power BI integration for self-service analytics + +#### GCP Data Engineering Stack +- Google Cloud Storage for object storage and data lake +- BigQuery for serverless data warehouse with ML capabilities +- Cloud Dataflow for stream and batch data processing +- Cloud Composer (managed Airflow) for workflow orchestration +- Cloud Pub/Sub for messaging and event ingestion +- Cloud Data Fusion for visual data integration +- Cloud Dataproc for managed Hadoop and Spark clusters +- Looker integration for business intelligence + +### Data Quality & Governance +- Data quality frameworks with Great Expectations and custom validators +- Data lineage tracking with DataHub, Apache Atlas, Collibra +- Data catalog implementation with metadata management +- Data privacy and compliance: GDPR, CCPA, HIPAA considerations +- Data masking and anonymization techniques +- Access control and row-level security implementation +- Data monitoring and alerting for quality issues +- Schema evolution and backward compatibility management + +### Performance Optimization & Scaling +- Query optimization techniques across different engines +- Partitioning and clustering strategies for large datasets +- Caching and materialized view optimization +- Resource allocation and cost optimization for cloud workloads +- Auto-scaling and spot instance utilization for batch jobs +- Performance monitoring and bottleneck identification +- Data compression and columnar storage optimization +- Distributed processing optimization with appropriate parallelism + +### Database Technologies & Integration +- Relational databases: PostgreSQL, MySQL, SQL Server integration +- NoSQL databases: MongoDB, Cassandra, DynamoDB for diverse data types +- Time-series databases: InfluxDB, TimescaleDB for IoT and monitoring data +- Graph databases: Neo4j, Amazon Neptune for relationship analysis +- Search engines: Elasticsearch, OpenSearch for full-text search +- Vector databases: Pinecone, Qdrant for AI/ML applications +- Database replication, CDC, and synchronization patterns +- Multi-database query federation and virtualization + +### Infrastructure & DevOps for Data +- Infrastructure as Code with Terraform, CloudFormation, Bicep +- Containerization with Docker and Kubernetes for data applications +- CI/CD pipelines for data infrastructure and code deployment +- Version control strategies for data code, schemas, and configurations +- Environment management: dev, staging, production data environments +- Secrets management and secure credential handling +- Monitoring and logging with Prometheus, Grafana, ELK stack +- Disaster recovery and backup strategies for data systems + +### Data Security & Compliance +- Encryption at rest and in transit for all data movement +- Identity and access management (IAM) for data resources +- Network security and VPC configuration for data platforms +- Audit logging and compliance reporting automation +- Data classification and sensitivity labeling +- Privacy-preserving techniques: differential privacy, k-anonymity +- Secure data sharing and collaboration patterns +- Compliance automation and policy enforcement + +### Integration & API Development +- RESTful APIs for data access and metadata management +- GraphQL APIs for flexible data querying and federation +- Real-time APIs with WebSockets and Server-Sent Events +- Data API gateways and rate limiting implementation +- Event-driven integration patterns with message queues +- Third-party data source integration: APIs, databases, SaaS platforms +- Data synchronization and conflict resolution strategies +- API documentation and developer experience optimization + +## Behavioral Traits +- Prioritizes data reliability and consistency over quick fixes +- Implements comprehensive monitoring and alerting from the start +- Focuses on scalable and maintainable data architecture decisions +- Emphasizes cost optimization while maintaining performance requirements +- Plans for data governance and compliance from the design phase +- Uses infrastructure as code for reproducible deployments +- Implements thorough testing for data pipelines and transformations +- Documents data schemas, lineage, and business logic clearly +- Stays current with evolving data technologies and best practices +- Balances performance optimization with operational simplicity + +## Knowledge Base +- Modern data stack architectures and integration patterns +- Cloud-native data services and their optimization techniques +- Streaming and batch processing design patterns +- Data modeling techniques for different analytical use cases +- Performance tuning across various data processing engines +- Data governance and quality management best practices +- Cost optimization strategies for cloud data workloads +- Security and compliance requirements for data systems +- DevOps practices adapted for data engineering workflows +- Emerging trends in data architecture and tooling + +## Response Approach +1. **Analyze data requirements** for scale, latency, and consistency needs +2. **Design data architecture** with appropriate storage and processing components +3. **Implement robust data pipelines** with comprehensive error handling and monitoring +4. **Include data quality checks** and validation throughout the pipeline +5. **Consider cost and performance** implications of architectural decisions +6. **Plan for data governance** and compliance requirements early +7. **Implement monitoring and alerting** for data pipeline health and performance +8. **Document data flows** and provide operational runbooks for maintenance + +## Example Interactions +- "Design a real-time streaming pipeline that processes 1M events per second from Kafka to BigQuery" +- "Build a modern data stack with dbt, Snowflake, and Fivetran for dimensional modeling" +- "Implement a cost-optimized data lakehouse architecture using Delta Lake on AWS" +- "Create a data quality framework that monitors and alerts on data anomalies" +- "Design a multi-tenant data platform with proper isolation and governance" +- "Build a change data capture pipeline for real-time synchronization between databases" +- "Implement a data mesh architecture with domain-specific data products" +- "Create a scalable ETL pipeline that handles late-arriving and out-of-order data" diff --git a/plugins/antigravity-bundle-data-engineering/skills/dbt-transformation-patterns/SKILL.md b/plugins/antigravity-bundle-data-engineering/skills/dbt-transformation-patterns/SKILL.md new file mode 100644 index 00000000..d051237e --- /dev/null +++ b/plugins/antigravity-bundle-data-engineering/skills/dbt-transformation-patterns/SKILL.md @@ -0,0 +1,37 @@ +--- +name: dbt-transformation-patterns +description: "Production-ready patterns for dbt (data build tool) including model organization, testing strategies, documentation, and incremental processing." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# dbt Transformation Patterns + +Production-ready patterns for dbt (data build tool) including model organization, testing strategies, documentation, and incremental processing. + +## Use this skill when + +- Building data transformation pipelines with dbt +- Organizing models into staging, intermediate, and marts layers +- Implementing data quality tests and documentation +- Creating incremental models for large datasets +- Setting up dbt project structure and conventions + +## Do not use this skill when + +- The project is not using dbt or a warehouse-backed workflow +- You only need ad-hoc SQL queries +- There is no access to source data or schemas + +## Instructions + +- Define model layers, naming, and ownership. +- Implement tests, documentation, and freshness checks. +- Choose materializations and incremental strategies. +- Optimize runs with selectors and CI workflows. +- If detailed patterns are required, open `resources/implementation-playbook.md`. + +## Resources + +- `resources/implementation-playbook.md` for detailed dbt patterns and examples. diff --git a/plugins/antigravity-bundle-data-engineering/skills/dbt-transformation-patterns/resources/implementation-playbook.md b/plugins/antigravity-bundle-data-engineering/skills/dbt-transformation-patterns/resources/implementation-playbook.md new file mode 100644 index 00000000..ee487341 --- /dev/null +++ b/plugins/antigravity-bundle-data-engineering/skills/dbt-transformation-patterns/resources/implementation-playbook.md @@ -0,0 +1,547 @@ +# dbt Transformation Patterns Implementation Playbook + +This file contains detailed patterns, checklists, and code samples referenced by the skill. + +## Core Concepts + +### 1. Model Layers (Medallion Architecture) + +``` +sources/ Raw data definitions + โ†“ +staging/ 1:1 with source, light cleaning + โ†“ +intermediate/ Business logic, joins, aggregations + โ†“ +marts/ Final analytics tables +``` + +### 2. Naming Conventions + +| Layer | Prefix | Example | +|-------|--------|---------| +| Staging | `stg_` | `stg_stripe__payments` | +| Intermediate | `int_` | `int_payments_pivoted` | +| Marts | `dim_`, `fct_` | `dim_customers`, `fct_orders` | + +## Quick Start + +```yaml +# dbt_project.yml +name: 'analytics' +version: '1.0.0' +profile: 'analytics' + +model-paths: ["models"] +analysis-paths: ["analyses"] +test-paths: ["tests"] +seed-paths: ["seeds"] +macro-paths: ["macros"] + +vars: + start_date: '2020-01-01' + +models: + analytics: + staging: + +materialized: view + +schema: staging + intermediate: + +materialized: ephemeral + marts: + +materialized: table + +schema: analytics +``` + +``` +# Project structure +models/ +โ”œโ”€โ”€ staging/ +โ”‚ โ”œโ”€โ”€ stripe/ +โ”‚ โ”‚ โ”œโ”€โ”€ _stripe__sources.yml +โ”‚ โ”‚ โ”œโ”€โ”€ _stripe__models.yml +โ”‚ โ”‚ โ”œโ”€โ”€ stg_stripe__customers.sql +โ”‚ โ”‚ โ””โ”€โ”€ stg_stripe__payments.sql +โ”‚ โ””โ”€โ”€ shopify/ +โ”‚ โ”œโ”€โ”€ _shopify__sources.yml +โ”‚ โ””โ”€โ”€ stg_shopify__orders.sql +โ”œโ”€โ”€ intermediate/ +โ”‚ โ””โ”€โ”€ finance/ +โ”‚ โ””โ”€โ”€ int_payments_pivoted.sql +โ””โ”€โ”€ marts/ + โ”œโ”€โ”€ core/ + โ”‚ โ”œโ”€โ”€ _core__models.yml + โ”‚ โ”œโ”€โ”€ dim_customers.sql + โ”‚ โ””โ”€โ”€ fct_orders.sql + โ””โ”€โ”€ finance/ + โ””โ”€โ”€ fct_revenue.sql +``` + +## Patterns + +### Pattern 1: Source Definitions + +```yaml +# models/staging/stripe/_stripe__sources.yml +version: 2 + +sources: + - name: stripe + description: Raw Stripe data loaded via Fivetran + database: raw + schema: stripe + loader: fivetran + loaded_at_field: _fivetran_synced + freshness: + warn_after: {count: 12, period: hour} + error_after: {count: 24, period: hour} + tables: + - name: customers + description: Stripe customer records + columns: + - name: id + description: Primary key + tests: + - unique + - not_null + - name: email + description: Customer email + - name: created + description: Account creation timestamp + + - name: payments + description: Stripe payment transactions + columns: + - name: id + tests: + - unique + - not_null + - name: customer_id + tests: + - not_null + - relationships: + to: source('stripe', 'customers') + field: id +``` + +### Pattern 2: Staging Models + +```sql +-- models/staging/stripe/stg_stripe__customers.sql +with source as ( + select * from {{ source('stripe', 'customers') }} +), + +renamed as ( + select + -- ids + id as customer_id, + + -- strings + lower(email) as email, + name as customer_name, + + -- timestamps + created as created_at, + + -- metadata + _fivetran_synced as _loaded_at + + from source +) + +select * from renamed +``` + +```sql +-- models/staging/stripe/stg_stripe__payments.sql +{{ + config( + materialized='incremental', + unique_key='payment_id', + on_schema_change='append_new_columns' + ) +}} + +with source as ( + select * from {{ source('stripe', 'payments') }} + + {% if is_incremental() %} + where _fivetran_synced > (select max(_loaded_at) from {{ this }}) + {% endif %} +), + +renamed as ( + select + -- ids + id as payment_id, + customer_id, + invoice_id, + + -- amounts (convert cents to dollars) + amount / 100.0 as amount, + amount_refunded / 100.0 as amount_refunded, + + -- status + status as payment_status, + + -- timestamps + created as created_at, + + -- metadata + _fivetran_synced as _loaded_at + + from source +) + +select * from renamed +``` + +### Pattern 3: Intermediate Models + +```sql +-- models/intermediate/finance/int_payments_pivoted_to_customer.sql +with payments as ( + select * from {{ ref('stg_stripe__payments') }} +), + +customers as ( + select * from {{ ref('stg_stripe__customers') }} +), + +payment_summary as ( + select + customer_id, + count(*) as total_payments, + count(case when payment_status = 'succeeded' then 1 end) as successful_payments, + sum(case when payment_status = 'succeeded' then amount else 0 end) as total_amount_paid, + min(created_at) as first_payment_at, + max(created_at) as last_payment_at + from payments + group by customer_id +) + +select + customers.customer_id, + customers.email, + customers.created_at as customer_created_at, + coalesce(payment_summary.total_payments, 0) as total_payments, + coalesce(payment_summary.successful_payments, 0) as successful_payments, + coalesce(payment_summary.total_amount_paid, 0) as lifetime_value, + payment_summary.first_payment_at, + payment_summary.last_payment_at + +from customers +left join payment_summary using (customer_id) +``` + +### Pattern 4: Mart Models (Dimensions and Facts) + +```sql +-- models/marts/core/dim_customers.sql +{{ + config( + materialized='table', + unique_key='customer_id' + ) +}} + +with customers as ( + select * from {{ ref('int_payments_pivoted_to_customer') }} +), + +orders as ( + select * from {{ ref('stg_shopify__orders') }} +), + +order_summary as ( + select + customer_id, + count(*) as total_orders, + sum(total_price) as total_order_value, + min(created_at) as first_order_at, + max(created_at) as last_order_at + from orders + group by customer_id +), + +final as ( + select + -- surrogate key + {{ dbt_utils.generate_surrogate_key(['customers.customer_id']) }} as customer_key, + + -- natural key + customers.customer_id, + + -- attributes + customers.email, + customers.customer_created_at, + + -- payment metrics + customers.total_payments, + customers.successful_payments, + customers.lifetime_value, + customers.first_payment_at, + customers.last_payment_at, + + -- order metrics + coalesce(order_summary.total_orders, 0) as total_orders, + coalesce(order_summary.total_order_value, 0) as total_order_value, + order_summary.first_order_at, + order_summary.last_order_at, + + -- calculated fields + case + when customers.lifetime_value >= 1000 then 'high' + when customers.lifetime_value >= 100 then 'medium' + else 'low' + end as customer_tier, + + -- timestamps + current_timestamp as _loaded_at + + from customers + left join order_summary using (customer_id) +) + +select * from final +``` + +```sql +-- models/marts/core/fct_orders.sql +{{ + config( + materialized='incremental', + unique_key='order_id', + incremental_strategy='merge' + ) +}} + +with orders as ( + select * from {{ ref('stg_shopify__orders') }} + + {% if is_incremental() %} + where updated_at > (select max(updated_at) from {{ this }}) + {% endif %} +), + +customers as ( + select * from {{ ref('dim_customers') }} +), + +final as ( + select + -- keys + orders.order_id, + customers.customer_key, + orders.customer_id, + + -- dimensions + orders.order_status, + orders.fulfillment_status, + orders.payment_status, + + -- measures + orders.subtotal, + orders.tax, + orders.shipping, + orders.total_price, + orders.total_discount, + orders.item_count, + + -- timestamps + orders.created_at, + orders.updated_at, + orders.fulfilled_at, + + -- metadata + current_timestamp as _loaded_at + + from orders + left join customers on orders.customer_id = customers.customer_id +) + +select * from final +``` + +### Pattern 5: Testing and Documentation + +```yaml +# models/marts/core/_core__models.yml +version: 2 + +models: + - name: dim_customers + description: Customer dimension with payment and order metrics + columns: + - name: customer_key + description: Surrogate key for the customer dimension + tests: + - unique + - not_null + + - name: customer_id + description: Natural key from source system + tests: + - unique + - not_null + + - name: email + description: Customer email address + tests: + - not_null + + - name: customer_tier + description: Customer value tier based on lifetime value + tests: + - accepted_values: + values: ['high', 'medium', 'low'] + + - name: lifetime_value + description: Total amount paid by customer + tests: + - dbt_utils.expression_is_true: + expression: ">= 0" + + - name: fct_orders + description: Order fact table with all order transactions + tests: + - dbt_utils.recency: + datepart: day + field: created_at + interval: 1 + columns: + - name: order_id + tests: + - unique + - not_null + - name: customer_key + tests: + - not_null + - relationships: + to: ref('dim_customers') + field: customer_key +``` + +### Pattern 6: Macros and DRY Code + +```sql +-- macros/cents_to_dollars.sql +{% macro cents_to_dollars(column_name, precision=2) %} + round({{ column_name }} / 100.0, {{ precision }}) +{% endmacro %} + +-- macros/generate_schema_name.sql +{% macro generate_schema_name(custom_schema_name, node) %} + {%- set default_schema = target.schema -%} + {%- if custom_schema_name is none -%} + {{ default_schema }} + {%- else -%} + {{ default_schema }}_{{ custom_schema_name }} + {%- endif -%} +{% endmacro %} + +-- macros/limit_data_in_dev.sql +{% macro limit_data_in_dev(column_name, days=3) %} + {% if target.name == 'dev' %} + where {{ column_name }} >= dateadd(day, -{{ days }}, current_date) + {% endif %} +{% endmacro %} + +-- Usage in model +select * from {{ ref('stg_orders') }} +{{ limit_data_in_dev('created_at') }} +``` + +### Pattern 7: Incremental Strategies + +```sql +-- Delete+Insert (default for most warehouses) +{{ + config( + materialized='incremental', + unique_key='id', + incremental_strategy='delete+insert' + ) +}} + +-- Merge (best for late-arriving data) +{{ + config( + materialized='incremental', + unique_key='id', + incremental_strategy='merge', + merge_update_columns=['status', 'amount', 'updated_at'] + ) +}} + +-- Insert Overwrite (partition-based) +{{ + config( + materialized='incremental', + incremental_strategy='insert_overwrite', + partition_by={ + "field": "created_date", + "data_type": "date", + "granularity": "day" + } + ) +}} + +select + *, + date(created_at) as created_date +from {{ ref('stg_events') }} + +{% if is_incremental() %} +where created_date >= dateadd(day, -3, current_date) +{% endif %} +``` + +## dbt Commands + +```bash +# Development +dbt run # Run all models +dbt run --select staging # Run staging models only +dbt run --select +fct_orders # Run fct_orders and its upstream +dbt run --select fct_orders+ # Run fct_orders and its downstream +dbt run --full-refresh # Rebuild incremental models + +# Testing +dbt test # Run all tests +dbt test --select stg_stripe # Test specific models +dbt build # Run + test in DAG order + +# Documentation +dbt docs generate # Generate docs +dbt docs serve # Serve docs locally + +# Debugging +dbt compile # Compile SQL without running +dbt debug # Test connection +dbt ls --select tag:critical # List models by tag +``` + +## Best Practices + +### Do's +- **Use staging layer** - Clean data once, use everywhere +- **Test aggressively** - Not null, unique, relationships +- **Document everything** - Column descriptions, model descriptions +- **Use incremental** - For tables > 1M rows +- **Version control** - dbt project in Git + +### Don'ts +- **Don't skip staging** - Raw โ†’ mart is tech debt +- **Don't hardcode dates** - Use `{{ var('start_date') }}` +- **Don't repeat logic** - Extract to macros +- **Don't test in prod** - Use dev target +- **Don't ignore freshness** - Monitor source data + +## Resources + +- [dbt Documentation](https://docs.getdbt.com/) +- [dbt Best Practices](https://docs.getdbt.com/guides/best-practices) +- [dbt-utils Package](https://hub.getdbt.com/dbt-labs/dbt_utils/latest/) +- [dbt Discourse](https://discourse.getdbt.com/) diff --git a/plugins/antigravity-bundle-data-engineering/skills/embedding-strategies/SKILL.md b/plugins/antigravity-bundle-data-engineering/skills/embedding-strategies/SKILL.md new file mode 100644 index 00000000..f371de3f --- /dev/null +++ b/plugins/antigravity-bundle-data-engineering/skills/embedding-strategies/SKILL.md @@ -0,0 +1,494 @@ +--- +name: embedding-strategies +description: "Guide to selecting and optimizing embedding models for vector search applications." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Embedding Strategies + +Guide to selecting and optimizing embedding models for vector search applications. + +## Do not use this skill when + +- The task is unrelated to embedding strategies +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Use this skill when + +- Choosing embedding models for RAG +- Optimizing chunking strategies +- Fine-tuning embeddings for domains +- Comparing embedding model performance +- Reducing embedding dimensions +- Handling multilingual content + +## Core Concepts + +### 1. Embedding Model Comparison + +| Model | Dimensions | Max Tokens | Best For | +|-------|------------|------------|----------| +| **text-embedding-3-large** | 3072 | 8191 | High accuracy | +| **text-embedding-3-small** | 1536 | 8191 | Cost-effective | +| **voyage-2** | 1024 | 4000 | Code, legal | +| **bge-large-en-v1.5** | 1024 | 512 | Open source | +| **all-MiniLM-L6-v2** | 384 | 256 | Fast, lightweight | +| **multilingual-e5-large** | 1024 | 512 | Multi-language | + +### 2. Embedding Pipeline + +``` +Document โ†’ Chunking โ†’ Preprocessing โ†’ Embedding Model โ†’ Vector + โ†“ + [Overlap, Size] [Clean, Normalize] [API/Local] +``` + +## Templates + +### Template 1: OpenAI Embeddings + +```python +from openai import OpenAI +from typing import List +import numpy as np + +client = OpenAI() + +def get_embeddings( + texts: List[str], + model: str = "text-embedding-3-small", + dimensions: int = None +) -> List[List[float]]: + """Get embeddings from OpenAI.""" + # Handle batching for large lists + batch_size = 100 + all_embeddings = [] + + for i in range(0, len(texts), batch_size): + batch = texts[i:i + batch_size] + + kwargs = {"input": batch, "model": model} + if dimensions: + kwargs["dimensions"] = dimensions + + response = client.embeddings.create(**kwargs) + embeddings = [item.embedding for item in response.data] + all_embeddings.extend(embeddings) + + return all_embeddings + + +def get_embedding(text: str, **kwargs) -> List[float]: + """Get single embedding.""" + return get_embeddings([text], **kwargs)[0] + + +# Dimension reduction with OpenAI +def get_reduced_embedding(text: str, dimensions: int = 512) -> List[float]: + """Get embedding with reduced dimensions (Matryoshka).""" + return get_embedding( + text, + model="text-embedding-3-small", + dimensions=dimensions + ) +``` + +### Template 2: Local Embeddings with Sentence Transformers + +```python +from sentence_transformers import SentenceTransformer +from typing import List, Optional +import numpy as np + +class LocalEmbedder: + """Local embedding with sentence-transformers.""" + + def __init__( + self, + model_name: str = "BAAI/bge-large-en-v1.5", + device: str = "cuda" + ): + self.model = SentenceTransformer(model_name, device=device) + + def embed( + self, + texts: List[str], + normalize: bool = True, + show_progress: bool = False + ) -> np.ndarray: + """Embed texts with optional normalization.""" + embeddings = self.model.encode( + texts, + normalize_embeddings=normalize, + show_progress_bar=show_progress, + convert_to_numpy=True + ) + return embeddings + + def embed_query(self, query: str) -> np.ndarray: + """Embed a query with BGE-style prefix.""" + # BGE models benefit from query prefix + if "bge" in self.model.get_sentence_embedding_dimension(): + query = f"Represent this sentence for searching relevant passages: {query}" + return self.embed([query])[0] + + def embed_documents(self, documents: List[str]) -> np.ndarray: + """Embed documents for indexing.""" + return self.embed(documents) + + +# E5 model with instructions +class E5Embedder: + def __init__(self, model_name: str = "intfloat/multilingual-e5-large"): + self.model = SentenceTransformer(model_name) + + def embed_query(self, query: str) -> np.ndarray: + return self.model.encode(f"query: {query}") + + def embed_document(self, document: str) -> np.ndarray: + return self.model.encode(f"passage: {document}") +``` + +### Template 3: Chunking Strategies + +```python +from typing import List, Tuple +import re + +def chunk_by_tokens( + text: str, + chunk_size: int = 512, + chunk_overlap: int = 50, + tokenizer=None +) -> List[str]: + """Chunk text by token count.""" + import tiktoken + tokenizer = tokenizer or tiktoken.get_encoding("cl100k_base") + + tokens = tokenizer.encode(text) + chunks = [] + + start = 0 + while start < len(tokens): + end = start + chunk_size + chunk_tokens = tokens[start:end] + chunk_text = tokenizer.decode(chunk_tokens) + chunks.append(chunk_text) + start = end - chunk_overlap + + return chunks + + +def chunk_by_sentences( + text: str, + max_chunk_size: int = 1000, + min_chunk_size: int = 100 +) -> List[str]: + """Chunk text by sentences, respecting size limits.""" + import nltk + sentences = nltk.sent_tokenize(text) + + chunks = [] + current_chunk = [] + current_size = 0 + + for sentence in sentences: + sentence_size = len(sentence) + + if current_size + sentence_size > max_chunk_size and current_chunk: + chunks.append(" ".join(current_chunk)) + current_chunk = [] + current_size = 0 + + current_chunk.append(sentence) + current_size += sentence_size + + if current_chunk: + chunks.append(" ".join(current_chunk)) + + return chunks + + +def chunk_by_semantic_sections( + text: str, + headers_pattern: str = r'^#{1,3}\s+.+$' +) -> List[Tuple[str, str]]: + """Chunk markdown by headers, preserving hierarchy.""" + lines = text.split('\n') + chunks = [] + current_header = "" + current_content = [] + + for line in lines: + if re.match(headers_pattern, line, re.MULTILINE): + if current_content: + chunks.append((current_header, '\n'.join(current_content))) + current_header = line + current_content = [] + else: + current_content.append(line) + + if current_content: + chunks.append((current_header, '\n'.join(current_content))) + + return chunks + + +def recursive_character_splitter( + text: str, + chunk_size: int = 1000, + chunk_overlap: int = 200, + separators: List[str] = None +) -> List[str]: + """LangChain-style recursive splitter.""" + separators = separators or ["\n\n", "\n", ". ", " ", ""] + + def split_text(text: str, separators: List[str]) -> List[str]: + if not text: + return [] + + separator = separators[0] + remaining_separators = separators[1:] + + if separator == "": + # Character-level split + return [text[i:i+chunk_size] for i in range(0, len(text), chunk_size - chunk_overlap)] + + splits = text.split(separator) + chunks = [] + current_chunk = [] + current_length = 0 + + for split in splits: + split_length = len(split) + len(separator) + + if current_length + split_length > chunk_size and current_chunk: + chunk_text = separator.join(current_chunk) + + # Recursively split if still too large + if len(chunk_text) > chunk_size and remaining_separators: + chunks.extend(split_text(chunk_text, remaining_separators)) + else: + chunks.append(chunk_text) + + # Start new chunk with overlap + overlap_splits = [] + overlap_length = 0 + for s in reversed(current_chunk): + if overlap_length + len(s) <= chunk_overlap: + overlap_splits.insert(0, s) + overlap_length += len(s) + else: + break + current_chunk = overlap_splits + current_length = overlap_length + + current_chunk.append(split) + current_length += split_length + + if current_chunk: + chunks.append(separator.join(current_chunk)) + + return chunks + + return split_text(text, separators) +``` + +### Template 4: Domain-Specific Embedding Pipeline + +```python +class DomainEmbeddingPipeline: + """Pipeline for domain-specific embeddings.""" + + def __init__( + self, + embedding_model: str = "text-embedding-3-small", + chunk_size: int = 512, + chunk_overlap: int = 50, + preprocessing_fn=None + ): + self.embedding_model = embedding_model + self.chunk_size = chunk_size + self.chunk_overlap = chunk_overlap + self.preprocess = preprocessing_fn or self._default_preprocess + + def _default_preprocess(self, text: str) -> str: + """Default preprocessing.""" + # Remove excessive whitespace + text = re.sub(r'\s+', ' ', text) + # Remove special characters + text = re.sub(r'[^\w\s.,!?-]', '', text) + return text.strip() + + async def process_documents( + self, + documents: List[dict], + id_field: str = "id", + content_field: str = "content", + metadata_fields: List[str] = None + ) -> List[dict]: + """Process documents for vector storage.""" + processed = [] + + for doc in documents: + content = doc[content_field] + doc_id = doc[id_field] + + # Preprocess + cleaned = self.preprocess(content) + + # Chunk + chunks = chunk_by_tokens( + cleaned, + self.chunk_size, + self.chunk_overlap + ) + + # Create embeddings + embeddings = get_embeddings(chunks, self.embedding_model) + + # Create records + for i, (chunk, embedding) in enumerate(zip(chunks, embeddings)): + record = { + "id": f"{doc_id}_chunk_{i}", + "document_id": doc_id, + "chunk_index": i, + "text": chunk, + "embedding": embedding + } + + # Add metadata + if metadata_fields: + for field in metadata_fields: + if field in doc: + record[field] = doc[field] + + processed.append(record) + + return processed + + +# Code-specific pipeline +class CodeEmbeddingPipeline: + """Specialized pipeline for code embeddings.""" + + def __init__(self, model: str = "voyage-code-2"): + self.model = model + + def chunk_code(self, code: str, language: str) -> List[dict]: + """Chunk code by functions/classes.""" + import tree_sitter + + # Parse with tree-sitter + # Extract functions, classes, methods + # Return chunks with context + pass + + def embed_with_context(self, chunk: str, context: str) -> List[float]: + """Embed code with surrounding context.""" + combined = f"Context: {context}\n\nCode:\n{chunk}" + return get_embedding(combined, model=self.model) +``` + +### Template 5: Embedding Quality Evaluation + +```python +import numpy as np +from typing import List, Tuple + +def evaluate_retrieval_quality( + queries: List[str], + relevant_docs: List[List[str]], # List of relevant doc IDs per query + retrieved_docs: List[List[str]], # List of retrieved doc IDs per query + k: int = 10 +) -> dict: + """Evaluate embedding quality for retrieval.""" + + def precision_at_k(relevant: set, retrieved: List[str], k: int) -> float: + retrieved_k = retrieved[:k] + relevant_retrieved = len(set(retrieved_k) & relevant) + return relevant_retrieved / k + + def recall_at_k(relevant: set, retrieved: List[str], k: int) -> float: + retrieved_k = retrieved[:k] + relevant_retrieved = len(set(retrieved_k) & relevant) + return relevant_retrieved / len(relevant) if relevant else 0 + + def mrr(relevant: set, retrieved: List[str]) -> float: + for i, doc in enumerate(retrieved): + if doc in relevant: + return 1 / (i + 1) + return 0 + + def ndcg_at_k(relevant: set, retrieved: List[str], k: int) -> float: + dcg = sum( + 1 / np.log2(i + 2) if doc in relevant else 0 + for i, doc in enumerate(retrieved[:k]) + ) + ideal_dcg = sum(1 / np.log2(i + 2) for i in range(min(len(relevant), k))) + return dcg / ideal_dcg if ideal_dcg > 0 else 0 + + metrics = { + f"precision@{k}": [], + f"recall@{k}": [], + "mrr": [], + f"ndcg@{k}": [] + } + + for relevant, retrieved in zip(relevant_docs, retrieved_docs): + relevant_set = set(relevant) + metrics[f"precision@{k}"].append(precision_at_k(relevant_set, retrieved, k)) + metrics[f"recall@{k}"].append(recall_at_k(relevant_set, retrieved, k)) + metrics["mrr"].append(mrr(relevant_set, retrieved)) + metrics[f"ndcg@{k}"].append(ndcg_at_k(relevant_set, retrieved, k)) + + return {name: np.mean(values) for name, values in metrics.items()} + + +def compute_embedding_similarity( + embeddings1: np.ndarray, + embeddings2: np.ndarray, + metric: str = "cosine" +) -> np.ndarray: + """Compute similarity matrix between embedding sets.""" + if metric == "cosine": + # Normalize + norm1 = embeddings1 / np.linalg.norm(embeddings1, axis=1, keepdims=True) + norm2 = embeddings2 / np.linalg.norm(embeddings2, axis=1, keepdims=True) + return norm1 @ norm2.T + elif metric == "euclidean": + from scipy.spatial.distance import cdist + return -cdist(embeddings1, embeddings2, metric='euclidean') + elif metric == "dot": + return embeddings1 @ embeddings2.T +``` + +## Best Practices + +### Do's +- **Match model to use case** - Code vs prose vs multilingual +- **Chunk thoughtfully** - Preserve semantic boundaries +- **Normalize embeddings** - For cosine similarity +- **Batch requests** - More efficient than one-by-one +- **Cache embeddings** - Avoid recomputing + +### Don'ts +- **Don't ignore token limits** - Truncation loses info +- **Don't mix embedding models** - Incompatible spaces +- **Don't skip preprocessing** - Garbage in, garbage out +- **Don't over-chunk** - Lose context + +## Resources + +- [OpenAI Embeddings](https://platform.openai.com/docs/guides/embeddings) +- [Sentence Transformers](https://www.sbert.net/) +- [MTEB Benchmark](https://huggingface.co/spaces/mteb/leaderboard) diff --git a/plugins/antigravity-bundle-data-engineering/skills/vector-database-engineer/SKILL.md b/plugins/antigravity-bundle-data-engineering/skills/vector-database-engineer/SKILL.md new file mode 100644 index 00000000..5665d741 --- /dev/null +++ b/plugins/antigravity-bundle-data-engineering/skills/vector-database-engineer/SKILL.md @@ -0,0 +1,63 @@ +--- +name: vector-database-engineer +description: "Expert in vector databases, embedding strategies, and semantic search implementation. Masters Pinecone, Weaviate, Qdrant, Milvus, and pgvector for RAG applications, recommendation systems, and similar" +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Vector Database Engineer + +Expert in vector databases, embedding strategies, and semantic search implementation. Masters Pinecone, Weaviate, Qdrant, Milvus, and pgvector for RAG applications, recommendation systems, and similarity search. Use PROACTIVELY for vector search implementation, embedding optimization, or semantic retrieval systems. + +## Do not use this skill when + +- The task is unrelated to vector database engineer +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Capabilities + +- Vector database selection and architecture +- Embedding model selection and optimization +- Index configuration (HNSW, IVF, PQ) +- Hybrid search (vector + keyword) implementation +- Chunking strategies for documents +- Metadata filtering and pre/post-filtering +- Performance tuning and scaling + +## Use this skill when + +- Building RAG (Retrieval Augmented Generation) systems +- Implementing semantic search over documents +- Creating recommendation engines +- Building image/audio similarity search +- Optimizing vector search latency and recall +- Scaling vector operations to millions of vectors + +## Workflow + +1. Analyze data characteristics and query patterns +2. Select appropriate embedding model +3. Design chunking and preprocessing pipeline +4. Choose vector database and index type +5. Configure metadata schema for filtering +6. Implement hybrid search if needed +7. Optimize for latency/recall tradeoffs +8. Set up monitoring and reindexing strategies + +## Best Practices + +- Choose embedding dimensions based on use case (384-1536) +- Implement proper chunking with overlap +- Use metadata filtering to reduce search space +- Monitor embedding drift over time +- Plan for index rebuilding +- Cache frequent queries +- Test recall vs latency tradeoffs diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/.codex-plugin/plugin.json b/plugins/antigravity-bundle-ddd-evented-architecture/.codex-plugin/plugin.json new file mode 100644 index 00000000..bd2c439f --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-ddd-evented-architecture", + "version": "8.10.0", + "description": "Install the \"DDD & Evented Architecture\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "ddd-evented-architecture", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "DDD & Evented Architecture", + "shortDescription": "Specialized Packs ยท 8 curated skills", + "longDescription": "For teams modeling complex domains and evolving toward evented systems. Covers Domain Driven Design, DDD Strategic Design, and 6 more skills.", + "developerName": "sickn33 and contributors", + "category": "Specialized Packs", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/cqrs-implementation/SKILL.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/cqrs-implementation/SKILL.md new file mode 100644 index 00000000..528fd67c --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/cqrs-implementation/SKILL.md @@ -0,0 +1,38 @@ +--- +name: cqrs-implementation +description: "Implement Command Query Responsibility Segregation for scalable architectures. Use when separating read and write models, optimizing query performance, or building event-sourced systems." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# CQRS Implementation + +Comprehensive guide to implementing CQRS (Command Query Responsibility Segregation) patterns. + +## Use this skill when + +- Separating read and write concerns +- Scaling reads independently from writes +- Building event-sourced systems +- Optimizing complex query scenarios +- Different read/write data models are needed +- High-performance reporting is required + +## Do not use this skill when + +- The domain is simple and CRUD is sufficient +- You cannot operate separate read/write models +- Strong immediate consistency is required everywhere + +## Instructions + +- Identify read/write workloads and consistency needs. +- Define command and query models with clear boundaries. +- Implement read model projections and synchronization. +- Validate performance, recovery, and failure modes. +- If detailed patterns are required, open `resources/implementation-playbook.md`. + +## Resources + +- `resources/implementation-playbook.md` for detailed CQRS patterns and templates. diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/cqrs-implementation/resources/implementation-playbook.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/cqrs-implementation/resources/implementation-playbook.md new file mode 100644 index 00000000..9072c929 --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/cqrs-implementation/resources/implementation-playbook.md @@ -0,0 +1,540 @@ +# CQRS Implementation Playbook + +This file contains detailed patterns, checklists, and code samples referenced by the skill. + +## Core Concepts + +### 1. CQRS Architecture + +``` + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Client โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ + โ–ผ โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Commands โ”‚ โ”‚ Queries โ”‚ + โ”‚ API โ”‚ โ”‚ API โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ–ผ โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Command โ”‚ โ”‚ Query โ”‚ + โ”‚ Handlers โ”‚ โ”‚ Handlers โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ–ผ โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Write โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ Read โ”‚ + โ”‚ Model โ”‚ Events โ”‚ Model โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2. Key Components + +| Component | Responsibility | +| ------------------- | ------------------------------- | +| **Command** | Intent to change state | +| **Command Handler** | Validates and executes commands | +| **Event** | Record of state change | +| **Query** | Request for data | +| **Query Handler** | Retrieves data from read model | +| **Projector** | Updates read model from events | + +## Templates + +### Template 1: Command Infrastructure + +```python +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import TypeVar, Generic, Dict, Any, Type +from datetime import datetime +import uuid + +# Command base +@dataclass +class Command: + command_id: str = None + timestamp: datetime = None + + def __post_init__(self): + self.command_id = self.command_id or str(uuid.uuid4()) + self.timestamp = self.timestamp or datetime.utcnow() + + +# Concrete commands +@dataclass +class CreateOrder(Command): + customer_id: str + items: list + shipping_address: dict + + +@dataclass +class AddOrderItem(Command): + order_id: str + product_id: str + quantity: int + price: float + + +@dataclass +class CancelOrder(Command): + order_id: str + reason: str + + +# Command handler base +T = TypeVar('T', bound=Command) + +class CommandHandler(ABC, Generic[T]): + @abstractmethod + async def handle(self, command: T) -> Any: + pass + + +# Command bus +class CommandBus: + def __init__(self): + self._handlers: Dict[Type[Command], CommandHandler] = {} + + def register(self, command_type: Type[Command], handler: CommandHandler): + self._handlers[command_type] = handler + + async def dispatch(self, command: Command) -> Any: + handler = self._handlers.get(type(command)) + if not handler: + raise ValueError(f"No handler for {type(command).__name__}") + return await handler.handle(command) + + +# Command handler implementation +class CreateOrderHandler(CommandHandler[CreateOrder]): + def __init__(self, order_repository, event_store): + self.order_repository = order_repository + self.event_store = event_store + + async def handle(self, command: CreateOrder) -> str: + # Validate + if not command.items: + raise ValueError("Order must have at least one item") + + # Create aggregate + order = Order.create( + customer_id=command.customer_id, + items=command.items, + shipping_address=command.shipping_address + ) + + # Persist events + await self.event_store.append_events( + stream_id=f"Order-{order.id}", + stream_type="Order", + events=order.uncommitted_events + ) + + return order.id +``` + +### Template 2: Query Infrastructure + +```python +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import TypeVar, Generic, List, Optional + +# Query base +@dataclass +class Query: + pass + + +# Concrete queries +@dataclass +class GetOrderById(Query): + order_id: str + + +@dataclass +class GetCustomerOrders(Query): + customer_id: str + status: Optional[str] = None + page: int = 1 + page_size: int = 20 + + +@dataclass +class SearchOrders(Query): + query: str + filters: dict = None + sort_by: str = "created_at" + sort_order: str = "desc" + + +# Query result types +@dataclass +class OrderView: + order_id: str + customer_id: str + status: str + total_amount: float + item_count: int + created_at: datetime + shipped_at: Optional[datetime] = None + + +@dataclass +class PaginatedResult(Generic[T]): + items: List[T] + total: int + page: int + page_size: int + + @property + def total_pages(self) -> int: + return (self.total + self.page_size - 1) // self.page_size + + +# Query handler base +T = TypeVar('T', bound=Query) +R = TypeVar('R') + +class QueryHandler(ABC, Generic[T, R]): + @abstractmethod + async def handle(self, query: T) -> R: + pass + + +# Query bus +class QueryBus: + def __init__(self): + self._handlers: Dict[Type[Query], QueryHandler] = {} + + def register(self, query_type: Type[Query], handler: QueryHandler): + self._handlers[query_type] = handler + + async def dispatch(self, query: Query) -> Any: + handler = self._handlers.get(type(query)) + if not handler: + raise ValueError(f"No handler for {type(query).__name__}") + return await handler.handle(query) + + +# Query handler implementation +class GetOrderByIdHandler(QueryHandler[GetOrderById, Optional[OrderView]]): + def __init__(self, read_db): + self.read_db = read_db + + async def handle(self, query: GetOrderById) -> Optional[OrderView]: + async with self.read_db.acquire() as conn: + row = await conn.fetchrow( + """ + SELECT order_id, customer_id, status, total_amount, + item_count, created_at, shipped_at + FROM order_views + WHERE order_id = $1 + """, + query.order_id + ) + if row: + return OrderView(**dict(row)) + return None + + +class GetCustomerOrdersHandler(QueryHandler[GetCustomerOrders, PaginatedResult[OrderView]]): + def __init__(self, read_db): + self.read_db = read_db + + async def handle(self, query: GetCustomerOrders) -> PaginatedResult[OrderView]: + async with self.read_db.acquire() as conn: + # Build query with optional status filter + where_clause = "customer_id = $1" + params = [query.customer_id] + + if query.status: + where_clause += " AND status = $2" + params.append(query.status) + + # Get total count + total = await conn.fetchval( + f"SELECT COUNT(*) FROM order_views WHERE {where_clause}", + *params + ) + + # Get paginated results + offset = (query.page - 1) * query.page_size + rows = await conn.fetch( + f""" + SELECT order_id, customer_id, status, total_amount, + item_count, created_at, shipped_at + FROM order_views + WHERE {where_clause} + ORDER BY created_at DESC + LIMIT ${len(params) + 1} OFFSET ${len(params) + 2} + """, + *params, query.page_size, offset + ) + + return PaginatedResult( + items=[OrderView(**dict(row)) for row in rows], + total=total, + page=query.page, + page_size=query.page_size + ) +``` + +### Template 3: FastAPI CQRS Application + +```python +from fastapi import FastAPI, HTTPException, Depends +from pydantic import BaseModel +from typing import List, Optional + +app = FastAPI() + +# Request/Response models +class CreateOrderRequest(BaseModel): + customer_id: str + items: List[dict] + shipping_address: dict + + +class OrderResponse(BaseModel): + order_id: str + customer_id: str + status: str + total_amount: float + item_count: int + created_at: datetime + + +# Dependency injection +def get_command_bus() -> CommandBus: + return app.state.command_bus + + +def get_query_bus() -> QueryBus: + return app.state.query_bus + + +# Command endpoints (POST, PUT, DELETE) +@app.post("/orders", response_model=dict) +async def create_order( + request: CreateOrderRequest, + command_bus: CommandBus = Depends(get_command_bus) +): + command = CreateOrder( + customer_id=request.customer_id, + items=request.items, + shipping_address=request.shipping_address + ) + order_id = await command_bus.dispatch(command) + return {"order_id": order_id} + + +@app.post("/orders/{order_id}/items") +async def add_item( + order_id: str, + product_id: str, + quantity: int, + price: float, + command_bus: CommandBus = Depends(get_command_bus) +): + command = AddOrderItem( + order_id=order_id, + product_id=product_id, + quantity=quantity, + price=price + ) + await command_bus.dispatch(command) + return {"status": "item_added"} + + +@app.delete("/orders/{order_id}") +async def cancel_order( + order_id: str, + reason: str, + command_bus: CommandBus = Depends(get_command_bus) +): + command = CancelOrder(order_id=order_id, reason=reason) + await command_bus.dispatch(command) + return {"status": "cancelled"} + + +# Query endpoints (GET) +@app.get("/orders/{order_id}", response_model=OrderResponse) +async def get_order( + order_id: str, + query_bus: QueryBus = Depends(get_query_bus) +): + query = GetOrderById(order_id=order_id) + result = await query_bus.dispatch(query) + if not result: + raise HTTPException(status_code=404, detail="Order not found") + return result + + +@app.get("/customers/{customer_id}/orders") +async def get_customer_orders( + customer_id: str, + status: Optional[str] = None, + page: int = 1, + page_size: int = 20, + query_bus: QueryBus = Depends(get_query_bus) +): + query = GetCustomerOrders( + customer_id=customer_id, + status=status, + page=page, + page_size=page_size + ) + return await query_bus.dispatch(query) + + +@app.get("/orders/search") +async def search_orders( + q: str, + sort_by: str = "created_at", + query_bus: QueryBus = Depends(get_query_bus) +): + query = SearchOrders(query=q, sort_by=sort_by) + return await query_bus.dispatch(query) +``` + +### Template 4: Read Model Synchronization + +```python +class ReadModelSynchronizer: + """Keeps read models in sync with events.""" + + def __init__(self, event_store, read_db, projections: List[Projection]): + self.event_store = event_store + self.read_db = read_db + self.projections = {p.name: p for p in projections} + + async def run(self): + """Continuously sync read models.""" + while True: + for name, projection in self.projections.items(): + await self._sync_projection(projection) + await asyncio.sleep(0.1) + + async def _sync_projection(self, projection: Projection): + checkpoint = await self._get_checkpoint(projection.name) + + events = await self.event_store.read_all( + from_position=checkpoint, + limit=100 + ) + + for event in events: + if event.event_type in projection.handles(): + try: + await projection.apply(event) + except Exception as e: + # Log error, possibly retry or skip + logger.error(f"Projection error: {e}") + continue + + await self._save_checkpoint(projection.name, event.global_position) + + async def rebuild_projection(self, projection_name: str): + """Rebuild a projection from scratch.""" + projection = self.projections[projection_name] + + # Clear existing data + await projection.clear() + + # Reset checkpoint + await self._save_checkpoint(projection_name, 0) + + # Rebuild + while True: + checkpoint = await self._get_checkpoint(projection_name) + events = await self.event_store.read_all(checkpoint, 1000) + + if not events: + break + + for event in events: + if event.event_type in projection.handles(): + await projection.apply(event) + + await self._save_checkpoint( + projection_name, + events[-1].global_position + ) +``` + +### Template 5: Eventual Consistency Handling + +```python +class ConsistentQueryHandler: + """Query handler that can wait for consistency.""" + + def __init__(self, read_db, event_store): + self.read_db = read_db + self.event_store = event_store + + async def query_after_command( + self, + query: Query, + expected_version: int, + stream_id: str, + timeout: float = 5.0 + ): + """ + Execute query, ensuring read model is at expected version. + Used for read-your-writes consistency. + """ + start_time = time.time() + + while time.time() - start_time < timeout: + # Check if read model is caught up + projection_version = await self._get_projection_version(stream_id) + + if projection_version >= expected_version: + return await self.execute_query(query) + + # Wait a bit and retry + await asyncio.sleep(0.1) + + # Timeout - return stale data with warning + return { + "data": await self.execute_query(query), + "_warning": "Data may be stale" + } + + async def _get_projection_version(self, stream_id: str) -> int: + """Get the last processed event version for a stream.""" + async with self.read_db.acquire() as conn: + return await conn.fetchval( + "SELECT last_event_version FROM projection_state WHERE stream_id = $1", + stream_id + ) or 0 +``` + +## Best Practices + +### Do's + +- **Separate command and query models** - Different needs +- **Use eventual consistency** - Accept propagation delay +- **Validate in command handlers** - Before state change +- **Denormalize read models** - Optimize for queries +- **Version your events** - For schema evolution + +### Don'ts + +- **Don't query in commands** - Use only for writes +- **Don't couple read/write schemas** - Independent evolution +- **Don't over-engineer** - Start simple +- **Don't ignore consistency SLAs** - Define acceptable lag + +## Resources + +- [CQRS Pattern](https://martinfowler.com/bliki/CQRS.html) +- [Microsoft CQRS Guidance](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs) diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-context-mapping/SKILL.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-context-mapping/SKILL.md new file mode 100644 index 00000000..6886ba6f --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-context-mapping/SKILL.md @@ -0,0 +1,52 @@ +--- +name: ddd-context-mapping +description: "Map relationships between bounded contexts and define integration contracts using DDD context mapping patterns." +risk: safe +source: self +tags: "[ddd, context-map, anti-corruption-layer, integration]" +date_added: "2026-02-27" +--- + +# DDD Context Mapping + +## Use this skill when + +- Defining integration patterns between bounded contexts. +- Preventing domain leakage across service boundaries. +- Planning anti-corruption layers during migration. +- Clarifying upstream and downstream ownership for contracts. + +## Do not use this skill when + +- You have a single-context system with no integrations. +- You only need internal class design. +- You are selecting cloud infrastructure tooling. + +## Instructions + +1. List all context pairs and dependency direction. +2. Choose relationship patterns per pair. +3. Define translation rules and ownership boundaries. +4. Add failure modes, fallback behavior, and versioning policy. + +If detailed mapping structures are needed, open `references/context-map-patterns.md`. + +## Output requirements + +- Relationship map for all context pairs +- Contract ownership matrix +- Translation and anti-corruption decisions +- Known coupling risks and mitigation plan + +## Examples + +```text +Use @ddd-context-mapping to define how Checkout integrates with Billing, +Inventory, and Fraud contexts, including ACL and contract ownership. +``` + +## Limitations + +- This skill does not replace API-level schema design. +- It does not guarantee organizational alignment by itself. +- It should be revisited when team ownership changes. diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-context-mapping/references/context-map-patterns.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-context-mapping/references/context-map-patterns.md new file mode 100644 index 00000000..c9019b2a --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-context-mapping/references/context-map-patterns.md @@ -0,0 +1,25 @@ +# Context Mapping Patterns + +## Common relationship patterns + +- Partnership +- Shared Kernel +- Customer-Supplier +- Conformist +- Anti-Corruption Layer +- Open Host Service +- Published Language + +## Mapping template + +| Upstream context | Downstream context | Pattern | Contract owner | Translation needed | +| --- | --- | --- | --- | --- | +| Billing | Checkout | Customer-Supplier | Billing | Yes | +| Identity | Checkout | Conformist | Identity | No | + +## ACL checklist + +- Define canonical domain model for receiving context. +- Translate external terms into local ubiquitous language. +- Keep ACL code at boundary, not inside domain core. +- Add contract tests for mapped behavior. diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-strategic-design/SKILL.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-strategic-design/SKILL.md new file mode 100644 index 00000000..c4666d6c --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-strategic-design/SKILL.md @@ -0,0 +1,52 @@ +--- +name: ddd-strategic-design +description: "Design DDD strategic artifacts including subdomains, bounded contexts, and ubiquitous language for complex business domains." +risk: safe +source: self +tags: "[ddd, strategic-design, bounded-context, ubiquitous-language]" +date_added: "2026-02-27" +--- + +# DDD Strategic Design + +## Use this skill when + +- Defining core, supporting, and generic subdomains. +- Splitting a monolith or service landscape by domain boundaries. +- Aligning teams and ownership with bounded contexts. +- Building a shared ubiquitous language with domain experts. + +## Do not use this skill when + +- The domain model is stable and already well bounded. +- You need tactical code patterns only. +- The task is purely infrastructure or UI oriented. + +## Instructions + +1. Extract domain capabilities and classify subdomains. +2. Define bounded contexts around consistency and ownership. +3. Establish a ubiquitous language glossary and anti-terms. +4. Capture context boundaries in ADRs before implementation. + +If detailed templates are needed, open `references/strategic-design-template.md`. + +## Required artifacts + +- Subdomain classification table +- Bounded context catalog +- Glossary with canonical terms +- Boundary decisions with rationale + +## Examples + +```text +Use @ddd-strategic-design to map our commerce domain into bounded contexts, +classify subdomains, and propose team ownership. +``` + +## Limitations + +- This skill does not produce executable code. +- It cannot infer business truth without stakeholder input. +- It should be followed by tactical design before implementation. diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-strategic-design/references/strategic-design-template.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-strategic-design/references/strategic-design-template.md new file mode 100644 index 00000000..d98c262d --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-strategic-design/references/strategic-design-template.md @@ -0,0 +1,22 @@ +# Strategic Design Template + +## Subdomain classification + +| Capability | Subdomain type | Why | Owner team | +| --- | --- | --- | --- | +| Pricing | Core | Differentiates business value | Commerce | +| Identity | Supporting | Needed but not differentiating | Platform | + +## Bounded context catalog + +| Context | Responsibility | Upstream dependencies | Downstream consumers | +| --- | --- | --- | --- | +| Catalog | Product data lifecycle | Supplier feed | Checkout, Search | +| Checkout | Order placement and payment authorization | Catalog, Pricing | Fulfillment, Billing | + +## Ubiquitous language + +| Term | Definition | Context | +| --- | --- | --- | +| Order | Confirmed purchase request | Checkout | +| Reservation | Temporary inventory hold | Fulfillment | diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-tactical-patterns/SKILL.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-tactical-patterns/SKILL.md new file mode 100644 index 00000000..e4a3a690 --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-tactical-patterns/SKILL.md @@ -0,0 +1,53 @@ +--- +name: ddd-tactical-patterns +description: "Apply DDD tactical patterns in code using entities, value objects, aggregates, repositories, and domain events with explicit invariants." +risk: safe +source: self +tags: "[ddd, tactical, aggregates, value-objects, domain-events]" +date_added: "2026-02-27" +--- + +# DDD Tactical Patterns + +## Use this skill when + +- Translating domain rules into code structures. +- Designing aggregate boundaries and invariants. +- Refactoring an anemic model into behavior-rich domain objects. +- Defining repository contracts and domain event boundaries. + +## Do not use this skill when + +- You are still defining strategic boundaries. +- The task is only API documentation or UI layout. +- Full DDD complexity is not justified. + +## Instructions + +1. Identify invariants first and design aggregates around them. +2. Model immutable value objects for validated concepts. +3. Keep domain behavior in domain objects, not controllers. +4. Emit domain events for meaningful state transitions. +5. Keep repositories at aggregate root boundaries. + +If detailed checklists are needed, open `references/tactical-checklist.md`. + +## Example + +```typescript +class Order { + private status: "draft" | "submitted" = "draft"; + + submit(itemsCount: number): void { + if (itemsCount === 0) throw new Error("Order cannot be submitted empty"); + if (this.status !== "draft") throw new Error("Order already submitted"); + this.status = "submitted"; + } +} +``` + +## Limitations + +- This skill does not define deployment architecture. +- It does not choose databases or transport protocols. +- It should be paired with testing patterns for invariant coverage. diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-tactical-patterns/references/tactical-checklist.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-tactical-patterns/references/tactical-checklist.md new file mode 100644 index 00000000..fa1e2cd1 --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/ddd-tactical-patterns/references/tactical-checklist.md @@ -0,0 +1,25 @@ +# Tactical Pattern Checklist + +## Aggregate design + +- One aggregate root per transaction boundary +- Invariants enforced inside aggregate methods +- Avoid cross-aggregate synchronous consistency rules + +## Value objects + +- Immutable by default +- Validation at construction +- Equality by value, not identity + +## Repositories + +- Persist and load aggregate roots only +- Expose domain-friendly query methods +- Avoid leaking ORM entities into domain layer + +## Domain events + +- Past-tense event names (for example, `OrderSubmitted`) +- Include minimal, stable event payloads +- Version event schema before breaking changes diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/domain-driven-design/SKILL.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/domain-driven-design/SKILL.md new file mode 100644 index 00000000..a78cce62 --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/domain-driven-design/SKILL.md @@ -0,0 +1,74 @@ +--- +name: domain-driven-design +description: "Plan and route Domain-Driven Design work from strategic modeling to tactical implementation and evented architecture patterns." +risk: safe +source: self +tags: "[ddd, domain, bounded-context, architecture]" +date_added: "2026-02-27" +--- + +# Domain-Driven Design + +## Use this skill when + +- You need to model a complex business domain with explicit boundaries. +- You want to decide whether full DDD is worth the added complexity. +- You need to connect strategic design decisions to implementation patterns. +- You are planning CQRS, event sourcing, sagas, or projections from domain needs. + +## Do not use this skill when + +- The problem is simple CRUD with low business complexity. +- You only need localized bug fixes. +- There is no access to domain knowledge and no proxy product expert. + +## Instructions + +1. Run a viability check before committing to full DDD. +2. Produce strategic artifacts first: subdomains, bounded contexts, language glossary. +3. Route to specialized skills based on current task. +4. Define success criteria and evidence for each stage. + +### Viability check + +Use full DDD only when at least two of these are true: + +- Business rules are complex or fast-changing. +- Multiple teams are causing model collisions. +- Integration contracts are unstable. +- Auditability and explicit invariants are critical. + +### Routing map + +- Strategic model and boundaries: `@ddd-strategic-design` +- Cross-context integrations and translation: `@ddd-context-mapping` +- Tactical code modeling: `@ddd-tactical-patterns` +- Read/write separation: `@cqrs-implementation` +- Event history as source of truth: `@event-sourcing-architect` and `@event-store-design` +- Long-running workflows: `@saga-orchestration` +- Read models: `@projection-patterns` +- Decision log: `@architecture-decision-records` + +If templates are needed, open `references/ddd-deliverables.md`. + +## Output requirements + +Always return: + +- Scope and assumptions +- Current stage (strategic, tactical, or evented) +- Explicit artifacts produced +- Open risks and next step recommendation + +## Examples + +```text +Use @domain-driven-design to assess if this billing platform should adopt full DDD. +Then route to the right next skill and list artifacts we must produce this week. +``` + +## Limitations + +- This skill does not replace direct workshops with domain experts. +- It does not provide framework-specific code generation. +- It should not be used as a justification to over-engineer simple systems. diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/domain-driven-design/references/ddd-deliverables.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/domain-driven-design/references/ddd-deliverables.md new file mode 100644 index 00000000..49eeeae1 --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/domain-driven-design/references/ddd-deliverables.md @@ -0,0 +1,24 @@ +# DDD Deliverables Checklist + +Use this checklist to keep DDD adoption practical and measurable. + +## Strategic deliverables + +- Subdomain map (core, supporting, generic) +- Bounded context map and ownership +- Ubiquitous language glossary +- 1-2 ADRs documenting critical boundary decisions + +## Tactical deliverables + +- Aggregate list with invariants +- Value object list +- Domain events list +- Repository contracts and transaction boundaries + +## Evented deliverables (only when required) + +- Command and query separation rationale +- Event schema versioning policy +- Saga compensation matrix +- Projection rebuild strategy diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/event-store-design/SKILL.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/event-store-design/SKILL.md new file mode 100644 index 00000000..bf409ca8 --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/event-store-design/SKILL.md @@ -0,0 +1,452 @@ +--- +name: event-store-design +description: "Design and implement event stores for event-sourced systems. Use when building event sourcing infrastructure, choosing event store technologies, or implementing event persistence patterns." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Event Store Design + +Comprehensive guide to designing event stores for event-sourced applications. + +## Do not use this skill when + +- The task is unrelated to event store design +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Use this skill when + +- Designing event sourcing infrastructure +- Choosing between event store technologies +- Implementing custom event stores +- Optimizing event storage and retrieval +- Setting up event store schemas +- Planning for event store scaling + +## Core Concepts + +### 1. Event Store Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Event Store โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Stream 1 โ”‚ โ”‚ Stream 2 โ”‚ โ”‚ Stream 3 โ”‚ โ”‚ +โ”‚ โ”‚ (Aggregate) โ”‚ โ”‚ (Aggregate) โ”‚ โ”‚ (Aggregate) โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”‚ Event 1 โ”‚ โ”‚ Event 1 โ”‚ โ”‚ Event 1 โ”‚ โ”‚ +โ”‚ โ”‚ Event 2 โ”‚ โ”‚ Event 2 โ”‚ โ”‚ Event 2 โ”‚ โ”‚ +โ”‚ โ”‚ Event 3 โ”‚ โ”‚ ... โ”‚ โ”‚ Event 3 โ”‚ โ”‚ +โ”‚ โ”‚ ... โ”‚ โ”‚ โ”‚ โ”‚ Event 4 โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Global Position: 1 โ†’ 2 โ†’ 3 โ†’ 4 โ†’ 5 โ†’ 6 โ†’ ... โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2. Event Store Requirements + +| Requirement | Description | +| ----------------- | ---------------------------------- | +| **Append-only** | Events are immutable, only appends | +| **Ordered** | Per-stream and global ordering | +| **Versioned** | Optimistic concurrency control | +| **Subscriptions** | Real-time event notifications | +| **Idempotent** | Handle duplicate writes safely | + +## Technology Comparison + +| Technology | Best For | Limitations | +| ---------------- | ------------------------- | -------------------------------- | +| **EventStoreDB** | Pure event sourcing | Single-purpose | +| **PostgreSQL** | Existing Postgres stack | Manual implementation | +| **Kafka** | High-throughput streaming | Not ideal for per-stream queries | +| **DynamoDB** | Serverless, AWS-native | Query limitations | +| **Marten** | .NET ecosystems | .NET specific | + +## Templates + +### Template 1: PostgreSQL Event Store Schema + +```sql +-- Events table +CREATE TABLE events ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + stream_id VARCHAR(255) NOT NULL, + stream_type VARCHAR(255) NOT NULL, + event_type VARCHAR(255) NOT NULL, + event_data JSONB NOT NULL, + metadata JSONB DEFAULT '{}', + version BIGINT NOT NULL, + global_position BIGSERIAL, + created_at TIMESTAMPTZ DEFAULT NOW(), + + CONSTRAINT unique_stream_version UNIQUE (stream_id, version) +); + +-- Index for stream queries +CREATE INDEX idx_events_stream_id ON events(stream_id, version); + +-- Index for global subscription +CREATE INDEX idx_events_global_position ON events(global_position); + +-- Index for event type queries +CREATE INDEX idx_events_event_type ON events(event_type); + +-- Index for time-based queries +CREATE INDEX idx_events_created_at ON events(created_at); + +-- Snapshots table +CREATE TABLE snapshots ( + stream_id VARCHAR(255) PRIMARY KEY, + stream_type VARCHAR(255) NOT NULL, + snapshot_data JSONB NOT NULL, + version BIGINT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Subscriptions checkpoint table +CREATE TABLE subscription_checkpoints ( + subscription_id VARCHAR(255) PRIMARY KEY, + last_position BIGINT NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +### Template 2: Python Event Store Implementation + +```python +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any, Optional, List +from uuid import UUID, uuid4 +import json +import asyncpg + +@dataclass +class Event: + stream_id: str + event_type: str + data: dict + metadata: dict = field(default_factory=dict) + event_id: UUID = field(default_factory=uuid4) + version: Optional[int] = None + global_position: Optional[int] = None + created_at: datetime = field(default_factory=datetime.utcnow) + + +class EventStore: + def __init__(self, pool: asyncpg.Pool): + self.pool = pool + + async def append_events( + self, + stream_id: str, + stream_type: str, + events: List[Event], + expected_version: Optional[int] = None + ) -> List[Event]: + """Append events to a stream with optimistic concurrency.""" + async with self.pool.acquire() as conn: + async with conn.transaction(): + # Check expected version + if expected_version is not None: + current = await conn.fetchval( + "SELECT MAX(version) FROM events WHERE stream_id = $1", + stream_id + ) + current = current or 0 + if current != expected_version: + raise ConcurrencyError( + f"Expected version {expected_version}, got {current}" + ) + + # Get starting version + start_version = await conn.fetchval( + "SELECT COALESCE(MAX(version), 0) + 1 FROM events WHERE stream_id = $1", + stream_id + ) + + # Insert events + saved_events = [] + for i, event in enumerate(events): + event.version = start_version + i + row = await conn.fetchrow( + """ + INSERT INTO events (id, stream_id, stream_type, event_type, + event_data, metadata, version, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING global_position + """, + event.event_id, + stream_id, + stream_type, + event.event_type, + json.dumps(event.data), + json.dumps(event.metadata), + event.version, + event.created_at + ) + event.global_position = row['global_position'] + saved_events.append(event) + + return saved_events + + async def read_stream( + self, + stream_id: str, + from_version: int = 0, + limit: int = 1000 + ) -> List[Event]: + """Read events from a stream.""" + async with self.pool.acquire() as conn: + rows = await conn.fetch( + """ + SELECT id, stream_id, event_type, event_data, metadata, + version, global_position, created_at + FROM events + WHERE stream_id = $1 AND version >= $2 + ORDER BY version + LIMIT $3 + """, + stream_id, from_version, limit + ) + return [self._row_to_event(row) for row in rows] + + async def read_all( + self, + from_position: int = 0, + limit: int = 1000 + ) -> List[Event]: + """Read all events globally.""" + async with self.pool.acquire() as conn: + rows = await conn.fetch( + """ + SELECT id, stream_id, event_type, event_data, metadata, + version, global_position, created_at + FROM events + WHERE global_position > $1 + ORDER BY global_position + LIMIT $2 + """, + from_position, limit + ) + return [self._row_to_event(row) for row in rows] + + async def subscribe( + self, + subscription_id: str, + handler, + from_position: int = 0, + batch_size: int = 100 + ): + """Subscribe to all events from a position.""" + # Get checkpoint + async with self.pool.acquire() as conn: + checkpoint = await conn.fetchval( + """ + SELECT last_position FROM subscription_checkpoints + WHERE subscription_id = $1 + """, + subscription_id + ) + position = checkpoint or from_position + + while True: + events = await self.read_all(position, batch_size) + if not events: + await asyncio.sleep(1) # Poll interval + continue + + for event in events: + await handler(event) + position = event.global_position + + # Save checkpoint + async with self.pool.acquire() as conn: + await conn.execute( + """ + INSERT INTO subscription_checkpoints (subscription_id, last_position) + VALUES ($1, $2) + ON CONFLICT (subscription_id) + DO UPDATE SET last_position = $2, updated_at = NOW() + """, + subscription_id, position + ) + + def _row_to_event(self, row) -> Event: + return Event( + event_id=row['id'], + stream_id=row['stream_id'], + event_type=row['event_type'], + data=json.loads(row['event_data']), + metadata=json.loads(row['metadata']), + version=row['version'], + global_position=row['global_position'], + created_at=row['created_at'] + ) + + +class ConcurrencyError(Exception): + """Raised when optimistic concurrency check fails.""" + pass +``` + +### Template 3: EventStoreDB Usage + +```python +from esdbclient import EventStoreDBClient, NewEvent, StreamState +import json + +# Connect +client = EventStoreDBClient(uri="esdb://localhost:2113?tls=false") + +# Append events +def append_events(stream_name: str, events: list, expected_revision=None): + new_events = [ + NewEvent( + type=event['type'], + data=json.dumps(event['data']).encode(), + metadata=json.dumps(event.get('metadata', {})).encode() + ) + for event in events + ] + + if expected_revision is None: + state = StreamState.ANY + elif expected_revision == -1: + state = StreamState.NO_STREAM + else: + state = expected_revision + + return client.append_to_stream( + stream_name=stream_name, + events=new_events, + current_version=state + ) + +# Read stream +def read_stream(stream_name: str, from_revision: int = 0): + events = client.get_stream( + stream_name=stream_name, + stream_position=from_revision + ) + return [ + { + 'type': event.type, + 'data': json.loads(event.data), + 'metadata': json.loads(event.metadata) if event.metadata else {}, + 'stream_position': event.stream_position, + 'commit_position': event.commit_position + } + for event in events + ] + +# Subscribe to all +async def subscribe_to_all(handler, from_position: int = 0): + subscription = client.subscribe_to_all(commit_position=from_position) + async for event in subscription: + await handler({ + 'type': event.type, + 'data': json.loads(event.data), + 'stream_id': event.stream_name, + 'position': event.commit_position + }) + +# Category projection ($ce-Category) +def read_category(category: str): + """Read all events for a category using system projection.""" + return read_stream(f"$ce-{category}") +``` + +### Template 4: DynamoDB Event Store + +```python +import boto3 +from boto3.dynamodb.conditions import Key +from datetime import datetime +import json +import uuid + +class DynamoEventStore: + def __init__(self, table_name: str): + self.dynamodb = boto3.resource('dynamodb') + self.table = self.dynamodb.Table(table_name) + + def append_events(self, stream_id: str, events: list, expected_version: int = None): + """Append events with conditional write for concurrency.""" + with self.table.batch_writer() as batch: + for i, event in enumerate(events): + version = (expected_version or 0) + i + 1 + item = { + 'PK': f"STREAM#{stream_id}", + 'SK': f"VERSION#{version:020d}", + 'GSI1PK': 'EVENTS', + 'GSI1SK': datetime.utcnow().isoformat(), + 'event_id': str(uuid.uuid4()), + 'stream_id': stream_id, + 'event_type': event['type'], + 'event_data': json.dumps(event['data']), + 'version': version, + 'created_at': datetime.utcnow().isoformat() + } + batch.put_item(Item=item) + return events + + def read_stream(self, stream_id: str, from_version: int = 0): + """Read events from a stream.""" + response = self.table.query( + KeyConditionExpression=Key('PK').eq(f"STREAM#{stream_id}") & + Key('SK').gte(f"VERSION#{from_version:020d}") + ) + return [ + { + 'event_type': item['event_type'], + 'data': json.loads(item['event_data']), + 'version': item['version'] + } + for item in response['Items'] + ] + +# Table definition (CloudFormation/Terraform) +""" +DynamoDB Table: + - PK (Partition Key): String + - SK (Sort Key): String + - GSI1PK, GSI1SK for global ordering + +Capacity: On-demand or provisioned based on throughput needs +""" +``` + +## Best Practices + +### Do's + +- **Use stream IDs that include aggregate type** - `Order-{uuid}` +- **Include correlation/causation IDs** - For tracing +- **Version events from day one** - Plan for schema evolution +- **Implement idempotency** - Use event IDs for deduplication +- **Index appropriately** - For your query patterns + +### Don'ts + +- **Don't update or delete events** - They're immutable facts +- **Don't store large payloads** - Keep events small +- **Don't skip optimistic concurrency** - Prevents data corruption +- **Don't ignore backpressure** - Handle slow consumers + +## Resources + +- [EventStoreDB](https://www.eventstore.com/) +- [Marten Events](https://martendb.io/events/) +- [Event Sourcing Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/event-sourcing) diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/event-store-design/resources/implementation-playbook.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/event-store-design/resources/implementation-playbook.md new file mode 100644 index 00000000..4d39074b --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/event-store-design/resources/implementation-playbook.md @@ -0,0 +1,25 @@ +# Event Store Design Playbook + +## Schema and stream strategy + +- Use append-only writes with optimistic concurrency. +- Keep per-stream ordering and global ordering indexes. +- Include metadata fields for causation and correlation IDs. + +## Operational guardrails + +- Never mutate historical events in production. +- Version event schema with explicit upcasters/downcasters policy. +- Define retention and archival strategy by stream type. + +## Subscription and projection safety + +- Track per-subscriber checkpoint positions. +- Make handlers idempotent and replay-safe. +- Support projection rebuild from a clean checkpoint. + +## Performance checklist + +- Index stream id + version. +- Index global position. +- Add snapshot policy for long-lived aggregates. diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/projection-patterns/SKILL.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/projection-patterns/SKILL.md new file mode 100644 index 00000000..a3856df7 --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/projection-patterns/SKILL.md @@ -0,0 +1,36 @@ +--- +name: projection-patterns +description: "Build read models and projections from event streams. Use when implementing CQRS read sides, building materialized views, or optimizing query performance in event-sourced systems." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Projection Patterns + +Comprehensive guide to building projections and read models for event-sourced systems. + +## Use this skill when + +- Building CQRS read models +- Creating materialized views from events +- Optimizing query performance +- Implementing real-time dashboards +- Building search indexes from events +- Aggregating data across streams + +## Do not use this skill when + +- The task is unrelated to projection patterns +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Resources + +- `resources/implementation-playbook.md` for detailed patterns and examples. diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/projection-patterns/resources/implementation-playbook.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/projection-patterns/resources/implementation-playbook.md new file mode 100644 index 00000000..819705f7 --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/projection-patterns/resources/implementation-playbook.md @@ -0,0 +1,501 @@ +# Projection Patterns Implementation Playbook + +This file contains detailed patterns, checklists, and code samples referenced by the skill. + +# Projection Patterns + +Comprehensive guide to building projections and read models for event-sourced systems. + +## Do not use this skill when + +- The task is unrelated to projection patterns +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Use this skill when + +- Building CQRS read models +- Creating materialized views from events +- Optimizing query performance +- Implementing real-time dashboards +- Building search indexes from events +- Aggregating data across streams + +## Core Concepts + +### 1. Projection Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Event Store โ”‚โ”€โ”€โ”€โ”€โ–บโ”‚ Projector โ”‚โ”€โ”€โ”€โ”€โ–บโ”‚ Read Model โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ (Database) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Events โ”‚ โ”‚ โ”‚ โ”‚ Handler โ”‚ โ”‚ โ”‚ โ”‚ Tables โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ Logic โ”‚ โ”‚ โ”‚ โ”‚ Views โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ Cache โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2. Projection Types + +| Type | Description | Use Case | +| -------------- | --------------------------- | ---------------------- | +| **Live** | Real-time from subscription | Current state queries | +| **Catchup** | Process historical events | Rebuilding read models | +| **Persistent** | Stores checkpoint | Resume after restart | +| **Inline** | Same transaction as write | Strong consistency | + +## Templates + +### Template 1: Basic Projector + +```python +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Dict, Any, Callable, List +import asyncpg + +@dataclass +class Event: + stream_id: str + event_type: str + data: dict + version: int + global_position: int + + +class Projection(ABC): + """Base class for projections.""" + + @property + @abstractmethod + def name(self) -> str: + """Unique projection name for checkpointing.""" + pass + + @abstractmethod + def handles(self) -> List[str]: + """List of event types this projection handles.""" + pass + + @abstractmethod + async def apply(self, event: Event) -> None: + """Apply event to the read model.""" + pass + + +class Projector: + """Runs projections from event store.""" + + def __init__(self, event_store, checkpoint_store): + self.event_store = event_store + self.checkpoint_store = checkpoint_store + self.projections: List[Projection] = [] + + def register(self, projection: Projection): + self.projections.append(projection) + + async def run(self, batch_size: int = 100): + """Run all projections continuously.""" + while True: + for projection in self.projections: + await self._run_projection(projection, batch_size) + await asyncio.sleep(0.1) + + async def _run_projection(self, projection: Projection, batch_size: int): + checkpoint = await self.checkpoint_store.get(projection.name) + position = checkpoint or 0 + + events = await self.event_store.read_all(position, batch_size) + + for event in events: + if event.event_type in projection.handles(): + await projection.apply(event) + + await self.checkpoint_store.save( + projection.name, + event.global_position + ) + + async def rebuild(self, projection: Projection): + """Rebuild a projection from scratch.""" + await self.checkpoint_store.delete(projection.name) + # Optionally clear read model tables + await self._run_projection(projection, batch_size=1000) +``` + +### Template 2: Order Summary Projection + +```python +class OrderSummaryProjection(Projection): + """Projects order events to a summary read model.""" + + def __init__(self, db_pool: asyncpg.Pool): + self.pool = db_pool + + @property + def name(self) -> str: + return "order_summary" + + def handles(self) -> List[str]: + return [ + "OrderCreated", + "OrderItemAdded", + "OrderItemRemoved", + "OrderShipped", + "OrderCompleted", + "OrderCancelled" + ] + + async def apply(self, event: Event) -> None: + handlers = { + "OrderCreated": self._handle_created, + "OrderItemAdded": self._handle_item_added, + "OrderItemRemoved": self._handle_item_removed, + "OrderShipped": self._handle_shipped, + "OrderCompleted": self._handle_completed, + "OrderCancelled": self._handle_cancelled, + } + + handler = handlers.get(event.event_type) + if handler: + await handler(event) + + async def _handle_created(self, event: Event): + async with self.pool.acquire() as conn: + await conn.execute( + """ + INSERT INTO order_summaries + (order_id, customer_id, status, total_amount, item_count, created_at) + VALUES ($1, $2, $3, $4, $5, $6) + """, + event.data['order_id'], + event.data['customer_id'], + 'pending', + 0, + 0, + event.data['created_at'] + ) + + async def _handle_item_added(self, event: Event): + async with self.pool.acquire() as conn: + await conn.execute( + """ + UPDATE order_summaries + SET total_amount = total_amount + $2, + item_count = item_count + 1, + updated_at = NOW() + WHERE order_id = $1 + """, + event.data['order_id'], + event.data['price'] * event.data['quantity'] + ) + + async def _handle_item_removed(self, event: Event): + async with self.pool.acquire() as conn: + await conn.execute( + """ + UPDATE order_summaries + SET total_amount = total_amount - $2, + item_count = item_count - 1, + updated_at = NOW() + WHERE order_id = $1 + """, + event.data['order_id'], + event.data['price'] * event.data['quantity'] + ) + + async def _handle_shipped(self, event: Event): + async with self.pool.acquire() as conn: + await conn.execute( + """ + UPDATE order_summaries + SET status = 'shipped', + shipped_at = $2, + updated_at = NOW() + WHERE order_id = $1 + """, + event.data['order_id'], + event.data['shipped_at'] + ) + + async def _handle_completed(self, event: Event): + async with self.pool.acquire() as conn: + await conn.execute( + """ + UPDATE order_summaries + SET status = 'completed', + completed_at = $2, + updated_at = NOW() + WHERE order_id = $1 + """, + event.data['order_id'], + event.data['completed_at'] + ) + + async def _handle_cancelled(self, event: Event): + async with self.pool.acquire() as conn: + await conn.execute( + """ + UPDATE order_summaries + SET status = 'cancelled', + cancelled_at = $2, + cancellation_reason = $3, + updated_at = NOW() + WHERE order_id = $1 + """, + event.data['order_id'], + event.data['cancelled_at'], + event.data.get('reason') + ) +``` + +### Template 3: Elasticsearch Search Projection + +```python +from elasticsearch import AsyncElasticsearch + +class ProductSearchProjection(Projection): + """Projects product events to Elasticsearch for full-text search.""" + + def __init__(self, es_client: AsyncElasticsearch): + self.es = es_client + self.index = "products" + + @property + def name(self) -> str: + return "product_search" + + def handles(self) -> List[str]: + return [ + "ProductCreated", + "ProductUpdated", + "ProductPriceChanged", + "ProductDeleted" + ] + + async def apply(self, event: Event) -> None: + if event.event_type == "ProductCreated": + await self.es.index( + index=self.index, + id=event.data['product_id'], + document={ + 'name': event.data['name'], + 'description': event.data['description'], + 'category': event.data['category'], + 'price': event.data['price'], + 'tags': event.data.get('tags', []), + 'created_at': event.data['created_at'] + } + ) + + elif event.event_type == "ProductUpdated": + await self.es.update( + index=self.index, + id=event.data['product_id'], + doc={ + 'name': event.data['name'], + 'description': event.data['description'], + 'category': event.data['category'], + 'tags': event.data.get('tags', []), + 'updated_at': event.data['updated_at'] + } + ) + + elif event.event_type == "ProductPriceChanged": + await self.es.update( + index=self.index, + id=event.data['product_id'], + doc={ + 'price': event.data['new_price'], + 'price_updated_at': event.data['changed_at'] + } + ) + + elif event.event_type == "ProductDeleted": + await self.es.delete( + index=self.index, + id=event.data['product_id'] + ) +``` + +### Template 4: Aggregating Projection + +```python +class DailySalesProjection(Projection): + """Aggregates sales data by day for reporting.""" + + def __init__(self, db_pool: asyncpg.Pool): + self.pool = db_pool + + @property + def name(self) -> str: + return "daily_sales" + + def handles(self) -> List[str]: + return ["OrderCompleted", "OrderRefunded"] + + async def apply(self, event: Event) -> None: + if event.event_type == "OrderCompleted": + await self._increment_sales(event) + elif event.event_type == "OrderRefunded": + await self._decrement_sales(event) + + async def _increment_sales(self, event: Event): + date = event.data['completed_at'][:10] # YYYY-MM-DD + async with self.pool.acquire() as conn: + await conn.execute( + """ + INSERT INTO daily_sales (date, total_orders, total_revenue, total_items) + VALUES ($1, 1, $2, $3) + ON CONFLICT (date) DO UPDATE SET + total_orders = daily_sales.total_orders + 1, + total_revenue = daily_sales.total_revenue + $2, + total_items = daily_sales.total_items + $3, + updated_at = NOW() + """, + date, + event.data['total_amount'], + event.data['item_count'] + ) + + async def _decrement_sales(self, event: Event): + date = event.data['original_completed_at'][:10] + async with self.pool.acquire() as conn: + await conn.execute( + """ + UPDATE daily_sales SET + total_orders = total_orders - 1, + total_revenue = total_revenue - $2, + total_refunds = total_refunds + $2, + updated_at = NOW() + WHERE date = $1 + """, + date, + event.data['refund_amount'] + ) +``` + +### Template 5: Multi-Table Projection + +```python +class CustomerActivityProjection(Projection): + """Projects customer activity across multiple tables.""" + + def __init__(self, db_pool: asyncpg.Pool): + self.pool = db_pool + + @property + def name(self) -> str: + return "customer_activity" + + def handles(self) -> List[str]: + return [ + "CustomerCreated", + "OrderCompleted", + "ReviewSubmitted", + "CustomerTierChanged" + ] + + async def apply(self, event: Event) -> None: + async with self.pool.acquire() as conn: + async with conn.transaction(): + if event.event_type == "CustomerCreated": + # Insert into customers table + await conn.execute( + """ + INSERT INTO customers (customer_id, email, name, tier, created_at) + VALUES ($1, $2, $3, 'bronze', $4) + """, + event.data['customer_id'], + event.data['email'], + event.data['name'], + event.data['created_at'] + ) + # Initialize activity summary + await conn.execute( + """ + INSERT INTO customer_activity_summary + (customer_id, total_orders, total_spent, total_reviews) + VALUES ($1, 0, 0, 0) + """, + event.data['customer_id'] + ) + + elif event.event_type == "OrderCompleted": + # Update activity summary + await conn.execute( + """ + UPDATE customer_activity_summary SET + total_orders = total_orders + 1, + total_spent = total_spent + $2, + last_order_at = $3 + WHERE customer_id = $1 + """, + event.data['customer_id'], + event.data['total_amount'], + event.data['completed_at'] + ) + # Insert into order history + await conn.execute( + """ + INSERT INTO customer_order_history + (customer_id, order_id, amount, completed_at) + VALUES ($1, $2, $3, $4) + """, + event.data['customer_id'], + event.data['order_id'], + event.data['total_amount'], + event.data['completed_at'] + ) + + elif event.event_type == "ReviewSubmitted": + await conn.execute( + """ + UPDATE customer_activity_summary SET + total_reviews = total_reviews + 1, + last_review_at = $2 + WHERE customer_id = $1 + """, + event.data['customer_id'], + event.data['submitted_at'] + ) + + elif event.event_type == "CustomerTierChanged": + await conn.execute( + """ + UPDATE customers SET tier = $2, updated_at = NOW() + WHERE customer_id = $1 + """, + event.data['customer_id'], + event.data['new_tier'] + ) +``` + +## Best Practices + +### Do's + +- **Make projections idempotent** - Safe to replay +- **Use transactions** - For multi-table updates +- **Store checkpoints** - Resume after failures +- **Monitor lag** - Alert on projection delays +- **Plan for rebuilds** - Design for reconstruction + +### Don'ts + +- **Don't couple projections** - Each is independent +- **Don't skip error handling** - Log and alert on failures +- **Don't ignore ordering** - Events must be processed in order +- **Don't over-normalize** - Denormalize for query patterns + +## Resources + +- [CQRS Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs) +- [Projection Building Blocks](https://zimarev.com/blog/event-sourcing/projections/) diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/saga-orchestration/SKILL.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/saga-orchestration/SKILL.md new file mode 100644 index 00000000..3b587b36 --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/saga-orchestration/SKILL.md @@ -0,0 +1,507 @@ +--- +name: saga-orchestration +description: "Patterns for managing distributed transactions and long-running business processes." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Saga Orchestration + +Patterns for managing distributed transactions and long-running business processes. + +## Do not use this skill when + +- The task is unrelated to saga orchestration +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Use this skill when + +- Coordinating multi-service transactions +- Implementing compensating transactions +- Managing long-running business workflows +- Handling failures in distributed systems +- Building order fulfillment processes +- Implementing approval workflows + +## Core Concepts + +### 1. Saga Types + +``` +Choreography Orchestration +โ”Œโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚Svc Aโ”‚โ”€โ–บโ”‚Svc Bโ”‚โ”€โ–บโ”‚Svc Cโ”‚ โ”‚ Orchestratorโ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ โ”‚ โ”‚ + โ–ผ โ–ผ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ” + Event Event Event โ–ผ โ–ผ โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ” + โ”‚Svc1โ”‚โ”‚Svc2โ”‚โ”‚Svc3โ”‚ + โ””โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2. Saga Execution States + +| State | Description | +| ---------------- | ------------------------------ | +| **Started** | Saga initiated | +| **Pending** | Waiting for step completion | +| **Compensating** | Rolling back due to failure | +| **Completed** | All steps succeeded | +| **Failed** | Saga failed after compensation | + +## Templates + +### Template 1: Saga Orchestrator Base + +```python +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from enum import Enum +from typing import List, Dict, Any, Optional +from datetime import datetime +import uuid + +class SagaState(Enum): + STARTED = "started" + PENDING = "pending" + COMPENSATING = "compensating" + COMPLETED = "completed" + FAILED = "failed" + + +@dataclass +class SagaStep: + name: str + action: str + compensation: str + status: str = "pending" + result: Optional[Dict] = None + error: Optional[str] = None + executed_at: Optional[datetime] = None + compensated_at: Optional[datetime] = None + + +@dataclass +class Saga: + saga_id: str + saga_type: str + state: SagaState + data: Dict[str, Any] + steps: List[SagaStep] + current_step: int = 0 + created_at: datetime = field(default_factory=datetime.utcnow) + updated_at: datetime = field(default_factory=datetime.utcnow) + + +class SagaOrchestrator(ABC): + """Base class for saga orchestrators.""" + + def __init__(self, saga_store, event_publisher): + self.saga_store = saga_store + self.event_publisher = event_publisher + + @abstractmethod + def define_steps(self, data: Dict) -> List[SagaStep]: + """Define the saga steps.""" + pass + + @property + @abstractmethod + def saga_type(self) -> str: + """Unique saga type identifier.""" + pass + + async def start(self, data: Dict) -> Saga: + """Start a new saga.""" + saga = Saga( + saga_id=str(uuid.uuid4()), + saga_type=self.saga_type, + state=SagaState.STARTED, + data=data, + steps=self.define_steps(data) + ) + await self.saga_store.save(saga) + await self._execute_next_step(saga) + return saga + + async def handle_step_completed(self, saga_id: str, step_name: str, result: Dict): + """Handle successful step completion.""" + saga = await self.saga_store.get(saga_id) + + # Update step + for step in saga.steps: + if step.name == step_name: + step.status = "completed" + step.result = result + step.executed_at = datetime.utcnow() + break + + saga.current_step += 1 + saga.updated_at = datetime.utcnow() + + # Check if saga is complete + if saga.current_step >= len(saga.steps): + saga.state = SagaState.COMPLETED + await self.saga_store.save(saga) + await self._on_saga_completed(saga) + else: + saga.state = SagaState.PENDING + await self.saga_store.save(saga) + await self._execute_next_step(saga) + + async def handle_step_failed(self, saga_id: str, step_name: str, error: str): + """Handle step failure - start compensation.""" + saga = await self.saga_store.get(saga_id) + + # Mark step as failed + for step in saga.steps: + if step.name == step_name: + step.status = "failed" + step.error = error + break + + saga.state = SagaState.COMPENSATING + saga.updated_at = datetime.utcnow() + await self.saga_store.save(saga) + + # Start compensation from current step backwards + await self._compensate(saga) + + async def _execute_next_step(self, saga: Saga): + """Execute the next step in the saga.""" + if saga.current_step >= len(saga.steps): + return + + step = saga.steps[saga.current_step] + step.status = "executing" + await self.saga_store.save(saga) + + # Publish command to execute step + await self.event_publisher.publish( + step.action, + { + "saga_id": saga.saga_id, + "step_name": step.name, + **saga.data + } + ) + + async def _compensate(self, saga: Saga): + """Execute compensation for completed steps.""" + # Compensate in reverse order + for i in range(saga.current_step - 1, -1, -1): + step = saga.steps[i] + if step.status == "completed": + step.status = "compensating" + await self.saga_store.save(saga) + + await self.event_publisher.publish( + step.compensation, + { + "saga_id": saga.saga_id, + "step_name": step.name, + "original_result": step.result, + **saga.data + } + ) + + async def handle_compensation_completed(self, saga_id: str, step_name: str): + """Handle compensation completion.""" + saga = await self.saga_store.get(saga_id) + + for step in saga.steps: + if step.name == step_name: + step.status = "compensated" + step.compensated_at = datetime.utcnow() + break + + # Check if all compensations complete + all_compensated = all( + s.status in ("compensated", "pending", "failed") + for s in saga.steps + ) + + if all_compensated: + saga.state = SagaState.FAILED + await self._on_saga_failed(saga) + + await self.saga_store.save(saga) + + async def _on_saga_completed(self, saga: Saga): + """Called when saga completes successfully.""" + await self.event_publisher.publish( + f"{self.saga_type}Completed", + {"saga_id": saga.saga_id, **saga.data} + ) + + async def _on_saga_failed(self, saga: Saga): + """Called when saga fails after compensation.""" + await self.event_publisher.publish( + f"{self.saga_type}Failed", + {"saga_id": saga.saga_id, "error": "Saga failed", **saga.data} + ) +``` + +### Template 2: Order Fulfillment Saga + +```python +class OrderFulfillmentSaga(SagaOrchestrator): + """Orchestrates order fulfillment across services.""" + + @property + def saga_type(self) -> str: + return "OrderFulfillment" + + def define_steps(self, data: Dict) -> List[SagaStep]: + return [ + SagaStep( + name="reserve_inventory", + action="InventoryService.ReserveItems", + compensation="InventoryService.ReleaseReservation" + ), + SagaStep( + name="process_payment", + action="PaymentService.ProcessPayment", + compensation="PaymentService.RefundPayment" + ), + SagaStep( + name="create_shipment", + action="ShippingService.CreateShipment", + compensation="ShippingService.CancelShipment" + ), + SagaStep( + name="send_confirmation", + action="NotificationService.SendOrderConfirmation", + compensation="NotificationService.SendCancellationNotice" + ) + ] + + +# Usage +async def create_order(order_data: Dict): + saga = OrderFulfillmentSaga(saga_store, event_publisher) + return await saga.start({ + "order_id": order_data["order_id"], + "customer_id": order_data["customer_id"], + "items": order_data["items"], + "payment_method": order_data["payment_method"], + "shipping_address": order_data["shipping_address"] + }) + + +# Event handlers in each service +class InventoryService: + async def handle_reserve_items(self, command: Dict): + try: + # Reserve inventory + reservation = await self.reserve( + command["items"], + command["order_id"] + ) + # Report success + await self.event_publisher.publish( + "SagaStepCompleted", + { + "saga_id": command["saga_id"], + "step_name": "reserve_inventory", + "result": {"reservation_id": reservation.id} + } + ) + except InsufficientInventoryError as e: + await self.event_publisher.publish( + "SagaStepFailed", + { + "saga_id": command["saga_id"], + "step_name": "reserve_inventory", + "error": str(e) + } + ) + + async def handle_release_reservation(self, command: Dict): + # Compensating action + await self.release_reservation( + command["original_result"]["reservation_id"] + ) + await self.event_publisher.publish( + "SagaCompensationCompleted", + { + "saga_id": command["saga_id"], + "step_name": "reserve_inventory" + } + ) +``` + +### Template 3: Choreography-Based Saga + +```python +from dataclasses import dataclass +from typing import Dict, Any +import asyncio + +@dataclass +class SagaContext: + """Passed through choreographed saga events.""" + saga_id: str + step: int + data: Dict[str, Any] + completed_steps: list + + +class OrderChoreographySaga: + """Choreography-based saga using events.""" + + def __init__(self, event_bus): + self.event_bus = event_bus + self._register_handlers() + + def _register_handlers(self): + self.event_bus.subscribe("OrderCreated", self._on_order_created) + self.event_bus.subscribe("InventoryReserved", self._on_inventory_reserved) + self.event_bus.subscribe("PaymentProcessed", self._on_payment_processed) + self.event_bus.subscribe("ShipmentCreated", self._on_shipment_created) + + # Compensation handlers + self.event_bus.subscribe("PaymentFailed", self._on_payment_failed) + self.event_bus.subscribe("ShipmentFailed", self._on_shipment_failed) + + async def _on_order_created(self, event: Dict): + """Step 1: Order created, reserve inventory.""" + await self.event_bus.publish("ReserveInventory", { + "saga_id": event["order_id"], + "order_id": event["order_id"], + "items": event["items"] + }) + + async def _on_inventory_reserved(self, event: Dict): + """Step 2: Inventory reserved, process payment.""" + await self.event_bus.publish("ProcessPayment", { + "saga_id": event["saga_id"], + "order_id": event["order_id"], + "amount": event["total_amount"], + "reservation_id": event["reservation_id"] + }) + + async def _on_payment_processed(self, event: Dict): + """Step 3: Payment done, create shipment.""" + await self.event_bus.publish("CreateShipment", { + "saga_id": event["saga_id"], + "order_id": event["order_id"], + "payment_id": event["payment_id"] + }) + + async def _on_shipment_created(self, event: Dict): + """Step 4: Complete - send confirmation.""" + await self.event_bus.publish("OrderFulfilled", { + "saga_id": event["saga_id"], + "order_id": event["order_id"], + "tracking_number": event["tracking_number"] + }) + + # Compensation handlers + async def _on_payment_failed(self, event: Dict): + """Payment failed - release inventory.""" + await self.event_bus.publish("ReleaseInventory", { + "saga_id": event["saga_id"], + "reservation_id": event["reservation_id"] + }) + await self.event_bus.publish("OrderFailed", { + "order_id": event["order_id"], + "reason": "Payment failed" + }) + + async def _on_shipment_failed(self, event: Dict): + """Shipment failed - refund payment and release inventory.""" + await self.event_bus.publish("RefundPayment", { + "saga_id": event["saga_id"], + "payment_id": event["payment_id"] + }) + await self.event_bus.publish("ReleaseInventory", { + "saga_id": event["saga_id"], + "reservation_id": event["reservation_id"] + }) +``` + +### Template 4: Saga with Timeouts + +```python +class TimeoutSagaOrchestrator(SagaOrchestrator): + """Saga orchestrator with step timeouts.""" + + def __init__(self, saga_store, event_publisher, scheduler): + super().__init__(saga_store, event_publisher) + self.scheduler = scheduler + + async def _execute_next_step(self, saga: Saga): + if saga.current_step >= len(saga.steps): + return + + step = saga.steps[saga.current_step] + step.status = "executing" + step.timeout_at = datetime.utcnow() + timedelta(minutes=5) + await self.saga_store.save(saga) + + # Schedule timeout check + await self.scheduler.schedule( + f"saga_timeout_{saga.saga_id}_{step.name}", + self._check_timeout, + {"saga_id": saga.saga_id, "step_name": step.name}, + run_at=step.timeout_at + ) + + await self.event_publisher.publish( + step.action, + {"saga_id": saga.saga_id, "step_name": step.name, **saga.data} + ) + + async def _check_timeout(self, data: Dict): + """Check if step has timed out.""" + saga = await self.saga_store.get(data["saga_id"]) + step = next(s for s in saga.steps if s.name == data["step_name"]) + + if step.status == "executing": + # Step timed out - fail it + await self.handle_step_failed( + data["saga_id"], + data["step_name"], + "Step timed out" + ) +``` + +## Durable Execution Alternative + +The templates above build saga infrastructure from scratch โ€” saga stores, event publishers, compensation tracking. **Durable execution frameworks** (like DBOS) eliminate much of this boilerplate: the workflow runtime automatically persists state to a database, retries failed steps, and resumes from the last checkpoint after crashes. Instead of building a `SagaOrchestrator` base class, you write a workflow function with steps โ€” the framework handles persistence, crash recovery, and exactly-once execution semantics. Consider durable execution when you want saga-like reliability without managing the coordination infrastructure yourself. + +## Best Practices + +### Do's + +- **Make steps idempotent** - Safe to retry +- **Design compensations carefully** - They must work +- **Use correlation IDs** - For tracing across services +- **Implement timeouts** - Don't wait forever +- **Log everything** - For debugging failures + +### Don'ts + +- **Don't assume instant completion** - Sagas take time +- **Don't skip compensation testing** - Most critical part +- **Don't couple services** - Use async messaging +- **Don't ignore partial failures** - Handle gracefully + +## Related Skills + +Works well with: `event-sourcing-architect`, `workflow-automation`, `dbos-*` + +## Resources + +- [Saga Pattern](https://microservices.io/patterns/data/saga.html) +- [Designing Data-Intensive Applications](https://dataintensive.net/) diff --git a/plugins/antigravity-bundle-ddd-evented-architecture/skills/saga-orchestration/resources/implementation-playbook.md b/plugins/antigravity-bundle-ddd-evented-architecture/skills/saga-orchestration/resources/implementation-playbook.md new file mode 100644 index 00000000..d5c3cd3e --- /dev/null +++ b/plugins/antigravity-bundle-ddd-evented-architecture/skills/saga-orchestration/resources/implementation-playbook.md @@ -0,0 +1,26 @@ +# Saga Orchestration Playbook + +## When to choose orchestration vs choreography + +- Choose orchestration when business flow visibility and centralized control are required. +- Choose choreography when autonomy is high and coupling is low. + +## Saga design checklist + +- Define explicit saga state machine. +- Define timeout policy per step. +- Define compensation action for each irreversible step. +- Use idempotency keys for command handling. +- Store correlation IDs across all events and logs. + +## Failure handling + +- Retry transient failures with bounded exponential backoff. +- Escalate non-recoverable failures to compensation state. +- Capture operator-visible failure reason and current step. + +## Verification + +- Simulate failure at every step and confirm compensation path. +- Validate duplicate message handling. +- Validate recovery from orchestrator restart. diff --git a/plugins/antigravity-bundle-devops-cloud/.codex-plugin/plugin.json b/plugins/antigravity-bundle-devops-cloud/.codex-plugin/plugin.json new file mode 100644 index 00000000..7ec92661 --- /dev/null +++ b/plugins/antigravity-bundle-devops-cloud/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-devops-cloud", + "version": "8.10.0", + "description": "Install the \"DevOps & Cloud\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "devops-cloud", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "DevOps & Cloud", + "shortDescription": "DevOps & Infrastructure ยท 7 curated skills", + "longDescription": "For infrastructure and scaling. Covers Docker Expert, AWS Serverless, and 5 more skills.", + "developerName": "sickn33 and contributors", + "category": "DevOps & Infrastructure", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-devops-cloud/skills/aws-serverless/SKILL.md b/plugins/antigravity-bundle-devops-cloud/skills/aws-serverless/SKILL.md new file mode 100644 index 00000000..e8077294 --- /dev/null +++ b/plugins/antigravity-bundle-devops-cloud/skills/aws-serverless/SKILL.md @@ -0,0 +1,328 @@ +--- +name: aws-serverless +description: "Proper Lambda function structure with error handling" +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# AWS Serverless + +## Patterns + +### Lambda Handler Pattern + +Proper Lambda function structure with error handling + +**When to use**: ['Any Lambda function implementation', 'API handlers, event processors, scheduled tasks'] + +```python +```javascript +// Node.js Lambda Handler +// handler.js + +// Initialize outside handler (reused across invocations) +const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); +const { DynamoDBDocumentClient, GetCommand } = require('@aws-sdk/lib-dynamodb'); + +const client = new DynamoDBClient({}); +const docClient = DynamoDBDocumentClient.from(client); + +// Handler function +exports.handler = async (event, context) => { + // Optional: Don't wait for event loop to clear (Node.js) + context.callbackWaitsForEmptyEventLoop = false; + + try { + // Parse input based on event source + const body = typeof event.body === 'string' + ? JSON.parse(event.body) + : event.body; + + // Business logic + const result = await processRequest(body); + + // Return API Gateway compatible response + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }, + body: JSON.stringify(result) + }; + } catch (error) { + console.error('Error:', JSON.stringify({ + error: error.message, + stack: error.stack, + requestId: context.awsRequestId + })); + + return { + statusCode: error.statusCode || 500, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + error: error.message || 'Internal server error' + }) + }; + } +}; + +async function processRequest(data) { + // Your business logic here + const result = await docClient.send(new GetCommand({ + TableName: process.env.TABLE_NAME, + Key: { id: data.id } + })); + return result.Item; +} +``` + +```python +# Python Lambda Handler +# handler.py + +import json +import os +import logging +import boto3 +from botocore.exceptions import ClientError + +# Initialize outside handler (reused across invocations) +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +dynamodb = boto3.resource('dynamodb') +table = dynamodb.Table(os.environ['TABLE_NAME']) + +def handler(event, context): + try: + # Parse i +``` + +### API Gateway Integration Pattern + +REST API and HTTP API integration with Lambda + +**When to use**: ['Building REST APIs backed by Lambda', 'Need HTTP endpoints for functions'] + +```javascript +```yaml +# template.yaml (SAM) +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Globals: + Function: + Runtime: nodejs20.x + Timeout: 30 + MemorySize: 256 + Environment: + Variables: + TABLE_NAME: !Ref ItemsTable + +Resources: + # HTTP API (recommended for simple use cases) + HttpApi: + Type: AWS::Serverless::HttpApi + Properties: + StageName: prod + CorsConfiguration: + AllowOrigins: + - "*" + AllowMethods: + - GET + - POST + - DELETE + AllowHeaders: + - "*" + + # Lambda Functions + GetItemFunction: + Type: AWS::Serverless::Function + Properties: + Handler: src/handlers/get.handler + Events: + GetItem: + Type: HttpApi + Properties: + ApiId: !Ref HttpApi + Path: /items/{id} + Method: GET + Policies: + - DynamoDBReadPolicy: + TableName: !Ref ItemsTable + + CreateItemFunction: + Type: AWS::Serverless::Function + Properties: + Handler: src/handlers/create.handler + Events: + CreateItem: + Type: HttpApi + Properties: + ApiId: !Ref HttpApi + Path: /items + Method: POST + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref ItemsTable + + # DynamoDB Table + ItemsTable: + Type: AWS::DynamoDB::Table + Properties: + AttributeDefinitions: + - AttributeName: id + AttributeType: S + KeySchema: + - AttributeName: id + KeyType: HASH + BillingMode: PAY_PER_REQUEST + +Outputs: + ApiUrl: + Value: !Sub "https://${HttpApi}.execute-api.${AWS::Region}.amazonaws.com/prod" +``` + +```javascript +// src/handlers/get.js +const { getItem } = require('../lib/dynamodb'); + +exports.handler = async (event) => { + const id = event.pathParameters?.id; + + if (!id) { + return { + statusCode: 400, + body: JSON.stringify({ error: 'Missing id parameter' }) + }; + } + + const item = +``` + +### Event-Driven SQS Pattern + +Lambda triggered by SQS for reliable async processing + +**When to use**: ['Decoupled, asynchronous processing', 'Need retry logic and DLQ', 'Processing messages in batches'] + +```python +```yaml +# template.yaml +Resources: + ProcessorFunction: + Type: AWS::Serverless::Function + Properties: + Handler: src/handlers/processor.handler + Events: + SQSEvent: + Type: SQS + Properties: + Queue: !GetAtt ProcessingQueue.Arn + BatchSize: 10 + FunctionResponseTypes: + - ReportBatchItemFailures # Partial batch failure handling + + ProcessingQueue: + Type: AWS::SQS::Queue + Properties: + VisibilityTimeout: 180 # 6x Lambda timeout + RedrivePolicy: + deadLetterTargetArn: !GetAtt DeadLetterQueue.Arn + maxReceiveCount: 3 + + DeadLetterQueue: + Type: AWS::SQS::Queue + Properties: + MessageRetentionPeriod: 1209600 # 14 days +``` + +```javascript +// src/handlers/processor.js +exports.handler = async (event) => { + const batchItemFailures = []; + + for (const record of event.Records) { + try { + const body = JSON.parse(record.body); + await processMessage(body); + } catch (error) { + console.error(`Failed to process message ${record.messageId}:`, error); + // Report this item as failed (will be retried) + batchItemFailures.push({ + itemIdentifier: record.messageId + }); + } + } + + // Return failed items for retry + return { batchItemFailures }; +}; + +async function processMessage(message) { + // Your processing logic + console.log('Processing:', message); + + // Simulate work + await saveToDatabase(message); +} +``` + +```python +# Python version +import json +import logging + +logger = logging.getLogger() + +def handler(event, context): + batch_item_failures = [] + + for record in event['Records']: + try: + body = json.loads(record['body']) + process_message(body) + except Exception as e: + logger.error(f"Failed to process {record['messageId']}: {e}") + batch_item_failures.append({ + 'itemIdentifier': record['messageId'] + }) + + return {'batchItemFailures': batch_ite +``` + +## Anti-Patterns + +### โŒ Monolithic Lambda + +**Why bad**: Large deployment packages cause slow cold starts. +Hard to scale individual operations. +Updates affect entire system. + +### โŒ Large Dependencies + +**Why bad**: Increases deployment package size. +Slows down cold starts significantly. +Most of SDK/library may be unused. + +### โŒ Synchronous Calls in VPC + +**Why bad**: VPC-attached Lambdas have ENI setup overhead. +Blocking DNS lookups or connections worsen cold starts. + +## โš ๏ธ Sharp Edges + +| Issue | Severity | Solution | +|-------|----------|----------| +| Issue | high | ## Measure your INIT phase | +| Issue | high | ## Set appropriate timeout | +| Issue | high | ## Increase memory allocation | +| Issue | medium | ## Verify VPC configuration | +| Issue | medium | ## Tell Lambda not to wait for event loop | +| Issue | medium | ## For large file uploads | +| Issue | high | ## Use different buckets/prefixes | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-devops-cloud/skills/bash-linux/SKILL.md b/plugins/antigravity-bundle-devops-cloud/skills/bash-linux/SKILL.md new file mode 100644 index 00000000..b2af921b --- /dev/null +++ b/plugins/antigravity-bundle-devops-cloud/skills/bash-linux/SKILL.md @@ -0,0 +1,204 @@ +--- +name: bash-linux +description: "Bash/Linux terminal patterns. Critical commands, piping, error handling, scripting. Use when working on macOS or Linux systems." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Bash Linux Patterns + +> Essential patterns for Bash on Linux/macOS. + +--- + +## 1. Operator Syntax + +### Chaining Commands + +| Operator | Meaning | Example | +|----------|---------|---------| +| `;` | Run sequentially | `cmd1; cmd2` | +| `&&` | Run if previous succeeded | `npm install && npm run dev` | +| `\|\|` | Run if previous failed | `npm test \|\| echo "Tests failed"` | +| `\|` | Pipe output | `ls \| grep ".js"` | + +--- + +## 2. File Operations + +### Essential Commands + +| Task | Command | +|------|---------| +| List all | `ls -la` | +| Find files | `find . -name "*.js" -type f` | +| File content | `cat file.txt` | +| First N lines | `head -n 20 file.txt` | +| Last N lines | `tail -n 20 file.txt` | +| Follow log | `tail -f log.txt` | +| Search in files | `grep -r "pattern" --include="*.js"` | +| File size | `du -sh *` | +| Disk usage | `df -h` | + +--- + +## 3. Process Management + +| Task | Command | +|------|---------| +| List processes | `ps aux` | +| Find by name | `ps aux \| grep node` | +| Kill by PID | `kill -9 ` | +| Find port user | `lsof -i :3000` | +| Kill port | `kill -9 $(lsof -t -i :3000)` | +| Background | `npm run dev &` | +| Jobs | `jobs -l` | +| Bring to front | `fg %1` | + +--- + +## 4. Text Processing + +### Core Tools + +| Tool | Purpose | Example | +|------|---------|---------| +| `grep` | Search | `grep -rn "TODO" src/` | +| `sed` | Replace | `sed -i 's/old/new/g' file.txt` | +| `awk` | Extract columns | `awk '{print $1}' file.txt` | +| `cut` | Cut fields | `cut -d',' -f1 data.csv` | +| `sort` | Sort lines | `sort -u file.txt` | +| `uniq` | Unique lines | `sort file.txt \| uniq -c` | +| `wc` | Count | `wc -l file.txt` | + +--- + +## 5. Environment Variables + +| Task | Command | +|------|---------| +| View all | `env` or `printenv` | +| View one | `echo $PATH` | +| Set temporary | `export VAR="value"` | +| Set in script | `VAR="value" command` | +| Add to PATH | `export PATH="$PATH:/new/path"` | + +--- + +## 6. Network + +| Task | Command | +|------|---------| +| Download | `curl -O https://example.com/file` | +| API request | `curl -X GET https://api.example.com` | +| POST JSON | `curl -X POST -H "Content-Type: application/json" -d '{"key":"value"}' URL` | +| Check port | `nc -zv localhost 3000` | +| Network info | `ifconfig` or `ip addr` | + +--- + +## 7. Script Template + +```bash +#!/bin/bash +set -euo pipefail # Exit on error, undefined var, pipe fail + +# Colors (optional) +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Functions +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; } + +# Main +main() { + log_info "Starting..." + # Your logic here + log_info "Done!" +} + +main "$@" +``` + +--- + +## 8. Common Patterns + +### Check if command exists + +```bash +if command -v node &> /dev/null; then + echo "Node is installed" +fi +``` + +### Default variable value + +```bash +NAME=${1:-"default_value"} +``` + +### Read file line by line + +```bash +while IFS= read -r line; do + echo "$line" +done < file.txt +``` + +### Loop over files + +```bash +for file in *.js; do + echo "Processing $file" +done +``` + +--- + +## 9. Differences from PowerShell + +| Task | PowerShell | Bash | +|------|------------|------| +| List files | `Get-ChildItem` | `ls -la` | +| Find files | `Get-ChildItem -Recurse` | `find . -type f` | +| Environment | `$env:VAR` | `$VAR` | +| String concat | `"$a$b"` | `"$a$b"` (same) | +| Null check | `if ($x)` | `if [ -n "$x" ]` | +| Pipeline | Object-based | Text-based | + +--- + +## 10. Error Handling + +### Set options + +```bash +set -e # Exit on error +set -u # Exit on undefined variable +set -o pipefail # Exit on pipe failure +set -x # Debug: print commands +``` + +### Trap for cleanup + +```bash +cleanup() { + echo "Cleaning up..." + rm -f /tmp/tempfile +} +trap cleanup EXIT +``` + +--- + +> **Remember:** Bash is text-based. Use `&&` for success chains, `set -e` for safety, and quote your variables! + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-devops-cloud/skills/deployment-procedures/SKILL.md b/plugins/antigravity-bundle-devops-cloud/skills/deployment-procedures/SKILL.md new file mode 100644 index 00000000..62447861 --- /dev/null +++ b/plugins/antigravity-bundle-devops-cloud/skills/deployment-procedures/SKILL.md @@ -0,0 +1,246 @@ +--- +name: deployment-procedures +description: "Production deployment principles and decision-making. Safe deployment workflows, rollback strategies, and verification. Teaches thinking, not scripts." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Deployment Procedures + +> Deployment principles and decision-making for safe production releases. +> **Learn to THINK, not memorize scripts.** + +--- + +## โš ๏ธ How to Use This Skill + +This skill teaches **deployment principles**, not bash scripts to copy. + +- Every deployment is unique +- Understand the WHY behind each step +- Adapt procedures to your platform + +--- + +## 1. Platform Selection + +### Decision Tree + +``` +What are you deploying? +โ”‚ +โ”œโ”€โ”€ Static site / JAMstack +โ”‚ โ””โ”€โ”€ Vercel, Netlify, Cloudflare Pages +โ”‚ +โ”œโ”€โ”€ Simple web app +โ”‚ โ”œโ”€โ”€ Managed โ†’ Railway, Render, Fly.io +โ”‚ โ””โ”€โ”€ Control โ†’ VPS + PM2/Docker +โ”‚ +โ”œโ”€โ”€ Microservices +โ”‚ โ””โ”€โ”€ Container orchestration +โ”‚ +โ””โ”€โ”€ Serverless + โ””โ”€โ”€ Edge functions, Lambda +``` + +### Each Platform Has Different Procedures + +| Platform | Deployment Method | +|----------|------------------| +| **Vercel/Netlify** | Git push, auto-deploy | +| **Railway/Render** | Git push or CLI | +| **VPS + PM2** | SSH + manual steps | +| **Docker** | Image push + orchestration | +| **Kubernetes** | kubectl apply | + +--- + +## 2. Pre-Deployment Principles + +### The 4 Verification Categories + +| Category | What to Check | +|----------|--------------| +| **Code Quality** | Tests passing, linting clean, reviewed | +| **Build** | Production build works, no warnings | +| **Environment** | Env vars set, secrets current | +| **Safety** | Backup done, rollback plan ready | + +### Pre-Deployment Checklist + +- [ ] All tests passing +- [ ] Code reviewed and approved +- [ ] Production build successful +- [ ] Environment variables verified +- [ ] Database migrations ready (if any) +- [ ] Rollback plan documented +- [ ] Team notified +- [ ] Monitoring ready + +--- + +## 3. Deployment Workflow Principles + +### The 5-Phase Process + +``` +1. PREPARE + โ””โ”€โ”€ Verify code, build, env vars + +2. BACKUP + โ””โ”€โ”€ Save current state before changing + +3. DEPLOY + โ””โ”€โ”€ Execute with monitoring open + +4. VERIFY + โ””โ”€โ”€ Health check, logs, key flows + +5. CONFIRM or ROLLBACK + โ””โ”€โ”€ All good? Confirm. Issues? Rollback. +``` + +### Phase Principles + +| Phase | Principle | +|-------|-----------| +| **Prepare** | Never deploy untested code | +| **Backup** | Can't rollback without backup | +| **Deploy** | Watch it happen, don't walk away | +| **Verify** | Trust but verify | +| **Confirm** | Have rollback trigger ready | + +--- + +## 4. Post-Deployment Verification + +### What to Verify + +| Check | Why | +|-------|-----| +| **Health endpoint** | Service is running | +| **Error logs** | No new errors | +| **Key user flows** | Critical features work | +| **Performance** | Response times acceptable | + +### Verification Window + +- **First 5 minutes**: Active monitoring +- **15 minutes**: Confirm stable +- **1 hour**: Final verification +- **Next day**: Review metrics + +--- + +## 5. Rollback Principles + +### When to Rollback + +| Symptom | Action | +|---------|--------| +| Service down | Rollback immediately | +| Critical errors | Rollback | +| Performance >50% degraded | Consider rollback | +| Minor issues | Fix forward if quick | + +### Rollback Strategy by Platform + +| Platform | Rollback Method | +|----------|----------------| +| **Vercel/Netlify** | Redeploy previous commit | +| **Railway/Render** | Rollback in dashboard | +| **VPS + PM2** | Restore backup, restart | +| **Docker** | Previous image tag | +| **K8s** | kubectl rollout undo | + +### Rollback Principles + +1. **Speed over perfection**: Rollback first, debug later +2. **Don't compound errors**: One rollback, not multiple changes +3. **Communicate**: Tell team what happened +4. **Post-mortem**: Understand why after stable + +--- + +## 6. Zero-Downtime Deployment + +### Strategies + +| Strategy | How It Works | +|----------|--------------| +| **Rolling** | Replace instances one by one | +| **Blue-Green** | Switch traffic between environments | +| **Canary** | Gradual traffic shift | + +### Selection Principles + +| Scenario | Strategy | +|----------|----------| +| Standard release | Rolling | +| High-risk change | Blue-green (easy rollback) | +| Need validation | Canary (test with real traffic) | + +--- + +## 7. Emergency Procedures + +### Service Down Priority + +1. **Assess**: What's the symptom? +2. **Quick fix**: Restart if unclear +3. **Rollback**: If restart doesn't help +4. **Investigate**: After stable + +### Investigation Order + +| Check | Common Issues | +|-------|--------------| +| **Logs** | Errors, exceptions | +| **Resources** | Disk full, memory | +| **Network** | DNS, firewall | +| **Dependencies** | Database, APIs | + +--- + +## 8. Anti-Patterns + +| โŒ Don't | โœ… Do | +|----------|-------| +| Deploy on Friday | Deploy early in week | +| Rush deployment | Follow the process | +| Skip staging | Always test first | +| Deploy without backup | Backup before deploy | +| Walk away after deploy | Monitor for 15+ min | +| Multiple changes at once | One change at a time | + +--- + +## 9. Decision Checklist + +Before deploying: + +- [ ] **Platform-appropriate procedure?** +- [ ] **Backup strategy ready?** +- [ ] **Rollback plan documented?** +- [ ] **Monitoring configured?** +- [ ] **Team notified?** +- [ ] **Time to monitor after?** + +--- + +## 10. Best Practices + +1. **Small, frequent deploys** over big releases +2. **Feature flags** for risky changes +3. **Automate** repetitive steps +4. **Document** every deployment +5. **Review** what went wrong after issues +6. **Test rollback** before you need it + +--- + +> **Remember:** Every deployment is a risk. Minimize risk through preparation, not speed. + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-devops-cloud/skills/docker-expert/SKILL.md b/plugins/antigravity-bundle-devops-cloud/skills/docker-expert/SKILL.md new file mode 100644 index 00000000..3319d3c8 --- /dev/null +++ b/plugins/antigravity-bundle-devops-cloud/skills/docker-expert/SKILL.md @@ -0,0 +1,413 @@ +--- +name: docker-expert +description: "You are an advanced Docker containerization expert with comprehensive, practical knowledge of container optimization, security hardening, multi-stage builds, orchestration patterns, and production deployment strategies based on current industry best practices." +category: devops +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Docker Expert + +You are an advanced Docker containerization expert with comprehensive, practical knowledge of container optimization, security hardening, multi-stage builds, orchestration patterns, and production deployment strategies based on current industry best practices. + +## When invoked: + +0. If the issue requires ultra-specific expertise outside Docker, recommend switching and stop: + - Kubernetes orchestration, pods, services, ingress โ†’ kubernetes-expert (future) + - GitHub Actions CI/CD with containers โ†’ github-actions-expert + - AWS ECS/Fargate or cloud-specific container services โ†’ devops-expert + - Database containerization with complex persistence โ†’ database-expert + + Example to output: + "This requires Kubernetes orchestration expertise. Please invoke: 'Use the kubernetes-expert subagent.' Stopping here." + +1. Analyze container setup comprehensively: + + **Use internal tools first (Read, Grep, Glob) for better performance. Shell commands are fallbacks.** + + ```bash + # Docker environment detection + docker --version 2>/dev/null || echo "No Docker installed" + docker info | grep -E "Server Version|Storage Driver|Container Runtime" 2>/dev/null + docker context ls 2>/dev/null | head -3 + + # Project structure analysis + find . -name "Dockerfile*" -type f | head -10 + find . -name "*compose*.yml" -o -name "*compose*.yaml" -type f | head -5 + find . -name ".dockerignore" -type f | head -3 + + # Container status if running + docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" 2>/dev/null | head -10 + docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" 2>/dev/null | head -10 + ``` + + **After detection, adapt approach:** + - Match existing Dockerfile patterns and base images + - Respect multi-stage build conventions + - Consider development vs production environments + - Account for existing orchestration setup (Compose/Swarm) + +2. Identify the specific problem category and complexity level + +3. Apply the appropriate solution strategy from my expertise + +4. Validate thoroughly: + ```bash + # Build and security validation + docker build --no-cache -t test-build . 2>/dev/null && echo "Build successful" + docker history test-build --no-trunc 2>/dev/null | head -5 + docker scout quickview test-build 2>/dev/null || echo "No Docker Scout" + + # Runtime validation + docker run --rm -d --name validation-test test-build 2>/dev/null + docker exec validation-test ps aux 2>/dev/null | head -3 + docker stop validation-test 2>/dev/null + + # Compose validation + docker-compose config 2>/dev/null && echo "Compose config valid" + ``` + +## Core Expertise Areas + +### 1. Dockerfile Optimization & Multi-Stage Builds + +**High-priority patterns I address:** +- **Layer caching optimization**: Separate dependency installation from source code copying +- **Multi-stage builds**: Minimize production image size while keeping build flexibility +- **Build context efficiency**: Comprehensive .dockerignore and build context management +- **Base image selection**: Alpine vs distroless vs scratch image strategies + +**Key techniques:** +```dockerfile +# Optimized multi-stage pattern +FROM node:18-alpine AS deps +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production && npm cache clean --force + +FROM node:18-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build && npm prune --production + +FROM node:18-alpine AS runtime +RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001 +WORKDIR /app +COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules +COPY --from=build --chown=nextjs:nodejs /app/dist ./dist +COPY --from=build --chown=nextjs:nodejs /app/package*.json ./ +USER nextjs +EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3000/health || exit 1 +CMD ["node", "dist/index.js"] +``` + +### 2. Container Security Hardening + +**Security focus areas:** +- **Non-root user configuration**: Proper user creation with specific UID/GID +- **Secrets management**: Docker secrets, build-time secrets, avoiding env vars +- **Base image security**: Regular updates, minimal attack surface +- **Runtime security**: Capability restrictions, resource limits + +**Security patterns:** +```dockerfile +# Security-hardened container +FROM node:18-alpine +RUN addgroup -g 1001 -S appgroup && \ + adduser -S appuser -u 1001 -G appgroup +WORKDIR /app +COPY --chown=appuser:appgroup package*.json ./ +RUN npm ci --only=production +COPY --chown=appuser:appgroup . . +USER 1001 +# Drop capabilities, set read-only root filesystem +``` + +### 3. Docker Compose Orchestration + +**Orchestration expertise:** +- **Service dependency management**: Health checks, startup ordering +- **Network configuration**: Custom networks, service discovery +- **Environment management**: Dev/staging/prod configurations +- **Volume strategies**: Named volumes, bind mounts, data persistence + +**Production-ready compose pattern:** +```yaml +version: '3.8' +services: + app: + build: + context: . + target: production + depends_on: + db: + condition: service_healthy + networks: + - frontend + - backend + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + deploy: + resources: + limits: + cpus: '0.5' + memory: 512M + reservations: + cpus: '0.25' + memory: 256M + + db: + image: postgres:15-alpine + environment: + POSTGRES_DB_FILE: /run/secrets/db_name + POSTGRES_USER_FILE: /run/secrets/db_user + POSTGRES_PASSWORD_FILE: /run/secrets/db_password + secrets: + - db_name + - db_user + - db_password + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - backend + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 10s + timeout: 5s + retries: 5 + +networks: + frontend: + driver: bridge + backend: + driver: bridge + internal: true + +volumes: + postgres_data: + +secrets: + db_name: + external: true + db_user: + external: true + db_password: + external: true +``` + +### 4. Image Size Optimization + +**Size reduction strategies:** +- **Distroless images**: Minimal runtime environments +- **Build artifact optimization**: Remove build tools and cache +- **Layer consolidation**: Combine RUN commands strategically +- **Multi-stage artifact copying**: Only copy necessary files + +**Optimization techniques:** +```dockerfile +# Minimal production image +FROM gcr.io/distroless/nodejs18-debian11 +COPY --from=build /app/dist /app +COPY --from=build /app/node_modules /app/node_modules +WORKDIR /app +EXPOSE 3000 +CMD ["index.js"] +``` + +### 5. Development Workflow Integration + +**Development patterns:** +- **Hot reloading setup**: Volume mounting and file watching +- **Debug configuration**: Port exposure and debugging tools +- **Testing integration**: Test-specific containers and environments +- **Development containers**: Remote development container support via CLI tools + +**Development workflow:** +```yaml +# Development override +services: + app: + build: + context: . + target: development + volumes: + - .:/app + - /app/node_modules + - /app/dist + environment: + - NODE_ENV=development + - DEBUG=app:* + ports: + - "9229:9229" # Debug port + command: npm run dev +``` + +### 6. Performance & Resource Management + +**Performance optimization:** +- **Resource limits**: CPU, memory constraints for stability +- **Build performance**: Parallel builds, cache utilization +- **Runtime performance**: Process management, signal handling +- **Monitoring integration**: Health checks, metrics exposure + +**Resource management:** +```yaml +services: + app: + deploy: + resources: + limits: + cpus: '1.0' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s +``` + +## Advanced Problem-Solving Patterns + +### Cross-Platform Builds +```bash +# Multi-architecture builds +docker buildx create --name multiarch-builder --use +docker buildx build --platform linux/amd64,linux/arm64 \ + -t myapp:latest --push . +``` + +### Build Cache Optimization +```dockerfile +# Mount build cache for package managers +FROM node:18-alpine AS deps +WORKDIR /app +COPY package*.json ./ +RUN --mount=type=cache,target=/root/.npm \ + npm ci --only=production +``` + +### Secrets Management +```dockerfile +# Build-time secrets (BuildKit) +FROM alpine +RUN --mount=type=secret,id=api_key \ + API_KEY=$(cat /run/secrets/api_key) && \ + # Use API_KEY for build process +``` + +### Health Check Strategies +```dockerfile +# Sophisticated health monitoring +COPY health-check.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/health-check.sh +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD ["/usr/local/bin/health-check.sh"] +``` + +## Code Review Checklist + +When reviewing Docker configurations, focus on: + +### Dockerfile Optimization & Multi-Stage Builds +- [ ] Dependencies copied before source code for optimal layer caching +- [ ] Multi-stage builds separate build and runtime environments +- [ ] Production stage only includes necessary artifacts +- [ ] Build context optimized with comprehensive .dockerignore +- [ ] Base image selection appropriate (Alpine vs distroless vs scratch) +- [ ] RUN commands consolidated to minimize layers where beneficial + +### Container Security Hardening +- [ ] Non-root user created with specific UID/GID (not default) +- [ ] Container runs as non-root user (USER directive) +- [ ] Secrets managed properly (not in ENV vars or layers) +- [ ] Base images kept up-to-date and scanned for vulnerabilities +- [ ] Minimal attack surface (only necessary packages installed) +- [ ] Health checks implemented for container monitoring + +### Docker Compose & Orchestration +- [ ] Service dependencies properly defined with health checks +- [ ] Custom networks configured for service isolation +- [ ] Environment-specific configurations separated (dev/prod) +- [ ] Volume strategies appropriate for data persistence needs +- [ ] Resource limits defined to prevent resource exhaustion +- [ ] Restart policies configured for production resilience + +### Image Size & Performance +- [ ] Final image size optimized (avoid unnecessary files/tools) +- [ ] Build cache optimization implemented +- [ ] Multi-architecture builds considered if needed +- [ ] Artifact copying selective (only required files) +- [ ] Package manager cache cleaned in same RUN layer + +### Development Workflow Integration +- [ ] Development targets separate from production +- [ ] Hot reloading configured properly with volume mounts +- [ ] Debug ports exposed when needed +- [ ] Environment variables properly configured for different stages +- [ ] Testing containers isolated from production builds + +### Networking & Service Discovery +- [ ] Port exposure limited to necessary services +- [ ] Service naming follows conventions for discovery +- [ ] Network security implemented (internal networks for backend) +- [ ] Load balancing considerations addressed +- [ ] Health check endpoints implemented and tested + +## Common Issue Diagnostics + +### Build Performance Issues +**Symptoms**: Slow builds (10+ minutes), frequent cache invalidation +**Root causes**: Poor layer ordering, large build context, no caching strategy +**Solutions**: Multi-stage builds, .dockerignore optimization, dependency caching + +### Security Vulnerabilities +**Symptoms**: Security scan failures, exposed secrets, root execution +**Root causes**: Outdated base images, hardcoded secrets, default user +**Solutions**: Regular base updates, secrets management, non-root configuration + +### Image Size Problems +**Symptoms**: Images over 1GB, deployment slowness +**Root causes**: Unnecessary files, build tools in production, poor base selection +**Solutions**: Distroless images, multi-stage optimization, artifact selection + +### Networking Issues +**Symptoms**: Service communication failures, DNS resolution errors +**Root causes**: Missing networks, port conflicts, service naming +**Solutions**: Custom networks, health checks, proper service discovery + +### Development Workflow Problems +**Symptoms**: Hot reload failures, debugging difficulties, slow iteration +**Root causes**: Volume mounting issues, port configuration, environment mismatch +**Solutions**: Development-specific targets, proper volume strategy, debug configuration + +## Integration & Handoff Guidelines + +**When to recommend other experts:** +- **Kubernetes orchestration** โ†’ kubernetes-expert: Pod management, services, ingress +- **CI/CD pipeline issues** โ†’ github-actions-expert: Build automation, deployment workflows +- **Database containerization** โ†’ database-expert: Complex persistence, backup strategies +- **Application-specific optimization** โ†’ Language experts: Code-level performance issues +- **Infrastructure automation** โ†’ devops-expert: Terraform, cloud-specific deployments + +**Collaboration patterns:** +- Provide Docker foundation for DevOps deployment automation +- Create optimized base images for language-specific experts +- Establish container standards for CI/CD integration +- Define security baselines for production orchestration + +I provide comprehensive Docker containerization expertise with focus on practical optimization, security hardening, and production-ready patterns. My solutions emphasize performance, maintainability, and security best practices for modern container workflows. + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-devops-cloud/skills/environment-setup-guide/SKILL.md b/plugins/antigravity-bundle-devops-cloud/skills/environment-setup-guide/SKILL.md new file mode 100644 index 00000000..a7c36e27 --- /dev/null +++ b/plugins/antigravity-bundle-devops-cloud/skills/environment-setup-guide/SKILL.md @@ -0,0 +1,482 @@ +--- +name: environment-setup-guide +description: "Guide developers through setting up development environments with proper tools, dependencies, and configurations" +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Environment Setup Guide + +## Overview + +Help developers set up complete development environments from scratch. This skill provides step-by-step guidance for installing tools, configuring dependencies, setting up environment variables, and verifying the setup works correctly. + +## When to Use This Skill + +- Use when starting a new project and need to set up the development environment +- Use when onboarding new team members to a project +- Use when switching to a new machine or operating system +- Use when troubleshooting environment-related issues +- Use when documenting setup instructions for a project +- Use when creating development environment documentation + +## How It Works + +### Step 1: Identify Requirements + +I'll help you determine what needs to be installed: +- Programming language and version (Node.js, Python, Go, etc.) +- Package managers (npm, pip, cargo, etc.) +- Database systems (PostgreSQL, MongoDB, Redis, etc.) +- Development tools (Git, Docker, IDE extensions, etc.) +- Environment variables and configuration files + +### Step 2: Check Current Setup + +Before installing anything, I'll help you check what's already installed: +```bash +# Check versions of installed tools +node --version +python --version +git --version +docker --version +``` + +### Step 3: Provide Installation Instructions + +I'll give platform-specific installation commands: +- **macOS:** Using Homebrew +- **Linux:** Using apt, yum, or package manager +- **Windows:** Using Chocolatey, Scoop, or direct installers + +### Step 4: Configure the Environment + +Help set up: +- Environment variables (.env files) +- Configuration files (.gitconfig, .npmrc, etc.) +- IDE settings (VS Code, IntelliJ, etc.) +- Shell configuration (.bashrc, .zshrc, etc.) + +### Step 5: Verify Installation + +Provide verification steps to ensure everything works: +- Run version checks +- Test basic commands +- Verify database connections +- Check environment variables are loaded + +## Examples + +### Example 1: Node.js Project Setup + +```markdown +## Setting Up Node.js Development Environment + +### Prerequisites +- macOS, Linux, or Windows +- Terminal/Command Prompt access +- Internet connection + +### Step 1: Install Node.js + +**macOS (using Homebrew):** +\`\`\`bash +# Install Homebrew if not installed +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + +# Install Node.js +brew install node +\`\`\` + +**Linux (Ubuntu/Debian):** +\`\`\`bash +# Update package list +sudo apt update + +# Install Node.js and npm +curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - +sudo apt install -y nodejs +\`\`\` + +**Windows (using Chocolatey):** +\`\`\`powershell +# Install Chocolatey if not installed +Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + +# Install Node.js +choco install nodejs +\`\`\` + +### Step 2: Verify Installation + +\`\`\`bash +node --version # Should show v20.x.x or higher +npm --version # Should show 10.x.x or higher +\`\`\` + +### Step 3: Install Project Dependencies + +\`\`\`bash +# Clone the repository +git clone https://github.com/your-repo/project.git +cd project + +# Install dependencies +npm install +\`\`\` + +### Step 4: Set Up Environment Variables + +Create a \`.env\` file: +\`\`\`bash +# Copy example environment file +cp .env.example .env + +# Edit with your values +nano .env +\`\`\` + +Example \`.env\` content: +\`\`\` +NODE_ENV=development +PORT=3000 +DATABASE_URL=postgresql://localhost:5432/mydb +API_KEY=your-api-key-here +\`\`\` + +### Step 5: Run the Project + +\`\`\`bash +# Start development server +npm run dev + +# Should see: Server running on http://localhost:3000 +\`\`\` + +### Troubleshooting + +**Problem:** "node: command not found" +**Solution:** Restart your terminal or run \`source ~/.bashrc\` (Linux) or \`source ~/.zshrc\` (macOS) + +**Problem:** "Permission denied" errors +**Solution:** Don't use sudo with npm. Fix permissions: +\`\`\`bash +mkdir ~/.npm-global +npm config set prefix '~/.npm-global' +echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc +source ~/.bashrc +\`\`\` +``` + +### Example 2: Python Project Setup + +```markdown +## Setting Up Python Development Environment + +### Step 1: Install Python + +**macOS:** +\`\`\`bash +brew install python@3.11 +\`\`\` + +**Linux:** +\`\`\`bash +sudo apt update +sudo apt install python3.11 python3.11-venv python3-pip +\`\`\` + +**Windows:** +\`\`\`powershell +choco install python --version=3.11 +\`\`\` + +### Step 2: Verify Installation + +\`\`\`bash +python3 --version # Should show Python 3.11.x +pip3 --version # Should show pip 23.x.x +\`\`\` + +### Step 3: Create Virtual Environment + +\`\`\`bash +# Navigate to project directory +cd my-project + +# Create virtual environment +python3 -m venv venv + +# Activate virtual environment +# macOS/Linux: +source venv/bin/activate + +# Windows: +venv\Scripts\activate +\`\`\` + +### Step 4: Install Dependencies + +\`\`\`bash +# Install from requirements.txt +pip install -r requirements.txt + +# Or install packages individually +pip install flask sqlalchemy python-dotenv +\`\`\` + +### Step 5: Set Up Environment Variables + +Create \`.env\` file: +\`\`\` +FLASK_APP=app.py +FLASK_ENV=development +DATABASE_URL=sqlite:///app.db +SECRET_KEY=your-secret-key-here +\`\`\` + +### Step 6: Run the Application + +\`\`\`bash +# Run Flask app +flask run + +# Should see: Running on http://127.0.0.1:5000 +\`\`\` +``` + +### Example 3: Docker Development Environment + +```markdown +## Setting Up Docker Development Environment + +### Step 1: Install Docker + +**macOS:** +\`\`\`bash +brew install --cask docker +# Or download Docker Desktop from docker.com +\`\`\` + +**Linux:** +\`\`\`bash +# Install Docker +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh + +# Add user to docker group +sudo usermod -aG docker $USER +newgrp docker +\`\`\` + +**Windows:** +Download Docker Desktop from docker.com + +### Step 2: Verify Installation + +\`\`\`bash +docker --version # Should show Docker version 24.x.x +docker-compose --version # Should show Docker Compose version 2.x.x +\`\`\` + +### Step 3: Create docker-compose.yml + +\`\`\`yaml +version: '3.8' + +services: + app: + build: . + ports: + - "3000:3000" + environment: + - NODE_ENV=development + - DATABASE_URL=postgresql://postgres:password@db:5432/mydb + volumes: + - .:/app + - /app/node_modules + depends_on: + - db + + db: + image: postgres:15 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_DB=mydb + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: +\`\`\` + +### Step 4: Start Services + +\`\`\`bash +# Build and start containers +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop services +docker-compose down +\`\`\` + +### Step 5: Verify Services + +\`\`\`bash +# Check running containers +docker ps + +# Test database connection +docker-compose exec db psql -U postgres -d mydb +\`\`\` +``` + +## Best Practices + +### โœ… Do This + +- **Document Everything** - Write clear setup instructions +- **Use Version Managers** - nvm for Node, pyenv for Python +- **Create .env.example** - Show required environment variables +- **Test on Clean System** - Verify instructions work from scratch +- **Include Troubleshooting** - Document common issues and solutions +- **Use Docker** - For consistent environments across machines +- **Pin Versions** - Specify exact versions in package files +- **Automate Setup** - Create setup scripts when possible +- **Check Prerequisites** - List required tools before starting +- **Provide Verification Steps** - Help users confirm setup works + +### โŒ Don't Do This + +- **Don't Assume Tools Installed** - Always check and provide install instructions +- **Don't Skip Environment Variables** - Document all required variables +- **Don't Use Sudo with npm** - Fix permissions instead +- **Don't Forget Platform Differences** - Provide OS-specific instructions +- **Don't Leave Out Verification** - Always include test steps +- **Don't Use Global Installs** - Prefer local/virtual environments +- **Don't Ignore Errors** - Document how to handle common errors +- **Don't Skip Database Setup** - Include database initialization steps + +## Common Pitfalls + +### Problem: "Command not found" after installation +**Symptoms:** Installed tool but terminal doesn't recognize it +**Solution:** +- Restart terminal or source shell config +- Check PATH environment variable +- Verify installation location +```bash +# Check PATH +echo $PATH + +# Add to PATH (example) +export PATH="/usr/local/bin:$PATH" +``` + +### Problem: Permission errors with npm/pip +**Symptoms:** "EACCES" or "Permission denied" errors +**Solution:** +- Don't use sudo +- Fix npm permissions or use nvm +- Use virtual environments for Python +```bash +# Fix npm permissions +mkdir ~/.npm-global +npm config set prefix '~/.npm-global' +echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc +``` + +### Problem: Port already in use +**Symptoms:** "Port 3000 is already in use" +**Solution:** +- Find and kill process using the port +- Use a different port +```bash +# Find process on port 3000 +lsof -i :3000 + +# Kill process +kill -9 + +# Or use different port +PORT=3001 npm start +``` + +### Problem: Database connection fails +**Symptoms:** "Connection refused" or "Authentication failed" +**Solution:** +- Verify database is running +- Check connection string +- Verify credentials +```bash +# Check if PostgreSQL is running +sudo systemctl status postgresql + +# Test connection +psql -h localhost -U postgres -d mydb +``` + +## Setup Script Template + +Create a `setup.sh` script to automate setup: + +```bash +#!/bin/bash + +echo "๐Ÿš€ Setting up development environment..." + +# Check prerequisites +command -v node >/dev/null 2>&1 || { echo "โŒ Node.js not installed"; exit 1; } +command -v git >/dev/null 2>&1 || { echo "โŒ Git not installed"; exit 1; } + +echo "โœ… Prerequisites check passed" + +# Install dependencies +echo "๐Ÿ“ฆ Installing dependencies..." +npm install + +# Copy environment file +if [ ! -f .env ]; then + echo "๐Ÿ“ Creating .env file..." + cp .env.example .env + echo "โš ๏ธ Please edit .env with your configuration" +fi + +# Run database migrations +echo "๐Ÿ—„๏ธ Running database migrations..." +npm run migrate + +# Verify setup +echo "๐Ÿ” Verifying setup..." +npm run test:setup + +echo "โœ… Setup complete! Run 'npm run dev' to start" +``` + +## Related Skills + +- `@brainstorming` - Plan environment requirements before setup +- `@systematic-debugging` - Debug environment issues +- `@doc-coauthoring` - Create setup documentation +- `@git-pushing` - Set up Git configuration + +## Additional Resources + +- [Node.js Installation Guide](https://nodejs.org/en/download/) +- [Python Virtual Environments](https://docs.python.org/3/tutorial/venv.html) +- [Docker Documentation](https://docs.docker.com/get-started/) +- [Homebrew (macOS)](https://brew.sh/) +- [Chocolatey (Windows)](https://chocolatey.org/) +- [nvm (Node Version Manager)](https://github.com/nvm-sh/nvm) +- [pyenv (Python Version Manager)](https://github.com/pyenv/pyenv) + +--- + +**Pro Tip:** Create a `setup.sh` or `setup.ps1` script to automate the entire setup process. Test it on a clean system to ensure it works! diff --git a/plugins/antigravity-bundle-devops-cloud/skills/kubernetes-architect/SKILL.md b/plugins/antigravity-bundle-devops-cloud/skills/kubernetes-architect/SKILL.md new file mode 100644 index 00000000..22c1eb01 --- /dev/null +++ b/plugins/antigravity-bundle-devops-cloud/skills/kubernetes-architect/SKILL.md @@ -0,0 +1,165 @@ +--- +name: kubernetes-architect +description: Expert Kubernetes architect specializing in cloud-native infrastructure, advanced GitOps workflows (ArgoCD/Flux), and enterprise container orchestration. +risk: unknown +source: community +date_added: '2026-02-27' +--- +You are a Kubernetes architect specializing in cloud-native infrastructure, modern GitOps workflows, and enterprise container orchestration at scale. + +## Use this skill when + +- Designing Kubernetes platform architecture or multi-cluster strategy +- Implementing GitOps workflows and progressive delivery +- Planning service mesh, security, or multi-tenancy patterns +- Improving reliability, cost, or developer experience in K8s + +## Do not use this skill when + +- You only need a local dev cluster or single-node setup +- You are troubleshooting application code without platform changes +- You are not using Kubernetes or container orchestration + +## Instructions + +1. Gather workload requirements, compliance needs, and scale targets. +2. Define cluster topology, networking, and security boundaries. +3. Choose GitOps tooling and delivery strategy for rollouts. +4. Validate with staging and define rollback and upgrade plans. + +## Safety + +- Avoid production changes without approvals and rollback plans. +- Test policy changes and admission controls in staging first. + +## Purpose +Expert Kubernetes architect with comprehensive knowledge of container orchestration, cloud-native technologies, and modern GitOps practices. Masters Kubernetes across all major providers (EKS, AKS, GKE) and on-premises deployments. Specializes in building scalable, secure, and cost-effective platform engineering solutions that enhance developer productivity. + +## Capabilities + +### Kubernetes Platform Expertise +- **Managed Kubernetes**: EKS (AWS), AKS (Azure), GKE (Google Cloud), advanced configuration and optimization +- **Enterprise Kubernetes**: Red Hat OpenShift, Rancher, VMware Tanzu, platform-specific features +- **Self-managed clusters**: kubeadm, kops, kubespray, bare-metal installations, air-gapped deployments +- **Cluster lifecycle**: Upgrades, node management, etcd operations, backup/restore strategies +- **Multi-cluster management**: Cluster API, fleet management, cluster federation, cross-cluster networking + +### GitOps & Continuous Deployment +- **GitOps tools**: ArgoCD, Flux v2, Jenkins X, Tekton, advanced configuration and best practices +- **OpenGitOps principles**: Declarative, versioned, automatically pulled, continuously reconciled +- **Progressive delivery**: Argo Rollouts, Flagger, canary deployments, blue/green strategies, A/B testing +- **GitOps repository patterns**: App-of-apps, mono-repo vs multi-repo, environment promotion strategies +- **Secret management**: External Secrets Operator, Sealed Secrets, HashiCorp Vault integration + +### Modern Infrastructure as Code +- **Kubernetes-native IaC**: Helm 3.x, Kustomize, Jsonnet, cdk8s, Pulumi Kubernetes provider +- **Cluster provisioning**: Terraform/OpenTofu modules, Cluster API, infrastructure automation +- **Configuration management**: Advanced Helm patterns, Kustomize overlays, environment-specific configs +- **Policy as Code**: Open Policy Agent (OPA), Gatekeeper, Kyverno, Falco rules, admission controllers +- **GitOps workflows**: Automated testing, validation pipelines, drift detection and remediation + +### Cloud-Native Security +- **Pod Security Standards**: Restricted, baseline, privileged policies, migration strategies +- **Network security**: Network policies, service mesh security, micro-segmentation +- **Runtime security**: Falco, Sysdig, Aqua Security, runtime threat detection +- **Image security**: Container scanning, admission controllers, vulnerability management +- **Supply chain security**: SLSA, Sigstore, image signing, SBOM generation +- **Compliance**: CIS benchmarks, NIST frameworks, regulatory compliance automation + +### Service Mesh Architecture +- **Istio**: Advanced traffic management, security policies, observability, multi-cluster mesh +- **Linkerd**: Lightweight service mesh, automatic mTLS, traffic splitting +- **Cilium**: eBPF-based networking, network policies, load balancing +- **Consul Connect**: Service mesh with HashiCorp ecosystem integration +- **Gateway API**: Next-generation ingress, traffic routing, protocol support + +### Container & Image Management +- **Container runtimes**: containerd, CRI-O, Docker runtime considerations +- **Registry strategies**: Harbor, ECR, ACR, GCR, multi-region replication +- **Image optimization**: Multi-stage builds, distroless images, security scanning +- **Build strategies**: BuildKit, Cloud Native Buildpacks, Tekton pipelines, Kaniko +- **Artifact management**: OCI artifacts, Helm chart repositories, policy distribution + +### Observability & Monitoring +- **Metrics**: Prometheus, VictoriaMetrics, Thanos for long-term storage +- **Logging**: Fluentd, Fluent Bit, Loki, centralized logging strategies +- **Tracing**: Jaeger, Zipkin, OpenTelemetry, distributed tracing patterns +- **Visualization**: Grafana, custom dashboards, alerting strategies +- **APM integration**: DataDog, New Relic, Dynatrace Kubernetes-specific monitoring + +### Multi-Tenancy & Platform Engineering +- **Namespace strategies**: Multi-tenancy patterns, resource isolation, network segmentation +- **RBAC design**: Advanced authorization, service accounts, cluster roles, namespace roles +- **Resource management**: Resource quotas, limit ranges, priority classes, QoS classes +- **Developer platforms**: Self-service provisioning, developer portals, abstract infrastructure complexity +- **Operator development**: Custom Resource Definitions (CRDs), controller patterns, Operator SDK + +### Scalability & Performance +- **Cluster autoscaling**: Horizontal Pod Autoscaler (HPA), Vertical Pod Autoscaler (VPA), Cluster Autoscaler +- **Custom metrics**: KEDA for event-driven autoscaling, custom metrics APIs +- **Performance tuning**: Node optimization, resource allocation, CPU/memory management +- **Load balancing**: Ingress controllers, service mesh load balancing, external load balancers +- **Storage**: Persistent volumes, storage classes, CSI drivers, data management + +### Cost Optimization & FinOps +- **Resource optimization**: Right-sizing workloads, spot instances, reserved capacity +- **Cost monitoring**: KubeCost, OpenCost, native cloud cost allocation +- **Bin packing**: Node utilization optimization, workload density +- **Cluster efficiency**: Resource requests/limits optimization, over-provisioning analysis +- **Multi-cloud cost**: Cross-provider cost analysis, workload placement optimization + +### Disaster Recovery & Business Continuity +- **Backup strategies**: Velero, cloud-native backup solutions, cross-region backups +- **Multi-region deployment**: Active-active, active-passive, traffic routing +- **Chaos engineering**: Chaos Monkey, Litmus, fault injection testing +- **Recovery procedures**: RTO/RPO planning, automated failover, disaster recovery testing + +## OpenGitOps Principles (CNCF) +1. **Declarative** - Entire system described declaratively with desired state +2. **Versioned and Immutable** - Desired state stored in Git with complete version history +3. **Pulled Automatically** - Software agents automatically pull desired state from Git +4. **Continuously Reconciled** - Agents continuously observe and reconcile actual vs desired state + +## Behavioral Traits +- Champions Kubernetes-first approaches while recognizing appropriate use cases +- Implements GitOps from project inception, not as an afterthought +- Prioritizes developer experience and platform usability +- Emphasizes security by default with defense in depth strategies +- Designs for multi-cluster and multi-region resilience +- Advocates for progressive delivery and safe deployment practices +- Focuses on cost optimization and resource efficiency +- Promotes observability and monitoring as foundational capabilities +- Values automation and Infrastructure as Code for all operations +- Considers compliance and governance requirements in architecture decisions + +## Knowledge Base +- Kubernetes architecture and component interactions +- CNCF landscape and cloud-native technology ecosystem +- GitOps patterns and best practices +- Container security and supply chain best practices +- Service mesh architectures and trade-offs +- Platform engineering methodologies +- Cloud provider Kubernetes services and integrations +- Observability patterns and tools for containerized environments +- Modern CI/CD practices and pipeline security + +## Response Approach +1. **Assess workload requirements** for container orchestration needs +2. **Design Kubernetes architecture** appropriate for scale and complexity +3. **Implement GitOps workflows** with proper repository structure and automation +4. **Configure security policies** with Pod Security Standards and network policies +5. **Set up observability stack** with metrics, logs, and traces +6. **Plan for scalability** with appropriate autoscaling and resource management +7. **Consider multi-tenancy** requirements and namespace isolation +8. **Optimize for cost** with right-sizing and efficient resource utilization +9. **Document platform** with clear operational procedures and developer guides + +## Example Interactions +- "Design a multi-cluster Kubernetes platform with GitOps for a financial services company" +- "Implement progressive delivery with Argo Rollouts and service mesh traffic splitting" +- "Create a secure multi-tenant Kubernetes platform with namespace isolation and RBAC" +- "Design disaster recovery for stateful applications across multiple Kubernetes clusters" +- "Optimize Kubernetes costs while maintaining performance and availability SLAs" +- "Implement observability stack with Prometheus, Grafana, and OpenTelemetry for microservices" +- "Create CI/CD pipeline with GitOps for container applications with security scanning" +- "Design Kubernetes operator for custom application lifecycle management" diff --git a/plugins/antigravity-bundle-devops-cloud/skills/terraform-specialist/SKILL.md b/plugins/antigravity-bundle-devops-cloud/skills/terraform-specialist/SKILL.md new file mode 100644 index 00000000..de9aa73a --- /dev/null +++ b/plugins/antigravity-bundle-devops-cloud/skills/terraform-specialist/SKILL.md @@ -0,0 +1,162 @@ +--- +name: terraform-specialist +description: Expert Terraform/OpenTofu specialist mastering advanced IaC automation, state management, and enterprise infrastructure patterns. +risk: unknown +source: community +date_added: '2026-02-27' +--- +You are a Terraform/OpenTofu specialist focused on advanced infrastructure automation, state management, and modern IaC practices. + +## Use this skill when + +- Designing Terraform/OpenTofu modules or environments +- Managing state backends, workspaces, or multi-cloud stacks +- Implementing policy-as-code and CI/CD automation for IaC + +## Do not use this skill when + +- You only need a one-off manual infrastructure change +- You are locked to a different IaC tool or platform +- You cannot store or secure state remotely + +## Instructions + +1. Define environments, providers, and security constraints. +2. Design modules and choose a remote state backend. +3. Implement plan/apply workflows with reviews and policies. +4. Validate drift, costs, and rollback strategies. + +## Safety + +- Always review plans before applying changes. +- Protect state files and avoid exposing secrets. + +## Purpose +Expert Infrastructure as Code specialist with comprehensive knowledge of Terraform, OpenTofu, and modern IaC ecosystems. Masters advanced module design, state management, provider development, and enterprise-scale infrastructure automation. Specializes in GitOps workflows, policy as code, and complex multi-cloud deployments. + +## Capabilities + +### Terraform/OpenTofu Expertise +- **Core concepts**: Resources, data sources, variables, outputs, locals, expressions +- **Advanced features**: Dynamic blocks, for_each loops, conditional expressions, complex type constraints +- **State management**: Remote backends, state locking, state encryption, workspace strategies +- **Module development**: Composition patterns, versioning strategies, testing frameworks +- **Provider ecosystem**: Official and community providers, custom provider development +- **OpenTofu migration**: Terraform to OpenTofu migration strategies, compatibility considerations + +### Advanced Module Design +- **Module architecture**: Hierarchical module design, root modules, child modules +- **Composition patterns**: Module composition, dependency injection, interface segregation +- **Reusability**: Generic modules, environment-specific configurations, module registries +- **Testing**: Terratest, unit testing, integration testing, contract testing +- **Documentation**: Auto-generated documentation, examples, usage patterns +- **Versioning**: Semantic versioning, compatibility matrices, upgrade guides + +### State Management & Security +- **Backend configuration**: S3, Azure Storage, GCS, Terraform Cloud, Consul, etcd +- **State encryption**: Encryption at rest, encryption in transit, key management +- **State locking**: DynamoDB, Azure Storage, GCS, Redis locking mechanisms +- **State operations**: Import, move, remove, refresh, advanced state manipulation +- **Backup strategies**: Automated backups, point-in-time recovery, state versioning +- **Security**: Sensitive variables, secret management, state file security + +### Multi-Environment Strategies +- **Workspace patterns**: Terraform workspaces vs separate backends +- **Environment isolation**: Directory structure, variable management, state separation +- **Deployment strategies**: Environment promotion, blue/green deployments +- **Configuration management**: Variable precedence, environment-specific overrides +- **GitOps integration**: Branch-based workflows, automated deployments + +### Provider & Resource Management +- **Provider configuration**: Version constraints, multiple providers, provider aliases +- **Resource lifecycle**: Creation, updates, destruction, import, replacement +- **Data sources**: External data integration, computed values, dependency management +- **Resource targeting**: Selective operations, resource addressing, bulk operations +- **Drift detection**: Continuous compliance, automated drift correction +- **Resource graphs**: Dependency visualization, parallelization optimization + +### Advanced Configuration Techniques +- **Dynamic configuration**: Dynamic blocks, complex expressions, conditional logic +- **Templating**: Template functions, file interpolation, external data integration +- **Validation**: Variable validation, precondition/postcondition checks +- **Error handling**: Graceful failure handling, retry mechanisms, recovery strategies +- **Performance optimization**: Resource parallelization, provider optimization + +### CI/CD & Automation +- **Pipeline integration**: GitHub Actions, GitLab CI, Azure DevOps, Jenkins +- **Automated testing**: Plan validation, policy checking, security scanning +- **Deployment automation**: Automated apply, approval workflows, rollback strategies +- **Policy as Code**: Open Policy Agent (OPA), Sentinel, custom validation +- **Security scanning**: tfsec, Checkov, Terrascan, custom security policies +- **Quality gates**: Pre-commit hooks, continuous validation, compliance checking + +### Multi-Cloud & Hybrid +- **Multi-cloud patterns**: Provider abstraction, cloud-agnostic modules +- **Hybrid deployments**: On-premises integration, edge computing, hybrid connectivity +- **Cross-provider dependencies**: Resource sharing, data passing between providers +- **Cost optimization**: Resource tagging, cost estimation, optimization recommendations +- **Migration strategies**: Cloud-to-cloud migration, infrastructure modernization + +### Modern IaC Ecosystem +- **Alternative tools**: Pulumi, AWS CDK, Azure Bicep, Google Deployment Manager +- **Complementary tools**: Helm, Kustomize, Ansible integration +- **State alternatives**: Stateless deployments, immutable infrastructure patterns +- **GitOps workflows**: ArgoCD, Flux integration, continuous reconciliation +- **Policy engines**: OPA/Gatekeeper, native policy frameworks + +### Enterprise & Governance +- **Access control**: RBAC, team-based access, service account management +- **Compliance**: SOC2, PCI-DSS, HIPAA infrastructure compliance +- **Auditing**: Change tracking, audit trails, compliance reporting +- **Cost management**: Resource tagging, cost allocation, budget enforcement +- **Service catalogs**: Self-service infrastructure, approved module catalogs + +### Troubleshooting & Operations +- **Debugging**: Log analysis, state inspection, resource investigation +- **Performance tuning**: Provider optimization, parallelization, resource batching +- **Error recovery**: State corruption recovery, failed apply resolution +- **Monitoring**: Infrastructure drift monitoring, change detection +- **Maintenance**: Provider updates, module upgrades, deprecation management + +## Behavioral Traits +- Follows DRY principles with reusable, composable modules +- Treats state files as critical infrastructure requiring protection +- Always plans before applying with thorough change review +- Implements version constraints for reproducible deployments +- Prefers data sources over hardcoded values for flexibility +- Advocates for automated testing and validation in all workflows +- Emphasizes security best practices for sensitive data and state management +- Designs for multi-environment consistency and scalability +- Values clear documentation and examples for all modules +- Considers long-term maintenance and upgrade strategies + +## Knowledge Base +- Terraform/OpenTofu syntax, functions, and best practices +- Major cloud provider services and their Terraform representations +- Infrastructure patterns and architectural best practices +- CI/CD tools and automation strategies +- Security frameworks and compliance requirements +- Modern development workflows and GitOps practices +- Testing frameworks and quality assurance approaches +- Monitoring and observability for infrastructure + +## Response Approach +1. **Analyze infrastructure requirements** for appropriate IaC patterns +2. **Design modular architecture** with proper abstraction and reusability +3. **Configure secure backends** with appropriate locking and encryption +4. **Implement comprehensive testing** with validation and security checks +5. **Set up automation pipelines** with proper approval workflows +6. **Document thoroughly** with examples and operational procedures +7. **Plan for maintenance** with upgrade strategies and deprecation handling +8. **Consider compliance requirements** and governance needs +9. **Optimize for performance** and cost efficiency + +## Example Interactions +- "Design a reusable Terraform module for a three-tier web application with proper testing" +- "Set up secure remote state management with encryption and locking for multi-team environment" +- "Create CI/CD pipeline for infrastructure deployment with security scanning and approval workflows" +- "Migrate existing Terraform codebase to OpenTofu with minimal disruption" +- "Implement policy as code validation for infrastructure compliance and cost control" +- "Design multi-cloud Terraform architecture with provider abstraction" +- "Troubleshoot state corruption and implement recovery procedures" +- "Create enterprise service catalog with approved infrastructure modules" diff --git a/plugins/antigravity-bundle-documents-presentations/.codex-plugin/plugin.json b/plugins/antigravity-bundle-documents-presentations/.codex-plugin/plugin.json new file mode 100644 index 00000000..10a9f1a0 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-documents-presentations", + "version": "8.10.0", + "description": "Install the \"Documents & Presentations\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "documents-presentations", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Documents & Presentations", + "shortDescription": "Specialized Packs ยท 7 curated skills", + "longDescription": "For document-heavy workflows, spreadsheets, PDFs, and presentations. Covers Office Productivity, DOCX Official, and 5 more skills.", + "developerName": "sickn33 and contributors", + "category": "Specialized Packs", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/LICENSE.txt b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/LICENSE.txt new file mode 100644 index 00000000..c55ab422 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/LICENSE.txt @@ -0,0 +1,30 @@ +ยฉ 2025 Anthropic, PBC. All rights reserved. + +LICENSE: Use of these materials (including all code, prompts, assets, files, +and other components of this Skill) is governed by your agreement with +Anthropic regarding use of Anthropic's services. If no separate agreement +exists, use is governed by Anthropic's Consumer Terms of Service or +Commercial Terms of Service, as applicable: +https://www.anthropic.com/legal/consumer-terms +https://www.anthropic.com/legal/commercial-terms +Your applicable agreement is referred to as the "Agreement." "Services" are +as defined in the Agreement. + +ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the +contrary, users may not: + +- Extract these materials from the Services or retain copies of these + materials outside the Services +- Reproduce or copy these materials, except for temporary copies created + automatically during authorized use of the Services +- Create derivative works based on these materials +- Distribute, sublicense, or transfer these materials to any third party +- Make, offer to sell, sell, or import any inventions embodied in these + materials +- Reverse engineer, decompile, or disassemble these materials + +The receipt, viewing, or possession of these materials does not convey or +imply any license or right beyond those expressly granted above. + +Anthropic retains all right, title, and interest in these materials, +including all copyrights, patents, and other intellectual property rights. diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/SKILL.md b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/SKILL.md new file mode 100644 index 00000000..00ee72f8 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/SKILL.md @@ -0,0 +1,202 @@ +--- +name: docx-official +description: "A user may ask you to create, edit, or analyze the contents of a .docx file. A .docx file is essentially a ZIP archive containing XML files and other resources that you can read or edit. You have different tools and workflows available for different tasks." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# DOCX creation, editing, and analysis + +## Overview + +A user may ask you to create, edit, or analyze the contents of a .docx file. A .docx file is essentially a ZIP archive containing XML files and other resources that you can read or edit. You have different tools and workflows available for different tasks. + +## Workflow Decision Tree + +### Reading/Analyzing Content +Use "Text extraction" or "Raw XML access" sections below + +### Creating New Document +Use "Creating a new Word document" workflow + +### Editing Existing Document +- **Your own document + simple changes** + Use "Basic OOXML editing" workflow + +- **Someone else's document** + Use **"Redlining workflow"** (recommended default) + +- **Legal, academic, business, or government docs** + Use **"Redlining workflow"** (required) + +## Reading and analyzing content + +### Text extraction +If you just need to read the text contents of a document, you should convert the document to markdown using pandoc. Pandoc provides excellent support for preserving document structure and can show tracked changes: + +```bash +# Convert document to markdown with tracked changes +pandoc --track-changes=all path-to-file.docx -o output.md +# Options: --track-changes=accept/reject/all +``` + +### Raw XML access +You need raw XML access for: comments, complex formatting, document structure, embedded media, and metadata. For any of these features, you'll need to unpack a document and read its raw XML contents. + +#### Unpacking a file +`python ooxml/scripts/unpack.py ` + +#### Key file structures +* `word/document.xml` - Main document contents +* `word/comments.xml` - Comments referenced in document.xml +* `word/media/` - Embedded images and media files +* Tracked changes use `` (insertions) and `` (deletions) tags + +## Creating a new Word document + +When creating a new Word document from scratch, use **docx-js**, which allows you to create Word documents using JavaScript/TypeScript. + +### Workflow +1. **MANDATORY - READ ENTIRE FILE**: Read [`docx-js.md`](docx-js.md) (~500 lines) completely from start to finish. **NEVER set any range limits when reading this file.** Read the full file content for detailed syntax, critical formatting rules, and best practices before proceeding with document creation. +2. Create a JavaScript/TypeScript file using Document, Paragraph, TextRun components (You can assume all dependencies are installed, but if not, refer to the dependencies section below) +3. Export as .docx using Packer.toBuffer() + +## Editing an existing Word document + +When editing an existing Word document, use the **Document library** (a Python library for OOXML manipulation). The library automatically handles infrastructure setup and provides methods for document manipulation. For complex scenarios, you can access the underlying DOM directly through the library. + +### Workflow +1. **MANDATORY - READ ENTIRE FILE**: Read [`ooxml.md`](ooxml.md) (~600 lines) completely from start to finish. **NEVER set any range limits when reading this file.** Read the full file content for the Document library API and XML patterns for directly editing document files. +2. Unpack the document: `python ooxml/scripts/unpack.py ` +3. Create and run a Python script using the Document library (see "Document Library" section in ooxml.md) +4. Pack the final document: `python ooxml/scripts/pack.py ` + +The Document library provides both high-level methods for common operations and direct DOM access for complex scenarios. + +## Redlining workflow for document review + +This workflow allows you to plan comprehensive tracked changes using markdown before implementing them in OOXML. **CRITICAL**: For complete tracked changes, you must implement ALL changes systematically. + +**Batching Strategy**: Group related changes into batches of 3-10 changes. This makes debugging manageable while maintaining efficiency. Test each batch before moving to the next. + +**Principle: Minimal, Precise Edits** +When implementing tracked changes, only mark text that actually changes. Repeating unchanged text makes edits harder to review and appears unprofessional. Break replacements into: [unchanged text] + [deletion] + [insertion] + [unchanged text]. Preserve the original run's RSID for unchanged text by extracting the `` element from the original and reusing it. + +Example - Changing "30 days" to "60 days" in a sentence: +```python +# BAD - Replaces entire sentence +'The term is 30 days.The term is 60 days.' + +# GOOD - Only marks what changed, preserves original for unchanged text +'The term is 3060 days.' +``` + +### Tracked changes workflow + +1. **Get markdown representation**: Convert document to markdown with tracked changes preserved: + ```bash + pandoc --track-changes=all path-to-file.docx -o current.md + ``` + +2. **Identify and group changes**: Review the document and identify ALL changes needed, organizing them into logical batches: + + **Location methods** (for finding changes in XML): + - Section/heading numbers (e.g., "Section 3.2", "Article IV") + - Paragraph identifiers if numbered + - Grep patterns with unique surrounding text + - Document structure (e.g., "first paragraph", "signature block") + - **DO NOT use markdown line numbers** - they don't map to XML structure + + **Batch organization** (group 3-10 related changes per batch): + - By section: "Batch 1: Section 2 amendments", "Batch 2: Section 5 updates" + - By type: "Batch 1: Date corrections", "Batch 2: Party name changes" + - By complexity: Start with simple text replacements, then tackle complex structural changes + - Sequential: "Batch 1: Pages 1-3", "Batch 2: Pages 4-6" + +3. **Read documentation and unpack**: + - **MANDATORY - READ ENTIRE FILE**: Read [`ooxml.md`](ooxml.md) (~600 lines) completely from start to finish. **NEVER set any range limits when reading this file.** Pay special attention to the "Document Library" and "Tracked Change Patterns" sections. + - **Unpack the document**: `python ooxml/scripts/unpack.py ` + - **Note the suggested RSID**: The unpack script will suggest an RSID to use for your tracked changes. Copy this RSID for use in step 4b. + +4. **Implement changes in batches**: Group changes logically (by section, by type, or by proximity) and implement them together in a single script. This approach: + - Makes debugging easier (smaller batch = easier to isolate errors) + - Allows incremental progress + - Maintains efficiency (batch size of 3-10 changes works well) + + **Suggested batch groupings:** + - By document section (e.g., "Section 3 changes", "Definitions", "Termination clause") + - By change type (e.g., "Date changes", "Party name updates", "Legal term replacements") + - By proximity (e.g., "Changes on pages 1-3", "Changes in first half of document") + + For each batch of related changes: + + **a. Map text to XML**: Grep for text in `word/document.xml` to verify how text is split across `` elements. + + **b. Create and run script**: Use `get_node` to find nodes, implement changes, then `doc.save()`. See **"Document Library"** section in ooxml.md for patterns. + + **Note**: Always grep `word/document.xml` immediately before writing a script to get current line numbers and verify text content. Line numbers change after each script run. + +5. **Pack the document**: After all batches are complete, convert the unpacked directory back to .docx: + ```bash + python ooxml/scripts/pack.py unpacked reviewed-document.docx + ``` + +6. **Final verification**: Do a comprehensive check of the complete document: + - Convert final document to markdown: + ```bash + pandoc --track-changes=all reviewed-document.docx -o verification.md + ``` + - Verify ALL changes were applied correctly: + ```bash + grep "original phrase" verification.md # Should NOT find it + grep "replacement phrase" verification.md # Should find it + ``` + - Check that no unintended changes were introduced + + +## Converting Documents to Images + +To visually analyze Word documents, convert them to images using a two-step process: + +1. **Convert DOCX to PDF**: + ```bash + soffice --headless --convert-to pdf document.docx + ``` + +2. **Convert PDF pages to JPEG images**: + ```bash + pdftoppm -jpeg -r 150 document.pdf page + ``` + This creates files like `page-1.jpg`, `page-2.jpg`, etc. + +Options: +- `-r 150`: Sets resolution to 150 DPI (adjust for quality/size balance) +- `-jpeg`: Output JPEG format (use `-png` for PNG if preferred) +- `-f N`: First page to convert (e.g., `-f 2` starts from page 2) +- `-l N`: Last page to convert (e.g., `-l 5` stops at page 5) +- `page`: Prefix for output files + +Example for specific range: +```bash +pdftoppm -jpeg -r 150 -f 2 -l 5 document.pdf page # Converts only pages 2-5 +``` + +## Code Style Guidelines +**IMPORTANT**: When generating code for DOCX operations: +- Write concise code +- Avoid verbose variable names and redundant operations +- Avoid unnecessary print statements + +## Dependencies + +Required dependencies (install if not available): + +- **pandoc**: `sudo apt-get install pandoc` (for text extraction) +- **docx**: `npm install -g docx` (for creating new documents) +- **LibreOffice**: `sudo apt-get install libreoffice` (for PDF conversion) +- **Poppler**: `sudo apt-get install poppler-utils` (for pdftoppm to convert PDF to images) +- **defusedxml**: `pip install defusedxml` (for secure XML parsing) + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/docx-js.md b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/docx-js.md new file mode 100644 index 00000000..c6d7b2dd --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/docx-js.md @@ -0,0 +1,350 @@ +# DOCX Library Tutorial + +Generate .docx files with JavaScript/TypeScript. + +**Important: Read this entire document before starting.** Critical formatting rules and common pitfalls are covered throughout - skipping sections may result in corrupted files or rendering issues. + +## Setup +Assumes docx is already installed globally +If not installed: `npm install -g docx` + +```javascript +const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun, Media, + Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink, + InternalHyperlink, TableOfContents, HeadingLevel, BorderStyle, WidthType, TabStopType, + TabStopPosition, UnderlineType, ShadingType, VerticalAlign, SymbolRun, PageNumber, + FootnoteReferenceRun, Footnote, PageBreak } = require('docx'); + +// Create & Save +const doc = new Document({ sections: [{ children: [/* content */] }] }); +Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer)); // Node.js +Packer.toBlob(doc).then(blob => { /* download logic */ }); // Browser +``` + +## Text & Formatting +```javascript +// IMPORTANT: Never use \n for line breaks - always use separate Paragraph elements +// โŒ WRONG: new TextRun("Line 1\nLine 2") +// โœ… CORRECT: new Paragraph({ children: [new TextRun("Line 1")] }), new Paragraph({ children: [new TextRun("Line 2")] }) + +// Basic text with all formatting options +new Paragraph({ + alignment: AlignmentType.CENTER, + spacing: { before: 200, after: 200 }, + indent: { left: 720, right: 720 }, + children: [ + new TextRun({ text: "Bold", bold: true }), + new TextRun({ text: "Italic", italics: true }), + new TextRun({ text: "Underlined", underline: { type: UnderlineType.DOUBLE, color: "FF0000" } }), + new TextRun({ text: "Colored", color: "FF0000", size: 28, font: "Arial" }), // Arial default + new TextRun({ text: "Highlighted", highlight: "yellow" }), + new TextRun({ text: "Strikethrough", strike: true }), + new TextRun({ text: "x2", superScript: true }), + new TextRun({ text: "H2O", subScript: true }), + new TextRun({ text: "SMALL CAPS", smallCaps: true }), + new SymbolRun({ char: "2022", font: "Symbol" }), // Bullet โ€ข + new SymbolRun({ char: "00A9", font: "Arial" }) // Copyright ยฉ - Arial for symbols + ] +}) +``` + +## Styles & Professional Formatting + +```javascript +const doc = new Document({ + styles: { + default: { document: { run: { font: "Arial", size: 24 } } }, // 12pt default + paragraphStyles: [ + // Document title style - override built-in Title style + { id: "Title", name: "Title", basedOn: "Normal", + run: { size: 56, bold: true, color: "000000", font: "Arial" }, + paragraph: { spacing: { before: 240, after: 120 }, alignment: AlignmentType.CENTER } }, + // IMPORTANT: Override built-in heading styles by using their exact IDs + { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, + run: { size: 32, bold: true, color: "000000", font: "Arial" }, // 16pt + paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, // Required for TOC + { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true, + run: { size: 28, bold: true, color: "000000", font: "Arial" }, // 14pt + paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } }, + // Custom styles use your own IDs + { id: "myStyle", name: "My Style", basedOn: "Normal", + run: { size: 28, bold: true, color: "000000" }, + paragraph: { spacing: { after: 120 }, alignment: AlignmentType.CENTER } } + ], + characterStyles: [{ id: "myCharStyle", name: "My Char Style", + run: { color: "FF0000", bold: true, underline: { type: UnderlineType.SINGLE } } }] + }, + sections: [{ + properties: { page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } }, + children: [ + new Paragraph({ heading: HeadingLevel.TITLE, children: [new TextRun("Document Title")] }), // Uses overridden Title style + new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Heading 1")] }), // Uses overridden Heading1 style + new Paragraph({ style: "myStyle", children: [new TextRun("Custom paragraph style")] }), + new Paragraph({ children: [ + new TextRun("Normal with "), + new TextRun({ text: "custom char style", style: "myCharStyle" }) + ]}) + ] + }] +}); +``` + +**Professional Font Combinations:** +- **Arial (Headers) + Arial (Body)** - Most universally supported, clean and professional +- **Times New Roman (Headers) + Arial (Body)** - Classic serif headers with modern sans-serif body +- **Georgia (Headers) + Verdana (Body)** - Optimized for screen reading, elegant contrast + +**Key Styling Principles:** +- **Override built-in styles**: Use exact IDs like "Heading1", "Heading2", "Heading3" to override Word's built-in heading styles +- **HeadingLevel constants**: `HeadingLevel.HEADING_1` uses "Heading1" style, `HeadingLevel.HEADING_2` uses "Heading2" style, etc. +- **Include outlineLevel**: Set `outlineLevel: 0` for H1, `outlineLevel: 1` for H2, etc. to ensure TOC works correctly +- **Use custom styles** instead of inline formatting for consistency +- **Set a default font** using `styles.default.document.run.font` - Arial is universally supported +- **Establish visual hierarchy** with different font sizes (titles > headers > body) +- **Add proper spacing** with `before` and `after` paragraph spacing +- **Use colors sparingly**: Default to black (000000) and shades of gray for titles and headings (heading 1, heading 2, etc.) +- **Set consistent margins** (1440 = 1 inch is standard) + + +## Lists (ALWAYS USE PROPER LISTS - NEVER USE UNICODE BULLETS) +```javascript +// Bullets - ALWAYS use the numbering config, NOT unicode symbols +// CRITICAL: Use LevelFormat.BULLET constant, NOT the string "bullet" +const doc = new Document({ + numbering: { + config: [ + { reference: "bullet-list", + levels: [{ level: 0, format: LevelFormat.BULLET, text: "โ€ข", alignment: AlignmentType.LEFT, + style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }, + { reference: "first-numbered-list", + levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT, + style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }, + { reference: "second-numbered-list", // Different reference = restarts at 1 + levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT, + style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] } + ] + }, + sections: [{ + children: [ + // Bullet list items + new Paragraph({ numbering: { reference: "bullet-list", level: 0 }, + children: [new TextRun("First bullet point")] }), + new Paragraph({ numbering: { reference: "bullet-list", level: 0 }, + children: [new TextRun("Second bullet point")] }), + // Numbered list items + new Paragraph({ numbering: { reference: "first-numbered-list", level: 0 }, + children: [new TextRun("First numbered item")] }), + new Paragraph({ numbering: { reference: "first-numbered-list", level: 0 }, + children: [new TextRun("Second numbered item")] }), + // โš ๏ธ CRITICAL: Different reference = INDEPENDENT list that restarts at 1 + // Same reference = CONTINUES previous numbering + new Paragraph({ numbering: { reference: "second-numbered-list", level: 0 }, + children: [new TextRun("Starts at 1 again (because different reference)")] }) + ] + }] +}); + +// โš ๏ธ CRITICAL NUMBERING RULE: Each reference creates an INDEPENDENT numbered list +// - Same reference = continues numbering (1, 2, 3... then 4, 5, 6...) +// - Different reference = restarts at 1 (1, 2, 3... then 1, 2, 3...) +// Use unique reference names for each separate numbered section! + +// โš ๏ธ CRITICAL: NEVER use unicode bullets - they create fake lists that don't work properly +// new TextRun("โ€ข Item") // WRONG +// new SymbolRun({ char: "2022" }) // WRONG +// โœ… ALWAYS use numbering config with LevelFormat.BULLET for real Word lists +``` + +## Tables +```javascript +// Complete table with margins, borders, headers, and bullet points +const tableBorder = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" }; +const cellBorders = { top: tableBorder, bottom: tableBorder, left: tableBorder, right: tableBorder }; + +new Table({ + columnWidths: [4680, 4680], // โš ๏ธ CRITICAL: Set column widths at table level - values in DXA (twentieths of a point) + margins: { top: 100, bottom: 100, left: 180, right: 180 }, // Set once for all cells + rows: [ + new TableRow({ + tableHeader: true, + children: [ + new TableCell({ + borders: cellBorders, + width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell + // โš ๏ธ CRITICAL: Always use ShadingType.CLEAR to prevent black backgrounds in Word. + shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, + verticalAlign: VerticalAlign.CENTER, + children: [new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "Header", bold: true, size: 22 })] + })] + }), + new TableCell({ + borders: cellBorders, + width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell + shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, + children: [new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "Bullet Points", bold: true, size: 22 })] + })] + }) + ] + }), + new TableRow({ + children: [ + new TableCell({ + borders: cellBorders, + width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell + children: [new Paragraph({ children: [new TextRun("Regular data")] })] + }), + new TableCell({ + borders: cellBorders, + width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell + children: [ + new Paragraph({ + numbering: { reference: "bullet-list", level: 0 }, + children: [new TextRun("First bullet point")] + }), + new Paragraph({ + numbering: { reference: "bullet-list", level: 0 }, + children: [new TextRun("Second bullet point")] + }) + ] + }) + ] + }) + ] +}) +``` + +**IMPORTANT: Table Width & Borders** +- Use BOTH `columnWidths: [width1, width2, ...]` array AND `width: { size: X, type: WidthType.DXA }` on each cell +- Values in DXA (twentieths of a point): 1440 = 1 inch, Letter usable width = 9360 DXA (with 1" margins) +- Apply borders to individual `TableCell` elements, NOT the `Table` itself + +**Precomputed Column Widths (Letter size with 1" margins = 9360 DXA total):** +- **2 columns:** `columnWidths: [4680, 4680]` (equal width) +- **3 columns:** `columnWidths: [3120, 3120, 3120]` (equal width) + +## Links & Navigation +```javascript +// TOC (requires headings) - CRITICAL: Use HeadingLevel only, NOT custom styles +// โŒ WRONG: new Paragraph({ heading: HeadingLevel.HEADING_1, style: "customHeader", children: [new TextRun("Title")] }) +// โœ… CORRECT: new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Title")] }) +new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" }), + +// External link +new Paragraph({ + children: [new ExternalHyperlink({ + children: [new TextRun({ text: "Google", style: "Hyperlink" })], + link: "https://www.google.com" + })] +}), + +// Internal link & bookmark +new Paragraph({ + children: [new InternalHyperlink({ + children: [new TextRun({ text: "Go to Section", style: "Hyperlink" })], + anchor: "section1" + })] +}), +new Paragraph({ + children: [new TextRun("Section Content")], + bookmark: { id: "section1", name: "section1" } +}), +``` + +## Images & Media +```javascript +// Basic image with sizing & positioning +// CRITICAL: Always specify 'type' parameter - it's REQUIRED for ImageRun +new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new ImageRun({ + type: "png", // NEW REQUIREMENT: Must specify image type (png, jpg, jpeg, gif, bmp, svg) + data: fs.readFileSync("image.png"), + transformation: { width: 200, height: 150, rotation: 0 }, // rotation in degrees + altText: { title: "Logo", description: "Company logo", name: "Name" } // IMPORTANT: All three fields are required + })] +}) +``` + +## Page Breaks +```javascript +// Manual page break +new Paragraph({ children: [new PageBreak()] }), + +// Page break before paragraph +new Paragraph({ + pageBreakBefore: true, + children: [new TextRun("This starts on a new page")] +}) + +// โš ๏ธ CRITICAL: NEVER use PageBreak standalone - it will create invalid XML that Word cannot open +// โŒ WRONG: new PageBreak() +// โœ… CORRECT: new Paragraph({ children: [new PageBreak()] }) +``` + +## Headers/Footers & Page Setup +```javascript +const doc = new Document({ + sections: [{ + properties: { + page: { + margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, // 1440 = 1 inch + size: { orientation: PageOrientation.LANDSCAPE }, + pageNumbers: { start: 1, formatType: "decimal" } // "upperRoman", "lowerRoman", "upperLetter", "lowerLetter" + } + }, + headers: { + default: new Header({ children: [new Paragraph({ + alignment: AlignmentType.RIGHT, + children: [new TextRun("Header Text")] + })] }) + }, + footers: { + default: new Footer({ children: [new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] }), new TextRun(" of "), new TextRun({ children: [PageNumber.TOTAL_PAGES] })] + })] }) + }, + children: [/* content */] + }] +}); +``` + +## Tabs +```javascript +new Paragraph({ + tabStops: [ + { type: TabStopType.LEFT, position: TabStopPosition.MAX / 4 }, + { type: TabStopType.CENTER, position: TabStopPosition.MAX / 2 }, + { type: TabStopType.RIGHT, position: TabStopPosition.MAX * 3 / 4 } + ], + children: [new TextRun("Left\tCenter\tRight")] +}) +``` + +## Constants & Quick Reference +- **Underlines:** `SINGLE`, `DOUBLE`, `WAVY`, `DASH` +- **Borders:** `SINGLE`, `DOUBLE`, `DASHED`, `DOTTED` +- **Numbering:** `DECIMAL` (1,2,3), `UPPER_ROMAN` (I,II,III), `LOWER_LETTER` (a,b,c) +- **Tabs:** `LEFT`, `CENTER`, `RIGHT`, `DECIMAL` +- **Symbols:** `"2022"` (โ€ข), `"00A9"` (ยฉ), `"00AE"` (ยฎ), `"2122"` (โ„ข), `"00B0"` (ยฐ), `"F070"` (โœ“), `"F0FC"` (โœ—) + +## Critical Issues & Common Mistakes +- **CRITICAL: PageBreak must ALWAYS be inside a Paragraph** - standalone PageBreak creates invalid XML that Word cannot open +- **ALWAYS use ShadingType.CLEAR for table cell shading** - Never use ShadingType.SOLID (causes black background). +- Measurements in DXA (1440 = 1 inch) | Each table cell needs โ‰ฅ1 Paragraph | TOC requires HeadingLevel styles only +- **ALWAYS use custom styles** with Arial font for professional appearance and proper visual hierarchy +- **ALWAYS set a default font** using `styles.default.document.run.font` - Arial recommended +- **ALWAYS use columnWidths array for tables** + individual cell widths for compatibility +- **NEVER use unicode symbols for bullets** - always use proper numbering configuration with `LevelFormat.BULLET` constant (NOT the string "bullet") +- **NEVER use \n for line breaks anywhere** - always use separate Paragraph elements for each line +- **ALWAYS use TextRun objects within Paragraph children** - never use text property directly on Paragraph +- **CRITICAL for images**: ImageRun REQUIRES `type` parameter - always specify "png", "jpg", "jpeg", "gif", "bmp", or "svg" +- **CRITICAL for bullets**: Must use `LevelFormat.BULLET` constant, not string "bullet", and include `text: "โ€ข"` for the bullet character +- **CRITICAL for numbering**: Each numbering reference creates an INDEPENDENT list. Same reference = continues numbering (1,2,3 then 4,5,6). Different reference = restarts at 1 (1,2,3 then 1,2,3). Use unique reference names for each separate numbered section! +- **CRITICAL for TOC**: When using TableOfContents, headings must use HeadingLevel ONLY - do NOT add custom styles to heading paragraphs or TOC will break +- **Tables**: Set `columnWidths` array + individual cell widths, apply borders to cells not table +- **Set table margins at TABLE level** for consistent cell padding (avoids repetition per cell) \ No newline at end of file diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml.md b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml.md new file mode 100644 index 00000000..7677e7b8 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml.md @@ -0,0 +1,610 @@ +# Office Open XML Technical Reference + +**Important: Read this entire document before starting.** This document covers: +- [Technical Guidelines](#technical-guidelines) - Schema compliance rules and validation requirements +- [Document Content Patterns](#document-content-patterns) - XML patterns for headings, lists, tables, formatting, etc. +- [Document Library (Python)](#document-library-python) - Recommended approach for OOXML manipulation with automatic infrastructure setup +- [Tracked Changes (Redlining)](#tracked-changes-redlining) - XML patterns for implementing tracked changes + +## Technical Guidelines + +### Schema Compliance +- **Element ordering in ``**: ``, ``, ``, ``, `` +- **Whitespace**: Add `xml:space='preserve'` to `` elements with leading/trailing spaces +- **Unicode**: Escape characters in ASCII content: `"` becomes `“` + - **Character encoding reference**: Curly quotes `""` become `“”`, apostrophe `'` becomes `’`, em-dash `โ€”` becomes `—` +- **Tracked changes**: Use `` and `` tags with `w:author="Claude"` outside `` elements + - **Critical**: `` closes with ``, `` closes with `` - never mix + - **RSIDs must be 8-digit hex**: Use values like `00AB1234` (only 0-9, A-F characters) + - **trackRevisions placement**: Add `` after `` in settings.xml +- **Images**: Add to `word/media/`, reference in `document.xml`, set dimensions to prevent overflow + +## Document Content Patterns + +### Basic Structure +```xml + + Text content + +``` + +### Headings and Styles +```xml + + + + + + Document Title + + + + + Section Heading + +``` + +### Text Formatting +```xml + +Bold + +Italic + +Underlined + +Highlighted +``` + +### Lists +```xml + + + + + + + + First item + + + + + + + + + + New list item 1 + + + + + + + + + + + Bullet item + +``` + +### Tables +```xml + + + + + + + + + + + + Cell 1 + + + + Cell 2 + + + +``` + +### Layout +```xml + + + + + + + + + + + + New Section Title + + + + + + + + + + Centered text + + + + + + + + Monospace text + + + + + + + This text is Courier New + + and this text uses default font + +``` + +## File Updates + +When adding content, update these files: + +**`word/_rels/document.xml.rels`:** +```xml + + +``` + +**`[Content_Types].xml`:** +```xml + + +``` + +### Images +**CRITICAL**: Calculate dimensions to prevent page overflow and maintain aspect ratio. + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### Links (Hyperlinks) + +**IMPORTANT**: All hyperlinks (both internal and external) require the Hyperlink style to be defined in styles.xml. Without this style, links will look like regular text instead of blue underlined clickable links. + +**External Links:** +```xml + + + + + Link Text + + + + + +``` + +**Internal Links:** + +```xml + + + + + Link Text + + + + + +Target content + +``` + +**Hyperlink Style (required in styles.xml):** +```xml + + + + + + + + + + +``` + +## Document Library (Python) + +Use the Document class from `scripts/document.py` for all tracked changes and comments. It automatically handles infrastructure setup (people.xml, RSIDs, settings.xml, comment files, relationships, content types). Only use direct XML manipulation for complex scenarios not supported by the library. + +**Working with Unicode and Entities:** +- **Searching**: Both entity notation and Unicode characters work - `contains="“Company"` and `contains="\u201cCompany"` find the same text +- **Replacing**: Use either entities (`“`) or Unicode (`\u201c`) - both work and will be converted appropriately based on the file's encoding (ascii โ†’ entities, utf-8 โ†’ Unicode) + +### Initialization + +**Find the docx skill root** (directory containing `scripts/` and `ooxml/`): +```bash +# Search for document.py to locate the skill root +# Note: /mnt/skills is used here as an example; check your context for the actual location +find /mnt/skills -name "document.py" -path "*/docx/scripts/*" 2>/dev/null | head -1 +# Example output: /mnt/skills/docx/scripts/document.py +# Skill root is: /mnt/skills/docx +``` + +**Run your script with PYTHONPATH** set to the docx skill root: +```bash +PYTHONPATH=/mnt/skills/docx python your_script.py +``` + +**In your script**, import from the skill root: +```python +from scripts.document import Document, DocxXMLEditor + +# Basic initialization (automatically creates temp copy and sets up infrastructure) +doc = Document('unpacked') + +# Customize author and initials +doc = Document('unpacked', author="John Doe", initials="JD") + +# Enable track revisions mode +doc = Document('unpacked', track_revisions=True) + +# Specify custom RSID (auto-generated if not provided) +doc = Document('unpacked', rsid="07DC5ECB") +``` + +### Creating Tracked Changes + +**CRITICAL**: Only mark text that actually changes. Keep ALL unchanged text outside ``/`` tags. Marking unchanged text makes edits unprofessional and harder to review. + +**Attribute Handling**: The Document class auto-injects attributes (w:id, w:date, w:rsidR, w:rsidDel, w16du:dateUtc, xml:space) into new elements. When preserving unchanged text from the original document, copy the original `` element with its existing attributes to maintain document integrity. + +**Method Selection Guide**: +- **Adding your own changes to regular text**: Use `replace_node()` with ``/`` tags, or `suggest_deletion()` for removing entire `` or `` elements +- **Partially modifying another author's tracked change**: Use `replace_node()` to nest your changes inside their ``/`` +- **Completely rejecting another author's insertion**: Use `revert_insertion()` on the `` element (NOT `suggest_deletion()`) +- **Completely rejecting another author's deletion**: Use `revert_deletion()` on the `` element to restore deleted content using tracked changes + +```python +# Minimal edit - change one word: "The report is monthly" โ†’ "The report is quarterly" +# Original: The report is monthly +node = doc["word/document.xml"].get_node(tag="w:r", contains="The report is monthly") +rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else "" +replacement = f'{rpr}The report is {rpr}monthly{rpr}quarterly' +doc["word/document.xml"].replace_node(node, replacement) + +# Minimal edit - change number: "within 30 days" โ†’ "within 45 days" +# Original: within 30 days +node = doc["word/document.xml"].get_node(tag="w:r", contains="within 30 days") +rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else "" +replacement = f'{rpr}within {rpr}30{rpr}45{rpr} days' +doc["word/document.xml"].replace_node(node, replacement) + +# Complete replacement - preserve formatting even when replacing all text +node = doc["word/document.xml"].get_node(tag="w:r", contains="apple") +rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else "" +replacement = f'{rpr}apple{rpr}banana orange' +doc["word/document.xml"].replace_node(node, replacement) + +# Insert new content (no attributes needed - auto-injected) +node = doc["word/document.xml"].get_node(tag="w:r", contains="existing text") +doc["word/document.xml"].insert_after(node, 'new text') + +# Partially delete another author's insertion +# Original: quarterly financial report +# Goal: Delete only "financial" to make it "quarterly report" +node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"}) +# IMPORTANT: Preserve w:author="Jane Smith" on the outer to maintain authorship +replacement = ''' + quarterly + financial + report +''' +doc["word/document.xml"].replace_node(node, replacement) + +# Change part of another author's insertion +# Original: in silence, safe and sound +# Goal: Change "safe and sound" to "soft and unbound" +node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "8"}) +replacement = f''' + in silence, + + + soft and unbound + + + safe and sound +''' +doc["word/document.xml"].replace_node(node, replacement) + +# Delete entire run (use only when deleting all content; use replace_node for partial deletions) +node = doc["word/document.xml"].get_node(tag="w:r", contains="text to delete") +doc["word/document.xml"].suggest_deletion(node) + +# Delete entire paragraph (in-place, handles both regular and numbered list paragraphs) +para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph to delete") +doc["word/document.xml"].suggest_deletion(para) + +# Add new numbered list item +target_para = doc["word/document.xml"].get_node(tag="w:p", contains="existing list item") +pPr = tags[0].toxml() if (tags := target_para.getElementsByTagName("w:pPr")) else "" +new_item = f'{pPr}New item' +tracked_para = DocxXMLEditor.suggest_paragraph(new_item) +doc["word/document.xml"].insert_after(target_para, tracked_para) +# Optional: add spacing paragraph before content for better visual separation +# spacing = DocxXMLEditor.suggest_paragraph('') +# doc["word/document.xml"].insert_after(target_para, spacing + tracked_para) +``` + +### Adding Comments + +```python +# Add comment spanning two existing tracked changes +# Note: w:id is auto-generated. Only search by w:id if you know it from XML inspection +start_node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"}) +end_node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "2"}) +doc.add_comment(start=start_node, end=end_node, text="Explanation of this change") + +# Add comment on a paragraph +para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text") +doc.add_comment(start=para, end=para, text="Comment on this paragraph") + +# Add comment on newly created tracked change +# First create the tracked change +node = doc["word/document.xml"].get_node(tag="w:r", contains="old") +new_nodes = doc["word/document.xml"].replace_node( + node, + 'oldnew' +) +# Then add comment on the newly created elements +# new_nodes[0] is the , new_nodes[1] is the +doc.add_comment(start=new_nodes[0], end=new_nodes[1], text="Changed old to new per requirements") + +# Reply to existing comment +doc.reply_to_comment(parent_comment_id=0, text="I agree with this change") +``` + +### Rejecting Tracked Changes + +**IMPORTANT**: Use `revert_insertion()` to reject insertions and `revert_deletion()` to restore deletions using tracked changes. Use `suggest_deletion()` only for regular unmarked content. + +```python +# Reject insertion (wraps it in deletion) +# Use this when another author inserted text that you want to delete +ins = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"}) +nodes = doc["word/document.xml"].revert_insertion(ins) # Returns [ins] + +# Reject deletion (creates insertion to restore deleted content) +# Use this when another author deleted text that you want to restore +del_elem = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "3"}) +nodes = doc["word/document.xml"].revert_deletion(del_elem) # Returns [del_elem, new_ins] + +# Reject all insertions in a paragraph +para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text") +nodes = doc["word/document.xml"].revert_insertion(para) # Returns [para] + +# Reject all deletions in a paragraph +para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text") +nodes = doc["word/document.xml"].revert_deletion(para) # Returns [para] +``` + +### Inserting Images + +**CRITICAL**: The Document class works with a temporary copy at `doc.unpacked_path`. Always copy images to this temp directory, not the original unpacked folder. + +```python +from PIL import Image +import shutil, os + +# Initialize document first +doc = Document('unpacked') + +# Copy image and calculate full-width dimensions with aspect ratio +media_dir = os.path.join(doc.unpacked_path, 'word/media') +os.makedirs(media_dir, exist_ok=True) +shutil.copy('image.png', os.path.join(media_dir, 'image1.png')) +img = Image.open(os.path.join(media_dir, 'image1.png')) +width_emus = int(6.5 * 914400) # 6.5" usable width, 914400 EMUs/inch +height_emus = int(width_emus * img.size[1] / img.size[0]) + +# Add relationship and content type +rels_editor = doc['word/_rels/document.xml.rels'] +next_rid = rels_editor.get_next_rid() +rels_editor.append_to(rels_editor.dom.documentElement, + f'') +doc['[Content_Types].xml'].append_to(doc['[Content_Types].xml'].dom.documentElement, + '') + +# Insert image +node = doc["word/document.xml"].get_node(tag="w:p", line_number=100) +doc["word/document.xml"].insert_after(node, f''' + + + + + + + + + + + + + + + + + +''') +``` + +### Getting Nodes + +```python +# By text content +node = doc["word/document.xml"].get_node(tag="w:p", contains="specific text") + +# By line range +para = doc["word/document.xml"].get_node(tag="w:p", line_number=range(100, 150)) + +# By attributes +node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"}) + +# By exact line number (must be line number where tag opens) +para = doc["word/document.xml"].get_node(tag="w:p", line_number=42) + +# Combine filters +node = doc["word/document.xml"].get_node(tag="w:r", line_number=range(40, 60), contains="text") + +# Disambiguate when text appears multiple times - add line_number range +node = doc["word/document.xml"].get_node(tag="w:r", contains="Section", line_number=range(2400, 2500)) +``` + +### Saving + +```python +# Save with automatic validation (copies back to original directory) +doc.save() # Validates by default, raises error if validation fails + +# Save to different location +doc.save('modified-unpacked') + +# Skip validation (debugging only - needing this in production indicates XML issues) +doc.save(validate=False) +``` + +### Direct DOM Manipulation + +For complex scenarios not covered by the library: + +```python +# Access any XML file +editor = doc["word/document.xml"] +editor = doc["word/comments.xml"] + +# Direct DOM access (defusedxml.minidom.Document) +node = doc["word/document.xml"].get_node(tag="w:p", line_number=5) +parent = node.parentNode +parent.removeChild(node) +parent.appendChild(node) # Move to end + +# General document manipulation (without tracked changes) +old_node = doc["word/document.xml"].get_node(tag="w:p", contains="original text") +doc["word/document.xml"].replace_node(old_node, "replacement text") + +# Multiple insertions - use return value to maintain order +node = doc["word/document.xml"].get_node(tag="w:r", line_number=100) +nodes = doc["word/document.xml"].insert_after(node, "A") +nodes = doc["word/document.xml"].insert_after(nodes[-1], "B") +nodes = doc["word/document.xml"].insert_after(nodes[-1], "C") +# Results in: original_node, A, B, C +``` + +## Tracked Changes (Redlining) + +**Use the Document class above for all tracked changes.** The patterns below are for reference when constructing replacement XML strings. + +### Validation Rules +The validator checks that the document text matches the original after reverting Claude's changes. This means: +- **NEVER modify text inside another author's `` or `` tags** +- **ALWAYS use nested deletions** to remove another author's insertions +- **Every edit must be properly tracked** with `` or `` tags + +### Tracked Change Patterns + +**CRITICAL RULES**: +1. Never modify the content inside another author's tracked changes. Always use nested deletions. +2. **XML Structure**: Always place `` and `` at paragraph level containing complete `` elements. Never nest inside `` elements - this creates invalid XML that breaks document processing. + +**Text Insertion:** +```xml + + + inserted text + + +``` + +**Text Deletion:** +```xml + + + deleted text + + +``` + +**Deleting Another Author's Insertion (MUST use nested structure):** +```xml + + + + monthly + + + + weekly + +``` + +**Restoring Another Author's Deletion:** +```xml + + + within 30 days + + + within 30 days + +``` \ No newline at end of file diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file mode 100644 index 00000000..6454ef9a --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd @@ -0,0 +1,1499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file mode 100644 index 00000000..afa4f463 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file mode 100644 index 00000000..64e66b8a --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd @@ -0,0 +1,1085 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file mode 100644 index 00000000..687eea82 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd @@ -0,0 +1,11 @@ + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file mode 100644 index 00000000..6ac81b06 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd @@ -0,0 +1,3081 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file mode 100644 index 00000000..1dbf0514 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file mode 100644 index 00000000..f1af17db --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file mode 100644 index 00000000..0a185ab6 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd new file mode 100644 index 00000000..14ef4888 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd @@ -0,0 +1,1676 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file mode 100644 index 00000000..c20f3bf1 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file mode 100644 index 00000000..ac602522 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file mode 100644 index 00000000..424b8ba8 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file mode 100644 index 00000000..2bddce29 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file mode 100644 index 00000000..8a8c18ba --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file mode 100644 index 00000000..5c42706a --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file mode 100644 index 00000000..853c341c --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file mode 100644 index 00000000..da835ee8 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file mode 100644 index 00000000..87ad2658 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file mode 100644 index 00000000..9e86f1b2 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd new file mode 100644 index 00000000..d0be42e7 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd @@ -0,0 +1,4439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file mode 100644 index 00000000..8821dd18 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd @@ -0,0 +1,570 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file mode 100644 index 00000000..ca2575c7 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file mode 100644 index 00000000..dd079e60 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file mode 100644 index 00000000..3dd6cf62 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file mode 100644 index 00000000..f1041e34 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd new file mode 100644 index 00000000..9c5b7a63 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd @@ -0,0 +1,3646 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd new file mode 100644 index 00000000..0f13678d --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd @@ -0,0 +1,116 @@ + + + + + + See http://www.w3.org/XML/1998/namespace.html and + http://www.w3.org/TR/REC-xml for information about this namespace. + + This schema document describes the XML namespace, in a form + suitable for import by other schema documents. + + Note that local names in this namespace are intended to be defined + only by the World Wide Web Consortium or its subgroups. The + following names are currently defined in this namespace and should + not be used with conflicting semantics by any Working Group, + specification, or document instance: + + base (as an attribute name): denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification. + + lang (as an attribute name): denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification. + + space (as an attribute name): denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification. + + Father (in any context at all): denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: + + In appreciation for his vision, leadership and dedication + the W3C XML Plenary on this 10th day of February, 2000 + reserves for Jon Bosak in perpetuity the XML name + xml:Father + + + + + This schema defines attributes and an attribute group + suitable for use by + schemas wishing to allow xml:base, xml:lang or xml:space attributes + on elements they define. + + To enable this, such a schema must import this schema + for the XML namespace, e.g. as follows: + <schema . . .> + . . . + <import namespace="http://www.w3.org/XML/1998/namespace" + schemaLocation="http://www.w3.org/2001/03/xml.xsd"/> + + Subsequently, qualified reference to any of the attributes + or the group defined below will have the desired effect, e.g. + + <type . . .> + . . . + <attributeGroup ref="xml:specialAttrs"/> + + will define a type which will schema-validate an instance + element with any of those attributes + + + + In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + http://www.w3.org/2001/03/xml.xsd. + At the date of issue it can also be found at + http://www.w3.org/2001/xml.xsd. + The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML Schema + itself. In other words, if the XML Schema namespace changes, the version + of this document at + http://www.w3.org/2001/xml.xsd will change + accordingly; the version at + http://www.w3.org/2001/03/xml.xsd will not change. + + + + + + In due course, we should install the relevant ISO 2- and 3-letter + codes as the enumerated possible values . . . + + + + + + + + + + + + + + + See http://www.w3.org/TR/xmlbase/ for + information about this attribute. + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file mode 100644 index 00000000..a6de9d27 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd @@ -0,0 +1,42 @@ +๏ปฟ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file mode 100644 index 00000000..10e978b6 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd @@ -0,0 +1,50 @@ +๏ปฟ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd new file mode 100644 index 00000000..4248bf7a --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd new file mode 100644 index 00000000..56497467 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd @@ -0,0 +1,33 @@ +๏ปฟ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/mce/mc.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/mce/mc.xsd new file mode 100644 index 00000000..ef725457 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/mce/mc.xsd @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-2010.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-2010.xsd new file mode 100644 index 00000000..f65f7777 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-2010.xsd @@ -0,0 +1,560 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-2012.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-2012.xsd new file mode 100644 index 00000000..6b00755a --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-2012.xsd @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-2018.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-2018.xsd new file mode 100644 index 00000000..f321d333 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-2018.xsd @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-cex-2018.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-cex-2018.xsd new file mode 100644 index 00000000..364c6a9b --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-cex-2018.xsd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-cid-2016.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-cid-2016.xsd new file mode 100644 index 00000000..fed9d15b --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-cid-2016.xsd @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd new file mode 100644 index 00000000..680cf154 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-symex-2015.xsd b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-symex-2015.xsd new file mode 100644 index 00000000..89ada908 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/schemas/microsoft/wml-symex-2015.xsd @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/pack.py b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/pack.py new file mode 100755 index 00000000..68bc0886 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/pack.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +""" +Tool to pack a directory into a .docx, .pptx, or .xlsx file with XML formatting undone. + +Example usage: + python pack.py [--force] +""" + +import argparse +import shutil +import subprocess +import sys +import tempfile +import defusedxml.minidom +import zipfile +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser(description="Pack a directory into an Office file") + parser.add_argument("input_directory", help="Unpacked Office document directory") + parser.add_argument("output_file", help="Output Office file (.docx/.pptx/.xlsx)") + parser.add_argument("--force", action="store_true", help="Skip validation") + args = parser.parse_args() + + try: + success = pack_document( + args.input_directory, args.output_file, validate=not args.force + ) + + # Show warning if validation was skipped + if args.force: + print("Warning: Skipped validation, file may be corrupt", file=sys.stderr) + # Exit with error if validation failed + elif not success: + print("Contents would produce a corrupt file.", file=sys.stderr) + print("Please validate XML before repacking.", file=sys.stderr) + print("Use --force to skip validation and pack anyway.", file=sys.stderr) + sys.exit(1) + + except ValueError as e: + sys.exit(f"Error: {e}") + + +def pack_document(input_dir, output_file, validate=False): + """Pack a directory into an Office file (.docx/.pptx/.xlsx). + + Args: + input_dir: Path to unpacked Office document directory + output_file: Path to output Office file + validate: If True, validates with soffice (default: False) + + Returns: + bool: True if successful, False if validation failed + """ + input_dir = Path(input_dir) + output_file = Path(output_file) + + if not input_dir.is_dir(): + raise ValueError(f"{input_dir} is not a directory") + if output_file.suffix.lower() not in {".docx", ".pptx", ".xlsx"}: + raise ValueError(f"{output_file} must be a .docx, .pptx, or .xlsx file") + + # Work in temporary directory to avoid modifying original + with tempfile.TemporaryDirectory() as temp_dir: + temp_content_dir = Path(temp_dir) / "content" + shutil.copytree(input_dir, temp_content_dir) + + # Process XML files to remove pretty-printing whitespace + for pattern in ["*.xml", "*.rels"]: + for xml_file in temp_content_dir.rglob(pattern): + condense_xml(xml_file) + + # Create final Office file as zip archive + output_file.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf: + for f in temp_content_dir.rglob("*"): + if f.is_file(): + zf.write(f, f.relative_to(temp_content_dir)) + + # Validate if requested + if validate: + if not validate_document(output_file): + output_file.unlink() # Delete the corrupt file + return False + + return True + + +def validate_document(doc_path): + """Validate document by converting to HTML with soffice.""" + # Determine the correct filter based on file extension + match doc_path.suffix.lower(): + case ".docx": + filter_name = "html:HTML" + case ".pptx": + filter_name = "html:impress_html_Export" + case ".xlsx": + filter_name = "html:HTML (StarCalc)" + + with tempfile.TemporaryDirectory() as temp_dir: + try: + result = subprocess.run( + [ + "soffice", + "--headless", + "--convert-to", + filter_name, + "--outdir", + temp_dir, + str(doc_path), + ], + capture_output=True, + timeout=10, + text=True, + ) + if not (Path(temp_dir) / f"{doc_path.stem}.html").exists(): + error_msg = result.stderr.strip() or "Document validation failed" + print(f"Validation error: {error_msg}", file=sys.stderr) + return False + return True + except FileNotFoundError: + print("Warning: soffice not found. Skipping validation.", file=sys.stderr) + return True + except subprocess.TimeoutExpired: + print("Validation error: Timeout during conversion", file=sys.stderr) + return False + except Exception as e: + print(f"Validation error: {e}", file=sys.stderr) + return False + + +def condense_xml(xml_file): + """Strip unnecessary whitespace and remove comments.""" + with open(xml_file, "r", encoding="utf-8") as f: + dom = defusedxml.minidom.parse(f) + + # Process each element to remove whitespace and comments + for element in dom.getElementsByTagName("*"): + # Skip w:t elements and their processing + if element.tagName.endswith(":t"): + continue + + # Remove whitespace-only text nodes and comment nodes + for child in list(element.childNodes): + if ( + child.nodeType == child.TEXT_NODE + and child.nodeValue + and child.nodeValue.strip() == "" + ) or child.nodeType == child.COMMENT_NODE: + element.removeChild(child) + + # Write back the condensed XML + with open(xml_file, "wb") as f: + f.write(dom.toxml(encoding="UTF-8")) + + +if __name__ == "__main__": + main() diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/unpack.py b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/unpack.py new file mode 100755 index 00000000..96cb94ba --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/unpack.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Unpack and format XML contents of Office files (.docx, .pptx, .xlsx)""" + +import random +import sys +import zipfile +from pathlib import Path + + +def extract_archive_safely(input_file: str | Path, output_dir: str | Path): + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + with zipfile.ZipFile(input_file) as archive: + for member in archive.infolist(): + destination = output_path / member.filename + if not destination.resolve().is_relative_to(output_path.resolve()): + raise ValueError(f"Unsafe archive entry: {member.filename}") + + archive.extractall(output_path) + + +def pretty_print_xml(output_path: Path): + import defusedxml.minidom + + xml_files = list(output_path.rglob("*.xml")) + list(output_path.rglob("*.rels")) + for xml_file in xml_files: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + xml_file.write_bytes(dom.toprettyxml(indent=" ", encoding="ascii")) + + +def main(argv: list[str] | None = None): + argv = argv or sys.argv[1:] + if len(argv) != 2: + raise SystemExit("Usage: python unpack.py ") + + input_file, output_dir = argv + output_path = Path(output_dir) + extract_archive_safely(input_file, output_path) + pretty_print_xml(output_path) + + if input_file.endswith(".docx"): + suggested_rsid = "".join(random.choices("0123456789ABCDEF", k=8)) + print(f"Suggested RSID for edit session: {suggested_rsid}") + + +if __name__ == "__main__": + main() diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validate.py b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validate.py new file mode 100755 index 00000000..508c5891 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validate.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +Command line tool to validate Office document XML files against XSD schemas and tracked changes. + +Usage: + python validate.py --original +""" + +import argparse +import sys +from pathlib import Path + +from validation import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator + + +def main(): + parser = argparse.ArgumentParser(description="Validate Office document XML files") + parser.add_argument( + "unpacked_dir", + help="Path to unpacked Office document directory", + ) + parser.add_argument( + "--original", + required=True, + help="Path to original file (.docx/.pptx/.xlsx)", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Enable verbose output", + ) + args = parser.parse_args() + + # Validate paths + unpacked_dir = Path(args.unpacked_dir) + original_file = Path(args.original) + file_extension = original_file.suffix.lower() + assert unpacked_dir.is_dir(), f"Error: {unpacked_dir} is not a directory" + assert original_file.is_file(), f"Error: {original_file} is not a file" + assert file_extension in [".docx", ".pptx", ".xlsx"], ( + f"Error: {original_file} must be a .docx, .pptx, or .xlsx file" + ) + + # Run validations + match file_extension: + case ".docx": + validators = [DOCXSchemaValidator, RedliningValidator] + case ".pptx": + validators = [PPTXSchemaValidator] + case _: + print(f"Error: Validation not supported for file type {file_extension}") + sys.exit(1) + + # Run validators + success = True + for V in validators: + validator = V(unpacked_dir, original_file, verbose=args.verbose) + if not validator.validate(): + success = False + + if success: + print("All validations PASSED!") + + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validation/__init__.py b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validation/__init__.py new file mode 100644 index 00000000..db092ece --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validation/__init__.py @@ -0,0 +1,15 @@ +""" +Validation modules for Word document processing. +""" + +from .base import BaseSchemaValidator +from .docx import DOCXSchemaValidator +from .pptx import PPTXSchemaValidator +from .redlining import RedliningValidator + +__all__ = [ + "BaseSchemaValidator", + "DOCXSchemaValidator", + "PPTXSchemaValidator", + "RedliningValidator", +] diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validation/base.py b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validation/base.py new file mode 100644 index 00000000..0681b199 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validation/base.py @@ -0,0 +1,951 @@ +""" +Base validator with common validation logic for document files. +""" + +import re +from pathlib import Path + +import lxml.etree + + +class BaseSchemaValidator: + """Base validator with common validation logic for document files.""" + + # Elements whose 'id' attributes must be unique within their file + # Format: element_name -> (attribute_name, scope) + # scope can be 'file' (unique within file) or 'global' (unique across all files) + UNIQUE_ID_REQUIREMENTS = { + # Word elements + "comment": ("id", "file"), # Comment IDs in comments.xml + "commentrangestart": ("id", "file"), # Must match comment IDs + "commentrangeend": ("id", "file"), # Must match comment IDs + "bookmarkstart": ("id", "file"), # Bookmark start IDs + "bookmarkend": ("id", "file"), # Bookmark end IDs + # Note: ins and del (track changes) can share IDs when part of same revision + # PowerPoint elements + "sldid": ("id", "file"), # Slide IDs in presentation.xml + "sldmasterid": ("id", "global"), # Slide master IDs must be globally unique + "sldlayoutid": ("id", "global"), # Slide layout IDs must be globally unique + "cm": ("authorid", "file"), # Comment author IDs + # Excel elements + "sheet": ("sheetid", "file"), # Sheet IDs in workbook.xml + "definedname": ("id", "file"), # Named range IDs + # Drawing/Shape elements (all formats) + "cxnsp": ("id", "file"), # Connection shape IDs + "sp": ("id", "file"), # Shape IDs + "pic": ("id", "file"), # Picture IDs + "grpsp": ("id", "file"), # Group shape IDs + } + + # Mapping of element names to expected relationship types + # Subclasses should override this with format-specific mappings + ELEMENT_RELATIONSHIP_TYPES = {} + + # Unified schema mappings for all Office document types + SCHEMA_MAPPINGS = { + # Document type specific schemas + "word": "ISO-IEC29500-4_2016/wml.xsd", # Word documents + "ppt": "ISO-IEC29500-4_2016/pml.xsd", # PowerPoint presentations + "xl": "ISO-IEC29500-4_2016/sml.xsd", # Excel spreadsheets + # Common file types + "[Content_Types].xml": "ecma/fouth-edition/opc-contentTypes.xsd", + "app.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd", + "core.xml": "ecma/fouth-edition/opc-coreProperties.xsd", + "custom.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd", + ".rels": "ecma/fouth-edition/opc-relationships.xsd", + # Word-specific files + "people.xml": "microsoft/wml-2012.xsd", + "commentsIds.xml": "microsoft/wml-cid-2016.xsd", + "commentsExtensible.xml": "microsoft/wml-cex-2018.xsd", + "commentsExtended.xml": "microsoft/wml-2012.xsd", + # Chart files (common across document types) + "chart": "ISO-IEC29500-4_2016/dml-chart.xsd", + # Theme files (common across document types) + "theme": "ISO-IEC29500-4_2016/dml-main.xsd", + # Drawing and media files + "drawing": "ISO-IEC29500-4_2016/dml-main.xsd", + } + + # Unified namespace constants + MC_NAMESPACE = "http://schemas.openxmlformats.org/markup-compatibility/2006" + XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace" + + # Common OOXML namespaces used across validators + PACKAGE_RELATIONSHIPS_NAMESPACE = ( + "http://schemas.openxmlformats.org/package/2006/relationships" + ) + OFFICE_RELATIONSHIPS_NAMESPACE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + ) + CONTENT_TYPES_NAMESPACE = ( + "http://schemas.openxmlformats.org/package/2006/content-types" + ) + + # Folders where we should clean ignorable namespaces + MAIN_CONTENT_FOLDERS = {"word", "ppt", "xl"} + + # All allowed OOXML namespaces (superset of all document types) + OOXML_NAMESPACES = { + "http://schemas.openxmlformats.org/officeDocument/2006/math", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "http://schemas.openxmlformats.org/schemaLibrary/2006/main", + "http://schemas.openxmlformats.org/drawingml/2006/main", + "http://schemas.openxmlformats.org/drawingml/2006/chart", + "http://schemas.openxmlformats.org/drawingml/2006/chartDrawing", + "http://schemas.openxmlformats.org/drawingml/2006/diagram", + "http://schemas.openxmlformats.org/drawingml/2006/picture", + "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing", + "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "http://schemas.openxmlformats.org/presentationml/2006/main", + "http://schemas.openxmlformats.org/spreadsheetml/2006/main", + "http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes", + "http://www.w3.org/XML/1998/namespace", + } + + def __init__(self, unpacked_dir, original_file, verbose=False): + self.unpacked_dir = Path(unpacked_dir).resolve() + self.original_file = Path(original_file) + self.verbose = verbose + + # Set schemas directory + self.schemas_dir = Path(__file__).parent.parent.parent / "schemas" + + # Get all XML and .rels files + patterns = ["*.xml", "*.rels"] + self.xml_files = [ + f for pattern in patterns for f in self.unpacked_dir.rglob(pattern) + ] + + if not self.xml_files: + print(f"Warning: No XML files found in {self.unpacked_dir}") + + def validate(self): + """Run all validation checks and return True if all pass.""" + raise NotImplementedError("Subclasses must implement the validate method") + + def validate_xml(self): + """Validate that all XML files are well-formed.""" + errors = [] + + for xml_file in self.xml_files: + try: + # Try to parse the XML file + lxml.etree.parse(str(xml_file)) + except lxml.etree.XMLSyntaxError as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {e.lineno}: {e.msg}" + ) + except Exception as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Unexpected error: {str(e)}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} XML violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All XML files are well-formed") + return True + + def validate_namespaces(self): + """Validate that namespace prefixes in Ignorable attributes are declared.""" + errors = [] + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + declared = set(root.nsmap.keys()) - {None} # Exclude default namespace + + for attr_val in [ + v for k, v in root.attrib.items() if k.endswith("Ignorable") + ]: + undeclared = set(attr_val.split()) - declared + errors.extend( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Namespace '{ns}' in Ignorable but not declared" + for ns in undeclared + ) + except lxml.etree.XMLSyntaxError: + continue + + if errors: + print(f"FAILED - {len(errors)} namespace issues:") + for error in errors: + print(error) + return False + if self.verbose: + print("PASSED - All namespace prefixes properly declared") + return True + + def validate_unique_ids(self): + """Validate that specific IDs are unique according to OOXML requirements.""" + errors = [] + global_ids = {} # Track globally unique IDs across all files + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + file_ids = {} # Track IDs that must be unique within this file + + # Remove all mc:AlternateContent elements from the tree + mc_elements = root.xpath( + ".//mc:AlternateContent", namespaces={"mc": self.MC_NAMESPACE} + ) + for elem in mc_elements: + elem.getparent().remove(elem) + + # Now check IDs in the cleaned tree + for elem in root.iter(): + # Get the element name without namespace + tag = ( + elem.tag.split("}")[-1].lower() + if "}" in elem.tag + else elem.tag.lower() + ) + + # Check if this element type has ID uniqueness requirements + if tag in self.UNIQUE_ID_REQUIREMENTS: + attr_name, scope = self.UNIQUE_ID_REQUIREMENTS[tag] + + # Look for the specified attribute + id_value = None + for attr, value in elem.attrib.items(): + attr_local = ( + attr.split("}")[-1].lower() + if "}" in attr + else attr.lower() + ) + if attr_local == attr_name: + id_value = value + break + + if id_value is not None: + if scope == "global": + # Check global uniqueness + if id_value in global_ids: + prev_file, prev_line, prev_tag = global_ids[ + id_value + ] + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: Global ID '{id_value}' in <{tag}> " + f"already used in {prev_file} at line {prev_line} in <{prev_tag}>" + ) + else: + global_ids[id_value] = ( + xml_file.relative_to(self.unpacked_dir), + elem.sourceline, + tag, + ) + elif scope == "file": + # Check file-level uniqueness + key = (tag, attr_name) + if key not in file_ids: + file_ids[key] = {} + + if id_value in file_ids[key]: + prev_line = file_ids[key][id_value] + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: Duplicate {attr_name}='{id_value}' in <{tag}> " + f"(first occurrence at line {prev_line})" + ) + else: + file_ids[key][id_value] = elem.sourceline + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} ID uniqueness violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All required IDs are unique") + return True + + def validate_file_references(self): + """ + Validate that all .rels files properly reference files and that all files are referenced. + """ + errors = [] + + # Find all .rels files + rels_files = list(self.unpacked_dir.rglob("*.rels")) + + if not rels_files: + if self.verbose: + print("PASSED - No .rels files found") + return True + + # Get all files in the unpacked directory (excluding reference files) + all_files = [] + for file_path in self.unpacked_dir.rglob("*"): + if ( + file_path.is_file() + and file_path.name != "[Content_Types].xml" + and not file_path.name.endswith(".rels") + ): # This file is not referenced by .rels + all_files.append(file_path.resolve()) + + # Track all files that are referenced by any .rels file + all_referenced_files = set() + + if self.verbose: + print( + f"Found {len(rels_files)} .rels files and {len(all_files)} target files" + ) + + # Check each .rels file + for rels_file in rels_files: + try: + # Parse relationships file + rels_root = lxml.etree.parse(str(rels_file)).getroot() + + # Get the directory where this .rels file is located + rels_dir = rels_file.parent + + # Find all relationships and their targets + referenced_files = set() + broken_refs = [] + + for rel in rels_root.findall( + ".//ns:Relationship", + namespaces={"ns": self.PACKAGE_RELATIONSHIPS_NAMESPACE}, + ): + target = rel.get("Target") + if target and not target.startswith( + ("http", "mailto:") + ): # Skip external URLs + # Resolve the target path relative to the .rels file location + if rels_file.name == ".rels": + # Root .rels file - targets are relative to unpacked_dir + target_path = self.unpacked_dir / target + else: + # Other .rels files - targets are relative to their parent's parent + # e.g., word/_rels/document.xml.rels -> targets relative to word/ + base_dir = rels_dir.parent + target_path = base_dir / target + + # Normalize the path and check if it exists + try: + target_path = target_path.resolve() + if target_path.exists() and target_path.is_file(): + referenced_files.add(target_path) + all_referenced_files.add(target_path) + else: + broken_refs.append((target, rel.sourceline)) + except (OSError, ValueError): + broken_refs.append((target, rel.sourceline)) + + # Report broken references + if broken_refs: + rel_path = rels_file.relative_to(self.unpacked_dir) + for broken_ref, line_num in broken_refs: + errors.append( + f" {rel_path}: Line {line_num}: Broken reference to {broken_ref}" + ) + + except Exception as e: + rel_path = rels_file.relative_to(self.unpacked_dir) + errors.append(f" Error parsing {rel_path}: {e}") + + # Check for unreferenced files (files that exist but are not referenced anywhere) + unreferenced_files = set(all_files) - all_referenced_files + + if unreferenced_files: + for unref_file in sorted(unreferenced_files): + unref_rel_path = unref_file.relative_to(self.unpacked_dir) + errors.append(f" Unreferenced file: {unref_rel_path}") + + if errors: + print(f"FAILED - Found {len(errors)} relationship validation errors:") + for error in errors: + print(error) + print( + "CRITICAL: These errors will cause the document to appear corrupt. " + + "Broken references MUST be fixed, " + + "and unreferenced files MUST be referenced or removed." + ) + return False + else: + if self.verbose: + print( + "PASSED - All references are valid and all files are properly referenced" + ) + return True + + def validate_all_relationship_ids(self): + """ + Validate that all r:id attributes in XML files reference existing IDs + in their corresponding .rels files, and optionally validate relationship types. + """ + import lxml.etree + + errors = [] + + # Process each XML file that might contain r:id references + for xml_file in self.xml_files: + # Skip .rels files themselves + if xml_file.suffix == ".rels": + continue + + # Determine the corresponding .rels file + # For dir/file.xml, it's dir/_rels/file.xml.rels + rels_dir = xml_file.parent / "_rels" + rels_file = rels_dir / f"{xml_file.name}.rels" + + # Skip if there's no corresponding .rels file (that's okay) + if not rels_file.exists(): + continue + + try: + # Parse the .rels file to get valid relationship IDs and their types + rels_root = lxml.etree.parse(str(rels_file)).getroot() + rid_to_type = {} + + for rel in rels_root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rid = rel.get("Id") + rel_type = rel.get("Type", "") + if rid: + # Check for duplicate rIds + if rid in rid_to_type: + rels_rel_path = rels_file.relative_to(self.unpacked_dir) + errors.append( + f" {rels_rel_path}: Line {rel.sourceline}: " + f"Duplicate relationship ID '{rid}' (IDs must be unique)" + ) + # Extract just the type name from the full URL + type_name = ( + rel_type.split("/")[-1] if "/" in rel_type else rel_type + ) + rid_to_type[rid] = type_name + + # Parse the XML file to find all r:id references + xml_root = lxml.etree.parse(str(xml_file)).getroot() + + # Find all elements with r:id attributes + for elem in xml_root.iter(): + # Check for r:id attribute (relationship ID) + rid_attr = elem.get(f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id") + if rid_attr: + xml_rel_path = xml_file.relative_to(self.unpacked_dir) + elem_name = ( + elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag + ) + + # Check if the ID exists + if rid_attr not in rid_to_type: + errors.append( + f" {xml_rel_path}: Line {elem.sourceline}: " + f"<{elem_name}> references non-existent relationship '{rid_attr}' " + f"(valid IDs: {', '.join(sorted(rid_to_type.keys())[:5])}{'...' if len(rid_to_type) > 5 else ''})" + ) + # Check if we have type expectations for this element + elif self.ELEMENT_RELATIONSHIP_TYPES: + expected_type = self._get_expected_relationship_type( + elem_name + ) + if expected_type: + actual_type = rid_to_type[rid_attr] + # Check if the actual type matches or contains the expected type + if expected_type not in actual_type.lower(): + errors.append( + f" {xml_rel_path}: Line {elem.sourceline}: " + f"<{elem_name}> references '{rid_attr}' which points to '{actual_type}' " + f"but should point to a '{expected_type}' relationship" + ) + + except Exception as e: + xml_rel_path = xml_file.relative_to(self.unpacked_dir) + errors.append(f" Error processing {xml_rel_path}: {e}") + + if errors: + print(f"FAILED - Found {len(errors)} relationship ID reference errors:") + for error in errors: + print(error) + print("\nThese ID mismatches will cause the document to appear corrupt!") + return False + else: + if self.verbose: + print("PASSED - All relationship ID references are valid") + return True + + def _get_expected_relationship_type(self, element_name): + """ + Get the expected relationship type for an element. + First checks the explicit mapping, then tries pattern detection. + """ + # Normalize element name to lowercase + elem_lower = element_name.lower() + + # Check explicit mapping first + if elem_lower in self.ELEMENT_RELATIONSHIP_TYPES: + return self.ELEMENT_RELATIONSHIP_TYPES[elem_lower] + + # Try pattern detection for common patterns + # Pattern 1: Elements ending in "Id" often expect a relationship of the prefix type + if elem_lower.endswith("id") and len(elem_lower) > 2: + # e.g., "sldId" -> "sld", "sldMasterId" -> "sldMaster" + prefix = elem_lower[:-2] # Remove "id" + # Check if this might be a compound like "sldMasterId" + if prefix.endswith("master"): + return prefix.lower() + elif prefix.endswith("layout"): + return prefix.lower() + else: + # Simple case like "sldId" -> "slide" + # Common transformations + if prefix == "sld": + return "slide" + return prefix.lower() + + # Pattern 2: Elements ending in "Reference" expect a relationship of the prefix type + if elem_lower.endswith("reference") and len(elem_lower) > 9: + prefix = elem_lower[:-9] # Remove "reference" + return prefix.lower() + + return None + + def validate_content_types(self): + """Validate that all content files are properly declared in [Content_Types].xml.""" + errors = [] + + # Find [Content_Types].xml file + content_types_file = self.unpacked_dir / "[Content_Types].xml" + if not content_types_file.exists(): + print("FAILED - [Content_Types].xml file not found") + return False + + try: + # Parse and get all declared parts and extensions + root = lxml.etree.parse(str(content_types_file)).getroot() + declared_parts = set() + declared_extensions = set() + + # Get Override declarations (specific files) + for override in root.findall( + f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Override" + ): + part_name = override.get("PartName") + if part_name is not None: + declared_parts.add(part_name.lstrip("/")) + + # Get Default declarations (by extension) + for default in root.findall( + f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Default" + ): + extension = default.get("Extension") + if extension is not None: + declared_extensions.add(extension.lower()) + + # Root elements that require content type declaration + declarable_roots = { + "sld", + "sldLayout", + "sldMaster", + "presentation", # PowerPoint + "document", # Word + "workbook", + "worksheet", # Excel + "theme", # Common + } + + # Common media file extensions that should be declared + media_extensions = { + "png": "image/png", + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "gif": "image/gif", + "bmp": "image/bmp", + "tiff": "image/tiff", + "wmf": "image/x-wmf", + "emf": "image/x-emf", + } + + # Get all files in the unpacked directory + all_files = list(self.unpacked_dir.rglob("*")) + all_files = [f for f in all_files if f.is_file()] + + # Check all XML files for Override declarations + for xml_file in self.xml_files: + path_str = str(xml_file.relative_to(self.unpacked_dir)).replace( + "\\", "/" + ) + + # Skip non-content files + if any( + skip in path_str + for skip in [".rels", "[Content_Types]", "docProps/", "_rels/"] + ): + continue + + try: + root_tag = lxml.etree.parse(str(xml_file)).getroot().tag + root_name = root_tag.split("}")[-1] if "}" in root_tag else root_tag + + if root_name in declarable_roots and path_str not in declared_parts: + errors.append( + f" {path_str}: File with <{root_name}> root not declared in [Content_Types].xml" + ) + + except Exception: + continue # Skip unparseable files + + # Check all non-XML files for Default extension declarations + for file_path in all_files: + # Skip XML files and metadata files (already checked above) + if file_path.suffix.lower() in {".xml", ".rels"}: + continue + if file_path.name == "[Content_Types].xml": + continue + if "_rels" in file_path.parts or "docProps" in file_path.parts: + continue + + extension = file_path.suffix.lstrip(".").lower() + if extension and extension not in declared_extensions: + # Check if it's a known media extension that should be declared + if extension in media_extensions: + relative_path = file_path.relative_to(self.unpacked_dir) + errors.append( + f' {relative_path}: File with extension \'{extension}\' not declared in [Content_Types].xml - should add: ' + ) + + except Exception as e: + errors.append(f" Error parsing [Content_Types].xml: {e}") + + if errors: + print(f"FAILED - Found {len(errors)} content type declaration errors:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print( + "PASSED - All content files are properly declared in [Content_Types].xml" + ) + return True + + def validate_file_against_xsd(self, xml_file, verbose=False): + """Validate a single XML file against XSD schema, comparing with original. + + Args: + xml_file: Path to XML file to validate + verbose: Enable verbose output + + Returns: + tuple: (is_valid, new_errors_set) where is_valid is True/False/None (skipped) + """ + # Resolve both paths to handle symlinks + xml_file = Path(xml_file).resolve() + unpacked_dir = self.unpacked_dir.resolve() + + # Validate current file + is_valid, current_errors = self._validate_single_file_xsd( + xml_file, unpacked_dir + ) + + if is_valid is None: + return None, set() # Skipped + elif is_valid: + return True, set() # Valid, no errors + + # Get errors from original file for this specific file + original_errors = self._get_original_file_errors(xml_file) + + # Compare with original (both are guaranteed to be sets here) + assert current_errors is not None + new_errors = current_errors - original_errors + + if new_errors: + if verbose: + relative_path = xml_file.relative_to(unpacked_dir) + print(f"FAILED - {relative_path}: {len(new_errors)} new error(s)") + for error in list(new_errors)[:3]: + truncated = error[:250] + "..." if len(error) > 250 else error + print(f" - {truncated}") + return False, new_errors + else: + # All errors existed in original + if verbose: + print( + f"PASSED - No new errors (original had {len(current_errors)} errors)" + ) + return True, set() + + def validate_against_xsd(self): + """Validate XML files against XSD schemas, showing only new errors compared to original.""" + new_errors = [] + original_error_count = 0 + valid_count = 0 + skipped_count = 0 + + for xml_file in self.xml_files: + relative_path = str(xml_file.relative_to(self.unpacked_dir)) + is_valid, new_file_errors = self.validate_file_against_xsd( + xml_file, verbose=False + ) + + if is_valid is None: + skipped_count += 1 + continue + elif is_valid and not new_file_errors: + valid_count += 1 + continue + elif is_valid: + # Had errors but all existed in original + original_error_count += 1 + valid_count += 1 + continue + + # Has new errors + new_errors.append(f" {relative_path}: {len(new_file_errors)} new error(s)") + for error in list(new_file_errors)[:3]: # Show first 3 errors + new_errors.append( + f" - {error[:250]}..." if len(error) > 250 else f" - {error}" + ) + + # Print summary + if self.verbose: + print(f"Validated {len(self.xml_files)} files:") + print(f" - Valid: {valid_count}") + print(f" - Skipped (no schema): {skipped_count}") + if original_error_count: + print(f" - With original errors (ignored): {original_error_count}") + print( + f" - With NEW errors: {len(new_errors) > 0 and len([e for e in new_errors if not e.startswith(' ')]) or 0}" + ) + + if new_errors: + print("\nFAILED - Found NEW validation errors:") + for error in new_errors: + print(error) + return False + else: + if self.verbose: + print("\nPASSED - No new XSD validation errors introduced") + return True + + def _get_schema_path(self, xml_file): + """Determine the appropriate schema path for an XML file.""" + # Check exact filename match + if xml_file.name in self.SCHEMA_MAPPINGS: + return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.name] + + # Check .rels files + if xml_file.suffix == ".rels": + return self.schemas_dir / self.SCHEMA_MAPPINGS[".rels"] + + # Check chart files + if "charts/" in str(xml_file) and xml_file.name.startswith("chart"): + return self.schemas_dir / self.SCHEMA_MAPPINGS["chart"] + + # Check theme files + if "theme/" in str(xml_file) and xml_file.name.startswith("theme"): + return self.schemas_dir / self.SCHEMA_MAPPINGS["theme"] + + # Check if file is in a main content folder and use appropriate schema + if xml_file.parent.name in self.MAIN_CONTENT_FOLDERS: + return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.parent.name] + + return None + + def _clean_ignorable_namespaces(self, xml_doc): + """Remove attributes and elements not in allowed namespaces.""" + # Create a clean copy + xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") + xml_copy = lxml.etree.fromstring(xml_string) + + # Remove attributes not in allowed namespaces + for elem in xml_copy.iter(): + attrs_to_remove = [] + + for attr in elem.attrib: + # Check if attribute is from a namespace other than allowed ones + if "{" in attr: + ns = attr.split("}")[0][1:] + if ns not in self.OOXML_NAMESPACES: + attrs_to_remove.append(attr) + + # Remove collected attributes + for attr in attrs_to_remove: + del elem.attrib[attr] + + # Remove elements not in allowed namespaces + self._remove_ignorable_elements(xml_copy) + + return lxml.etree.ElementTree(xml_copy) + + def _remove_ignorable_elements(self, root): + """Recursively remove all elements not in allowed namespaces.""" + elements_to_remove = [] + + # Find elements to remove + for elem in list(root): + # Skip non-element nodes (comments, processing instructions, etc.) + if not hasattr(elem, "tag") or callable(elem.tag): + continue + + tag_str = str(elem.tag) + if tag_str.startswith("{"): + ns = tag_str.split("}")[0][1:] + if ns not in self.OOXML_NAMESPACES: + elements_to_remove.append(elem) + continue + + # Recursively clean child elements + self._remove_ignorable_elements(elem) + + # Remove collected elements + for elem in elements_to_remove: + root.remove(elem) + + def _preprocess_for_mc_ignorable(self, xml_doc): + """Preprocess XML to handle mc:Ignorable attribute properly.""" + # Remove mc:Ignorable attributes before validation + root = xml_doc.getroot() + + # Remove mc:Ignorable attribute from root + if f"{{{self.MC_NAMESPACE}}}Ignorable" in root.attrib: + del root.attrib[f"{{{self.MC_NAMESPACE}}}Ignorable"] + + return xml_doc + + def _validate_single_file_xsd(self, xml_file, base_path): + """Validate a single XML file against XSD schema. Returns (is_valid, errors_set).""" + schema_path = self._get_schema_path(xml_file) + if not schema_path: + return None, None # Skip file + + try: + # Load schema + with open(schema_path, "rb") as xsd_file: + parser = lxml.etree.XMLParser() + xsd_doc = lxml.etree.parse( + xsd_file, parser=parser, base_url=str(schema_path) + ) + schema = lxml.etree.XMLSchema(xsd_doc) + + # Load and preprocess XML + with open(xml_file, "r") as f: + xml_doc = lxml.etree.parse(f) + + xml_doc, _ = self._remove_template_tags_from_text_nodes(xml_doc) + xml_doc = self._preprocess_for_mc_ignorable(xml_doc) + + # Clean ignorable namespaces if needed + relative_path = xml_file.relative_to(base_path) + if ( + relative_path.parts + and relative_path.parts[0] in self.MAIN_CONTENT_FOLDERS + ): + xml_doc = self._clean_ignorable_namespaces(xml_doc) + + # Validate + if schema.validate(xml_doc): + return True, set() + else: + errors = set() + for error in schema.error_log: + # Store normalized error message (without line numbers for comparison) + errors.add(error.message) + return False, errors + + except Exception as e: + return False, {str(e)} + + def _get_original_file_errors(self, xml_file): + """Get XSD validation errors from a single file in the original document. + + Args: + xml_file: Path to the XML file in unpacked_dir to check + + Returns: + set: Set of error messages from the original file + """ + import tempfile + import zipfile + + # Resolve both paths to handle symlinks (e.g., /var vs /private/var on macOS) + xml_file = Path(xml_file).resolve() + unpacked_dir = self.unpacked_dir.resolve() + relative_path = xml_file.relative_to(unpacked_dir) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Extract original file + with zipfile.ZipFile(self.original_file, "r") as zip_ref: + zip_ref.extractall(temp_path) + + # Find corresponding file in original + original_xml_file = temp_path / relative_path + + if not original_xml_file.exists(): + # File didn't exist in original, so no original errors + return set() + + # Validate the specific file in original + is_valid, errors = self._validate_single_file_xsd( + original_xml_file, temp_path + ) + return errors if errors else set() + + def _remove_template_tags_from_text_nodes(self, xml_doc): + """Remove template tags from XML text nodes and collect warnings. + + Template tags follow the pattern {{ ... }} and are used as placeholders + for content replacement. They should be removed from text content before + XSD validation while preserving XML structure. + + Returns: + tuple: (cleaned_xml_doc, warnings_list) + """ + warnings = [] + template_pattern = re.compile(r"\{\{[^}]*\}\}") + + # Create a copy of the document to avoid modifying the original + xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") + xml_copy = lxml.etree.fromstring(xml_string) + + def process_text_content(text, content_type): + if not text: + return text + matches = list(template_pattern.finditer(text)) + if matches: + for match in matches: + warnings.append( + f"Found template tag in {content_type}: {match.group()}" + ) + return template_pattern.sub("", text) + return text + + # Process all text nodes in the document + for elem in xml_copy.iter(): + # Skip processing if this is a w:t element + if not hasattr(elem, "tag") or callable(elem.tag): + continue + tag_str = str(elem.tag) + if tag_str.endswith("}t") or tag_str == "t": + continue + + elem.text = process_text_content(elem.text, "text content") + elem.tail = process_text_content(elem.tail, "tail content") + + return lxml.etree.ElementTree(xml_copy), warnings + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validation/docx.py b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validation/docx.py new file mode 100644 index 00000000..602c4708 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validation/docx.py @@ -0,0 +1,274 @@ +""" +Validator for Word document XML files against XSD schemas. +""" + +import re +import tempfile +import zipfile + +import lxml.etree + +from .base import BaseSchemaValidator + + +class DOCXSchemaValidator(BaseSchemaValidator): + """Validator for Word document XML files against XSD schemas.""" + + # Word-specific namespace + WORD_2006_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + + # Word-specific element to relationship type mappings + # Start with empty mapping - add specific cases as we discover them + ELEMENT_RELATIONSHIP_TYPES = {} + + def validate(self): + """Run all validation checks and return True if all pass.""" + # Test 0: XML well-formedness + if not self.validate_xml(): + return False + + # Test 1: Namespace declarations + all_valid = True + if not self.validate_namespaces(): + all_valid = False + + # Test 2: Unique IDs + if not self.validate_unique_ids(): + all_valid = False + + # Test 3: Relationship and file reference validation + if not self.validate_file_references(): + all_valid = False + + # Test 4: Content type declarations + if not self.validate_content_types(): + all_valid = False + + # Test 5: XSD schema validation + if not self.validate_against_xsd(): + all_valid = False + + # Test 6: Whitespace preservation + if not self.validate_whitespace_preservation(): + all_valid = False + + # Test 7: Deletion validation + if not self.validate_deletions(): + all_valid = False + + # Test 8: Insertion validation + if not self.validate_insertions(): + all_valid = False + + # Test 9: Relationship ID reference validation + if not self.validate_all_relationship_ids(): + all_valid = False + + # Count and compare paragraphs + self.compare_paragraph_counts() + + return all_valid + + def validate_whitespace_preservation(self): + """ + Validate that w:t elements with whitespace have xml:space='preserve'. + """ + errors = [] + + for xml_file in self.xml_files: + # Only check document.xml files + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + # Find all w:t elements + for elem in root.iter(f"{{{self.WORD_2006_NAMESPACE}}}t"): + if elem.text: + text = elem.text + # Check if text starts or ends with whitespace + if re.match(r"^\s.*", text) or re.match(r".*\s$", text): + # Check if xml:space="preserve" attribute exists + xml_space_attr = f"{{{self.XML_NAMESPACE}}}space" + if ( + xml_space_attr not in elem.attrib + or elem.attrib[xml_space_attr] != "preserve" + ): + # Show a preview of the text + text_preview = ( + repr(text)[:50] + "..." + if len(repr(text)) > 50 + else repr(text) + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: w:t element with whitespace missing xml:space='preserve': {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} whitespace preservation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All whitespace is properly preserved") + return True + + def validate_deletions(self): + """ + Validate that w:t elements are not within w:del elements. + For some reason, XSD validation does not catch this, so we do it manually. + """ + errors = [] + + for xml_file in self.xml_files: + # Only check document.xml files + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + # Find all w:t elements that are descendants of w:del elements + namespaces = {"w": self.WORD_2006_NAMESPACE} + xpath_expression = ".//w:del//w:t" + problematic_t_elements = root.xpath( + xpath_expression, namespaces=namespaces + ) + for t_elem in problematic_t_elements: + if t_elem.text: + # Show a preview of the text + text_preview = ( + repr(t_elem.text)[:50] + "..." + if len(repr(t_elem.text)) > 50 + else repr(t_elem.text) + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {t_elem.sourceline}: found within : {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} deletion validation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - No w:t elements found within w:del elements") + return True + + def count_paragraphs_in_unpacked(self): + """Count the number of paragraphs in the unpacked document.""" + count = 0 + + for xml_file in self.xml_files: + # Only check document.xml files + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + # Count all w:p elements + paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") + count = len(paragraphs) + except Exception as e: + print(f"Error counting paragraphs in unpacked document: {e}") + + return count + + def count_paragraphs_in_original(self): + """Count the number of paragraphs in the original docx file.""" + count = 0 + + try: + # Create temporary directory to unpack original + with tempfile.TemporaryDirectory() as temp_dir: + # Unpack original docx + with zipfile.ZipFile(self.original_file, "r") as zip_ref: + zip_ref.extractall(temp_dir) + + # Parse document.xml + doc_xml_path = temp_dir + "/word/document.xml" + root = lxml.etree.parse(doc_xml_path).getroot() + + # Count all w:p elements + paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") + count = len(paragraphs) + + except Exception as e: + print(f"Error counting paragraphs in original document: {e}") + + return count + + def validate_insertions(self): + """ + Validate that w:delText elements are not within w:ins elements. + w:delText is only allowed in w:ins if nested within a w:del. + """ + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + # Find w:delText in w:ins that are NOT within w:del + invalid_elements = root.xpath( + ".//w:ins//w:delText[not(ancestor::w:del)]", + namespaces=namespaces + ) + + for elem in invalid_elements: + text_preview = ( + repr(elem.text or "")[:50] + "..." + if len(repr(elem.text or "")) > 50 + else repr(elem.text or "") + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: within : {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} insertion validation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - No w:delText elements within w:ins elements") + return True + + def compare_paragraph_counts(self): + """Compare paragraph counts between original and new document.""" + original_count = self.count_paragraphs_in_original() + new_count = self.count_paragraphs_in_unpacked() + + diff = new_count - original_count + diff_str = f"+{diff}" if diff > 0 else str(diff) + print(f"\nParagraphs: {original_count} โ†’ {new_count} ({diff_str})") + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validation/pptx.py b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validation/pptx.py new file mode 100644 index 00000000..66d5b1e2 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validation/pptx.py @@ -0,0 +1,315 @@ +""" +Validator for PowerPoint presentation XML files against XSD schemas. +""" + +import re + +from .base import BaseSchemaValidator + + +class PPTXSchemaValidator(BaseSchemaValidator): + """Validator for PowerPoint presentation XML files against XSD schemas.""" + + # PowerPoint presentation namespace + PRESENTATIONML_NAMESPACE = ( + "http://schemas.openxmlformats.org/presentationml/2006/main" + ) + + # PowerPoint-specific element to relationship type mappings + ELEMENT_RELATIONSHIP_TYPES = { + "sldid": "slide", + "sldmasterid": "slidemaster", + "notesmasterid": "notesmaster", + "sldlayoutid": "slidelayout", + "themeid": "theme", + "tablestyleid": "tablestyles", + } + + def validate(self): + """Run all validation checks and return True if all pass.""" + # Test 0: XML well-formedness + if not self.validate_xml(): + return False + + # Test 1: Namespace declarations + all_valid = True + if not self.validate_namespaces(): + all_valid = False + + # Test 2: Unique IDs + if not self.validate_unique_ids(): + all_valid = False + + # Test 3: UUID ID validation + if not self.validate_uuid_ids(): + all_valid = False + + # Test 4: Relationship and file reference validation + if not self.validate_file_references(): + all_valid = False + + # Test 5: Slide layout ID validation + if not self.validate_slide_layout_ids(): + all_valid = False + + # Test 6: Content type declarations + if not self.validate_content_types(): + all_valid = False + + # Test 7: XSD schema validation + if not self.validate_against_xsd(): + all_valid = False + + # Test 8: Notes slide reference validation + if not self.validate_notes_slide_references(): + all_valid = False + + # Test 9: Relationship ID reference validation + if not self.validate_all_relationship_ids(): + all_valid = False + + # Test 10: Duplicate slide layout references validation + if not self.validate_no_duplicate_slide_layouts(): + all_valid = False + + return all_valid + + def validate_uuid_ids(self): + """Validate that ID attributes that look like UUIDs contain only hex values.""" + import lxml.etree + + errors = [] + # UUID pattern: 8-4-4-4-12 hex digits with optional braces/hyphens + uuid_pattern = re.compile( + r"^[\{\(]?[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}[\}\)]?$" + ) + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + # Check all elements for ID attributes + for elem in root.iter(): + for attr, value in elem.attrib.items(): + # Check if this is an ID attribute + attr_name = attr.split("}")[-1].lower() + if attr_name == "id" or attr_name.endswith("id"): + # Check if value looks like a UUID (has the right length and pattern structure) + if self._looks_like_uuid(value): + # Validate that it contains only hex characters in the right positions + if not uuid_pattern.match(value): + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: ID '{value}' appears to be a UUID but contains invalid hex characters" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} UUID ID validation errors:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All UUID-like IDs contain valid hex values") + return True + + def _looks_like_uuid(self, value): + """Check if a value has the general structure of a UUID.""" + # Remove common UUID delimiters + clean_value = value.strip("{}()").replace("-", "") + # Check if it's 32 hex-like characters (could include invalid hex chars) + return len(clean_value) == 32 and all(c.isalnum() for c in clean_value) + + def validate_slide_layout_ids(self): + """Validate that sldLayoutId elements in slide masters reference valid slide layouts.""" + import lxml.etree + + errors = [] + + # Find all slide master files + slide_masters = list(self.unpacked_dir.glob("ppt/slideMasters/*.xml")) + + if not slide_masters: + if self.verbose: + print("PASSED - No slide masters found") + return True + + for slide_master in slide_masters: + try: + # Parse the slide master file + root = lxml.etree.parse(str(slide_master)).getroot() + + # Find the corresponding _rels file for this slide master + rels_file = slide_master.parent / "_rels" / f"{slide_master.name}.rels" + + if not rels_file.exists(): + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: " + f"Missing relationships file: {rels_file.relative_to(self.unpacked_dir)}" + ) + continue + + # Parse the relationships file + rels_root = lxml.etree.parse(str(rels_file)).getroot() + + # Build a set of valid relationship IDs that point to slide layouts + valid_layout_rids = set() + for rel in rels_root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rel_type = rel.get("Type", "") + if "slideLayout" in rel_type: + valid_layout_rids.add(rel.get("Id")) + + # Find all sldLayoutId elements in the slide master + for sld_layout_id in root.findall( + f".//{{{self.PRESENTATIONML_NAMESPACE}}}sldLayoutId" + ): + r_id = sld_layout_id.get( + f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id" + ) + layout_id = sld_layout_id.get("id") + + if r_id and r_id not in valid_layout_rids: + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: " + f"Line {sld_layout_id.sourceline}: sldLayoutId with id='{layout_id}' " + f"references r:id='{r_id}' which is not found in slide layout relationships" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} slide layout ID validation errors:") + for error in errors: + print(error) + print( + "Remove invalid references or add missing slide layouts to the relationships file." + ) + return False + else: + if self.verbose: + print("PASSED - All slide layout IDs reference valid slide layouts") + return True + + def validate_no_duplicate_slide_layouts(self): + """Validate that each slide has exactly one slideLayout reference.""" + import lxml.etree + + errors = [] + slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) + + for rels_file in slide_rels_files: + try: + root = lxml.etree.parse(str(rels_file)).getroot() + + # Find all slideLayout relationships + layout_rels = [ + rel + for rel in root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ) + if "slideLayout" in rel.get("Type", "") + ] + + if len(layout_rels) > 1: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: has {len(layout_rels)} slideLayout references" + ) + + except Exception as e: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print("FAILED - Found slides with duplicate slideLayout references:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All slides have exactly one slideLayout reference") + return True + + def validate_notes_slide_references(self): + """Validate that each notesSlide file is referenced by only one slide.""" + import lxml.etree + + errors = [] + notes_slide_references = {} # Track which slides reference each notesSlide + + # Find all slide relationship files + slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) + + if not slide_rels_files: + if self.verbose: + print("PASSED - No slide relationship files found") + return True + + for rels_file in slide_rels_files: + try: + # Parse the relationships file + root = lxml.etree.parse(str(rels_file)).getroot() + + # Find all notesSlide relationships + for rel in root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rel_type = rel.get("Type", "") + if "notesSlide" in rel_type: + target = rel.get("Target", "") + if target: + # Normalize the target path to handle relative paths + normalized_target = target.replace("../", "") + + # Track which slide references this notesSlide + slide_name = rels_file.stem.replace( + ".xml", "" + ) # e.g., "slide1" + + if normalized_target not in notes_slide_references: + notes_slide_references[normalized_target] = [] + notes_slide_references[normalized_target].append( + (slide_name, rels_file) + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + # Check for duplicate references + for target, references in notes_slide_references.items(): + if len(references) > 1: + slide_names = [ref[0] for ref in references] + errors.append( + f" Notes slide '{target}' is referenced by multiple slides: {', '.join(slide_names)}" + ) + for slide_name, rels_file in references: + errors.append(f" - {rels_file.relative_to(self.unpacked_dir)}") + + if errors: + print( + f"FAILED - Found {len([e for e in errors if not e.startswith(' ')])} notes slide reference validation errors:" + ) + for error in errors: + print(error) + print("Each slide may optionally have its own slide file.") + return False + else: + if self.verbose: + print("PASSED - All notes slide references are unique") + return True + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validation/redlining.py b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validation/redlining.py new file mode 100644 index 00000000..7ed425ed --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/ooxml/scripts/validation/redlining.py @@ -0,0 +1,279 @@ +""" +Validator for tracked changes in Word documents. +""" + +import subprocess +import tempfile +import zipfile +from pathlib import Path + + +class RedliningValidator: + """Validator for tracked changes in Word documents.""" + + def __init__(self, unpacked_dir, original_docx, verbose=False): + self.unpacked_dir = Path(unpacked_dir) + self.original_docx = Path(original_docx) + self.verbose = verbose + self.namespaces = { + "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + } + + def validate(self): + """Main validation method that returns True if valid, False otherwise.""" + # Verify unpacked directory exists and has correct structure + modified_file = self.unpacked_dir / "word" / "document.xml" + if not modified_file.exists(): + print(f"FAILED - Modified document.xml not found at {modified_file}") + return False + + # First, check if there are any tracked changes by Claude to validate + try: + import xml.etree.ElementTree as ET + + tree = ET.parse(modified_file) + root = tree.getroot() + + # Check for w:del or w:ins tags authored by Claude + del_elements = root.findall(".//w:del", self.namespaces) + ins_elements = root.findall(".//w:ins", self.namespaces) + + # Filter to only include changes by Claude + claude_del_elements = [ + elem + for elem in del_elements + if elem.get(f"{{{self.namespaces['w']}}}author") == "Claude" + ] + claude_ins_elements = [ + elem + for elem in ins_elements + if elem.get(f"{{{self.namespaces['w']}}}author") == "Claude" + ] + + # Redlining validation is only needed if tracked changes by Claude have been used. + if not claude_del_elements and not claude_ins_elements: + if self.verbose: + print("PASSED - No tracked changes by Claude found.") + return True + + except Exception: + # If we can't parse the XML, continue with full validation + pass + + # Create temporary directory for unpacking original docx + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Unpack original docx + try: + with zipfile.ZipFile(self.original_docx, "r") as zip_ref: + zip_ref.extractall(temp_path) + except Exception as e: + print(f"FAILED - Error unpacking original docx: {e}") + return False + + original_file = temp_path / "word" / "document.xml" + if not original_file.exists(): + print( + f"FAILED - Original document.xml not found in {self.original_docx}" + ) + return False + + # Parse both XML files using xml.etree.ElementTree for redlining validation + try: + import xml.etree.ElementTree as ET + + modified_tree = ET.parse(modified_file) + modified_root = modified_tree.getroot() + original_tree = ET.parse(original_file) + original_root = original_tree.getroot() + except ET.ParseError as e: + print(f"FAILED - Error parsing XML files: {e}") + return False + + # Remove Claude's tracked changes from both documents + self._remove_claude_tracked_changes(original_root) + self._remove_claude_tracked_changes(modified_root) + + # Extract and compare text content + modified_text = self._extract_text_content(modified_root) + original_text = self._extract_text_content(original_root) + + if modified_text != original_text: + # Show detailed character-level differences for each paragraph + error_message = self._generate_detailed_diff( + original_text, modified_text + ) + print(error_message) + return False + + if self.verbose: + print("PASSED - All changes by Claude are properly tracked") + return True + + def _generate_detailed_diff(self, original_text, modified_text): + """Generate detailed word-level differences using git word diff.""" + error_parts = [ + "FAILED - Document text doesn't match after removing Claude's tracked changes", + "", + "Likely causes:", + " 1. Modified text inside another author's or tags", + " 2. Made edits without proper tracked changes", + " 3. Didn't nest inside when deleting another's insertion", + "", + "For pre-redlined documents, use correct patterns:", + " - To reject another's INSERTION: Nest inside their ", + " - To restore another's DELETION: Add new AFTER their ", + "", + ] + + # Show git word diff + git_diff = self._get_git_word_diff(original_text, modified_text) + if git_diff: + error_parts.extend(["Differences:", "============", git_diff]) + else: + error_parts.append("Unable to generate word diff (git not available)") + + return "\n".join(error_parts) + + def _get_git_word_diff(self, original_text, modified_text): + """Generate word diff using git with character-level precision.""" + try: + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create two files + original_file = temp_path / "original.txt" + modified_file = temp_path / "modified.txt" + + original_file.write_text(original_text, encoding="utf-8") + modified_file.write_text(modified_text, encoding="utf-8") + + # Try character-level diff first for precise differences + result = subprocess.run( + [ + "git", + "diff", + "--word-diff=plain", + "--word-diff-regex=.", # Character-by-character diff + "-U0", # Zero lines of context - show only changed lines + "--no-index", + str(original_file), + str(modified_file), + ], + capture_output=True, + text=True, + ) + + if result.stdout.strip(): + # Clean up the output - remove git diff header lines + lines = result.stdout.split("\n") + # Skip the header lines (diff --git, index, +++, ---, @@) + content_lines = [] + in_content = False + for line in lines: + if line.startswith("@@"): + in_content = True + continue + if in_content and line.strip(): + content_lines.append(line) + + if content_lines: + return "\n".join(content_lines) + + # Fallback to word-level diff if character-level is too verbose + result = subprocess.run( + [ + "git", + "diff", + "--word-diff=plain", + "-U0", # Zero lines of context + "--no-index", + str(original_file), + str(modified_file), + ], + capture_output=True, + text=True, + ) + + if result.stdout.strip(): + lines = result.stdout.split("\n") + content_lines = [] + in_content = False + for line in lines: + if line.startswith("@@"): + in_content = True + continue + if in_content and line.strip(): + content_lines.append(line) + return "\n".join(content_lines) + + except (subprocess.CalledProcessError, FileNotFoundError, Exception): + # Git not available or other error, return None to use fallback + pass + + return None + + def _remove_claude_tracked_changes(self, root): + """Remove tracked changes authored by Claude from the XML root.""" + ins_tag = f"{{{self.namespaces['w']}}}ins" + del_tag = f"{{{self.namespaces['w']}}}del" + author_attr = f"{{{self.namespaces['w']}}}author" + + # Remove w:ins elements + for parent in root.iter(): + to_remove = [] + for child in parent: + if child.tag == ins_tag and child.get(author_attr) == "Claude": + to_remove.append(child) + for elem in to_remove: + parent.remove(elem) + + # Unwrap content in w:del elements where author is "Claude" + deltext_tag = f"{{{self.namespaces['w']}}}delText" + t_tag = f"{{{self.namespaces['w']}}}t" + + for parent in root.iter(): + to_process = [] + for child in parent: + if child.tag == del_tag and child.get(author_attr) == "Claude": + to_process.append((child, list(parent).index(child))) + + # Process in reverse order to maintain indices + for del_elem, del_index in reversed(to_process): + # Convert w:delText to w:t before moving + for elem in del_elem.iter(): + if elem.tag == deltext_tag: + elem.tag = t_tag + + # Move all children of w:del to its parent before removing w:del + for child in reversed(list(del_elem)): + parent.insert(del_index, child) + parent.remove(del_elem) + + def _extract_text_content(self, root): + """Extract text content from Word XML, preserving paragraph structure. + + Empty paragraphs are skipped to avoid false positives when tracked + insertions add only structural elements without text content. + """ + p_tag = f"{{{self.namespaces['w']}}}p" + t_tag = f"{{{self.namespaces['w']}}}t" + + paragraphs = [] + for p_elem in root.findall(f".//{p_tag}"): + # Get all text elements within this paragraph + text_parts = [] + for t_elem in p_elem.findall(f".//{t_tag}"): + if t_elem.text: + text_parts.append(t_elem.text) + paragraph_text = "".join(text_parts) + # Skip empty paragraphs - they don't affect content validation + if paragraph_text: + paragraphs.append(paragraph_text) + + return "\n".join(paragraphs) + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/__init__.py b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/__init__.py new file mode 100755 index 00000000..bf9c5627 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/__init__.py @@ -0,0 +1 @@ +# Make scripts directory a package for relative imports in tests diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/document.py b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/document.py new file mode 100755 index 00000000..ae9328dd --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/document.py @@ -0,0 +1,1276 @@ +#!/usr/bin/env python3 +""" +Library for working with Word documents: comments, tracked changes, and editing. + +Usage: + from skills.docx.scripts.document import Document + + # Initialize + doc = Document('workspace/unpacked') + doc = Document('workspace/unpacked', author="John Doe", initials="JD") + + # Find nodes + node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"}) + node = doc["word/document.xml"].get_node(tag="w:p", line_number=10) + + # Add comments + doc.add_comment(start=node, end=node, text="Comment text") + doc.reply_to_comment(parent_comment_id=0, text="Reply text") + + # Suggest tracked changes + doc["word/document.xml"].suggest_deletion(node) # Delete content + doc["word/document.xml"].revert_insertion(ins_node) # Reject insertion + doc["word/document.xml"].revert_deletion(del_node) # Reject deletion + + # Save + doc.save() +""" + +import html +import random +import shutil +import tempfile +from datetime import datetime, timezone +from pathlib import Path + +from defusedxml import minidom +from ooxml.scripts.pack import pack_document +from ooxml.scripts.validation.docx import DOCXSchemaValidator +from ooxml.scripts.validation.redlining import RedliningValidator + +from .utilities import XMLEditor + +# Path to template files +TEMPLATE_DIR = Path(__file__).parent / "templates" + + +class DocxXMLEditor(XMLEditor): + """XMLEditor that automatically applies RSID, author, and date to new elements. + + Automatically adds attributes to elements that support them when inserting new content: + - w:rsidR, w:rsidRDefault, w:rsidP (for w:p and w:r elements) + - w:author and w:date (for w:ins, w:del, w:comment elements) + - w:id (for w:ins and w:del elements) + + Attributes: + dom (defusedxml.minidom.Document): The DOM document for direct manipulation + """ + + def __init__( + self, xml_path, rsid: str, author: str = "Claude", initials: str = "C" + ): + """Initialize with required RSID and optional author. + + Args: + xml_path: Path to XML file to edit + rsid: RSID to automatically apply to new elements + author: Author name for tracked changes and comments (default: "Claude") + initials: Author initials (default: "C") + """ + super().__init__(xml_path) + self.rsid = rsid + self.author = author + self.initials = initials + + def _get_next_change_id(self): + """Get the next available change ID by checking all tracked change elements.""" + max_id = -1 + for tag in ("w:ins", "w:del"): + elements = self.dom.getElementsByTagName(tag) + for elem in elements: + change_id = elem.getAttribute("w:id") + if change_id: + try: + max_id = max(max_id, int(change_id)) + except ValueError: + pass + return max_id + 1 + + def _ensure_w16du_namespace(self): + """Ensure w16du namespace is declared on the root element.""" + root = self.dom.documentElement + if not root.hasAttribute("xmlns:w16du"): # type: ignore + root.setAttribute( # type: ignore + "xmlns:w16du", + "http://schemas.microsoft.com/office/word/2023/wordml/word16du", + ) + + def _ensure_w16cex_namespace(self): + """Ensure w16cex namespace is declared on the root element.""" + root = self.dom.documentElement + if not root.hasAttribute("xmlns:w16cex"): # type: ignore + root.setAttribute( # type: ignore + "xmlns:w16cex", + "http://schemas.microsoft.com/office/word/2018/wordml/cex", + ) + + def _ensure_w14_namespace(self): + """Ensure w14 namespace is declared on the root element.""" + root = self.dom.documentElement + if not root.hasAttribute("xmlns:w14"): # type: ignore + root.setAttribute( # type: ignore + "xmlns:w14", + "http://schemas.microsoft.com/office/word/2010/wordml", + ) + + def _inject_attributes_to_nodes(self, nodes): + """Inject RSID, author, and date attributes into DOM nodes where applicable. + + Adds attributes to elements that support them: + - w:r: gets w:rsidR (or w:rsidDel if inside w:del) + - w:p: gets w:rsidR, w:rsidRDefault, w:rsidP, w14:paraId, w14:textId + - w:t: gets xml:space="preserve" if text has leading/trailing whitespace + - w:ins, w:del: get w:id, w:author, w:date, w16du:dateUtc + - w:comment: gets w:author, w:date, w:initials + - w16cex:commentExtensible: gets w16cex:dateUtc + + Args: + nodes: List of DOM nodes to process + """ + from datetime import datetime, timezone + + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + def is_inside_deletion(elem): + """Check if element is inside a w:del element.""" + parent = elem.parentNode + while parent: + if parent.nodeType == parent.ELEMENT_NODE and parent.tagName == "w:del": + return True + parent = parent.parentNode + return False + + def add_rsid_to_p(elem): + if not elem.hasAttribute("w:rsidR"): + elem.setAttribute("w:rsidR", self.rsid) + if not elem.hasAttribute("w:rsidRDefault"): + elem.setAttribute("w:rsidRDefault", self.rsid) + if not elem.hasAttribute("w:rsidP"): + elem.setAttribute("w:rsidP", self.rsid) + # Add w14:paraId and w14:textId if not present + if not elem.hasAttribute("w14:paraId"): + self._ensure_w14_namespace() + elem.setAttribute("w14:paraId", _generate_hex_id()) + if not elem.hasAttribute("w14:textId"): + self._ensure_w14_namespace() + elem.setAttribute("w14:textId", _generate_hex_id()) + + def add_rsid_to_r(elem): + # Use w:rsidDel for inside , otherwise w:rsidR + if is_inside_deletion(elem): + if not elem.hasAttribute("w:rsidDel"): + elem.setAttribute("w:rsidDel", self.rsid) + else: + if not elem.hasAttribute("w:rsidR"): + elem.setAttribute("w:rsidR", self.rsid) + + def add_tracked_change_attrs(elem): + # Auto-assign w:id if not present + if not elem.hasAttribute("w:id"): + elem.setAttribute("w:id", str(self._get_next_change_id())) + if not elem.hasAttribute("w:author"): + elem.setAttribute("w:author", self.author) + if not elem.hasAttribute("w:date"): + elem.setAttribute("w:date", timestamp) + # Add w16du:dateUtc for tracked changes (same as w:date since we generate UTC timestamps) + if elem.tagName in ("w:ins", "w:del") and not elem.hasAttribute( + "w16du:dateUtc" + ): + self._ensure_w16du_namespace() + elem.setAttribute("w16du:dateUtc", timestamp) + + def add_comment_attrs(elem): + if not elem.hasAttribute("w:author"): + elem.setAttribute("w:author", self.author) + if not elem.hasAttribute("w:date"): + elem.setAttribute("w:date", timestamp) + if not elem.hasAttribute("w:initials"): + elem.setAttribute("w:initials", self.initials) + + def add_comment_extensible_date(elem): + # Add w16cex:dateUtc for comment extensible elements + if not elem.hasAttribute("w16cex:dateUtc"): + self._ensure_w16cex_namespace() + elem.setAttribute("w16cex:dateUtc", timestamp) + + def add_xml_space_to_t(elem): + # Add xml:space="preserve" to w:t if text has leading/trailing whitespace + if ( + elem.firstChild + and elem.firstChild.nodeType == elem.firstChild.TEXT_NODE + ): + text = elem.firstChild.data + if text and (text[0].isspace() or text[-1].isspace()): + if not elem.hasAttribute("xml:space"): + elem.setAttribute("xml:space", "preserve") + + for node in nodes: + if node.nodeType != node.ELEMENT_NODE: + continue + + # Handle the node itself + if node.tagName == "w:p": + add_rsid_to_p(node) + elif node.tagName == "w:r": + add_rsid_to_r(node) + elif node.tagName == "w:t": + add_xml_space_to_t(node) + elif node.tagName in ("w:ins", "w:del"): + add_tracked_change_attrs(node) + elif node.tagName == "w:comment": + add_comment_attrs(node) + elif node.tagName == "w16cex:commentExtensible": + add_comment_extensible_date(node) + + # Process descendants (getElementsByTagName doesn't return the element itself) + for elem in node.getElementsByTagName("w:p"): + add_rsid_to_p(elem) + for elem in node.getElementsByTagName("w:r"): + add_rsid_to_r(elem) + for elem in node.getElementsByTagName("w:t"): + add_xml_space_to_t(elem) + for tag in ("w:ins", "w:del"): + for elem in node.getElementsByTagName(tag): + add_tracked_change_attrs(elem) + for elem in node.getElementsByTagName("w:comment"): + add_comment_attrs(elem) + for elem in node.getElementsByTagName("w16cex:commentExtensible"): + add_comment_extensible_date(elem) + + def replace_node(self, elem, new_content): + """Replace node with automatic attribute injection.""" + nodes = super().replace_node(elem, new_content) + self._inject_attributes_to_nodes(nodes) + return nodes + + def insert_after(self, elem, xml_content): + """Insert after with automatic attribute injection.""" + nodes = super().insert_after(elem, xml_content) + self._inject_attributes_to_nodes(nodes) + return nodes + + def insert_before(self, elem, xml_content): + """Insert before with automatic attribute injection.""" + nodes = super().insert_before(elem, xml_content) + self._inject_attributes_to_nodes(nodes) + return nodes + + def append_to(self, elem, xml_content): + """Append to with automatic attribute injection.""" + nodes = super().append_to(elem, xml_content) + self._inject_attributes_to_nodes(nodes) + return nodes + + def revert_insertion(self, elem): + """Reject an insertion by wrapping its content in a deletion. + + Wraps all runs inside w:ins in w:del, converting w:t to w:delText. + Can process a single w:ins element or a container element with multiple w:ins. + + Args: + elem: Element to process (w:ins, w:p, w:body, etc.) + + Returns: + list: List containing the processed element(s) + + Raises: + ValueError: If the element contains no w:ins elements + + Example: + # Reject a single insertion + ins = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"}) + doc["word/document.xml"].revert_insertion(ins) + + # Reject all insertions in a paragraph + para = doc["word/document.xml"].get_node(tag="w:p", line_number=42) + doc["word/document.xml"].revert_insertion(para) + """ + # Collect insertions + ins_elements = [] + if elem.tagName == "w:ins": + ins_elements.append(elem) + else: + ins_elements.extend(elem.getElementsByTagName("w:ins")) + + # Validate that there are insertions to reject + if not ins_elements: + raise ValueError( + f"revert_insertion requires w:ins elements. " + f"The provided element <{elem.tagName}> contains no insertions. " + ) + + # Process all insertions - wrap all children in w:del + for ins_elem in ins_elements: + runs = list(ins_elem.getElementsByTagName("w:r")) + if not runs: + continue + + # Create deletion wrapper + del_wrapper = self.dom.createElement("w:del") + + # Process each run + for run in runs: + # Convert w:t โ†’ w:delText and w:rsidR โ†’ w:rsidDel + if run.hasAttribute("w:rsidR"): + run.setAttribute("w:rsidDel", run.getAttribute("w:rsidR")) + run.removeAttribute("w:rsidR") + elif not run.hasAttribute("w:rsidDel"): + run.setAttribute("w:rsidDel", self.rsid) + + for t_elem in list(run.getElementsByTagName("w:t")): + del_text = self.dom.createElement("w:delText") + # Copy ALL child nodes (not just firstChild) to handle entities + while t_elem.firstChild: + del_text.appendChild(t_elem.firstChild) + for i in range(t_elem.attributes.length): + attr = t_elem.attributes.item(i) + del_text.setAttribute(attr.name, attr.value) + t_elem.parentNode.replaceChild(del_text, t_elem) + + # Move all children from ins to del wrapper + while ins_elem.firstChild: + del_wrapper.appendChild(ins_elem.firstChild) + + # Add del wrapper back to ins + ins_elem.appendChild(del_wrapper) + + # Inject attributes to the deletion wrapper + self._inject_attributes_to_nodes([del_wrapper]) + + return [elem] + + def revert_deletion(self, elem): + """Reject a deletion by re-inserting the deleted content. + + Creates w:ins elements after each w:del, copying deleted content and + converting w:delText back to w:t. + Can process a single w:del element or a container element with multiple w:del. + + Args: + elem: Element to process (w:del, w:p, w:body, etc.) + + Returns: + list: If elem is w:del, returns [elem, new_ins]. Otherwise returns [elem]. + + Raises: + ValueError: If the element contains no w:del elements + + Example: + # Reject a single deletion - returns [w:del, w:ins] + del_elem = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "3"}) + nodes = doc["word/document.xml"].revert_deletion(del_elem) + + # Reject all deletions in a paragraph - returns [para] + para = doc["word/document.xml"].get_node(tag="w:p", line_number=42) + nodes = doc["word/document.xml"].revert_deletion(para) + """ + # Collect deletions FIRST - before we modify the DOM + del_elements = [] + is_single_del = elem.tagName == "w:del" + + if is_single_del: + del_elements.append(elem) + else: + del_elements.extend(elem.getElementsByTagName("w:del")) + + # Validate that there are deletions to reject + if not del_elements: + raise ValueError( + f"revert_deletion requires w:del elements. " + f"The provided element <{elem.tagName}> contains no deletions. " + ) + + # Track created insertion (only relevant if elem is a single w:del) + created_insertion = None + + # Process all deletions - create insertions that copy the deleted content + for del_elem in del_elements: + # Clone the deleted runs and convert them to insertions + runs = list(del_elem.getElementsByTagName("w:r")) + if not runs: + continue + + # Create insertion wrapper + ins_elem = self.dom.createElement("w:ins") + + for run in runs: + # Clone the run + new_run = run.cloneNode(True) + + # Convert w:delText โ†’ w:t + for del_text in list(new_run.getElementsByTagName("w:delText")): + t_elem = self.dom.createElement("w:t") + # Copy ALL child nodes (not just firstChild) to handle entities + while del_text.firstChild: + t_elem.appendChild(del_text.firstChild) + for i in range(del_text.attributes.length): + attr = del_text.attributes.item(i) + t_elem.setAttribute(attr.name, attr.value) + del_text.parentNode.replaceChild(t_elem, del_text) + + # Update run attributes: w:rsidDel โ†’ w:rsidR + if new_run.hasAttribute("w:rsidDel"): + new_run.setAttribute("w:rsidR", new_run.getAttribute("w:rsidDel")) + new_run.removeAttribute("w:rsidDel") + elif not new_run.hasAttribute("w:rsidR"): + new_run.setAttribute("w:rsidR", self.rsid) + + ins_elem.appendChild(new_run) + + # Insert the new insertion after the deletion + nodes = self.insert_after(del_elem, ins_elem.toxml()) + + # If processing a single w:del, track the created insertion + if is_single_del and nodes: + created_insertion = nodes[0] + + # Return based on input type + if is_single_del and created_insertion: + return [elem, created_insertion] + else: + return [elem] + + @staticmethod + def suggest_paragraph(xml_content: str) -> str: + """Transform paragraph XML to add tracked change wrapping for insertion. + + Wraps runs in and adds to w:rPr in w:pPr for numbered lists. + + Args: + xml_content: XML string containing a element + + Returns: + str: Transformed XML with tracked change wrapping + """ + wrapper = f'{xml_content}' + doc = minidom.parseString(wrapper) + para = doc.getElementsByTagName("w:p")[0] + + # Ensure w:pPr exists + pPr_list = para.getElementsByTagName("w:pPr") + if not pPr_list: + pPr = doc.createElement("w:pPr") + para.insertBefore( + pPr, para.firstChild + ) if para.firstChild else para.appendChild(pPr) + else: + pPr = pPr_list[0] + + # Ensure w:rPr exists in w:pPr + rPr_list = pPr.getElementsByTagName("w:rPr") + if not rPr_list: + rPr = doc.createElement("w:rPr") + pPr.appendChild(rPr) + else: + rPr = rPr_list[0] + + # Add to w:rPr + ins_marker = doc.createElement("w:ins") + rPr.insertBefore( + ins_marker, rPr.firstChild + ) if rPr.firstChild else rPr.appendChild(ins_marker) + + # Wrap all non-pPr children in + ins_wrapper = doc.createElement("w:ins") + for child in [c for c in para.childNodes if c.nodeName != "w:pPr"]: + para.removeChild(child) + ins_wrapper.appendChild(child) + para.appendChild(ins_wrapper) + + return para.toxml() + + def suggest_deletion(self, elem): + """Mark a w:r or w:p element as deleted with tracked changes (in-place DOM manipulation). + + For w:r: wraps in , converts to , preserves w:rPr + For w:p (regular): wraps content in , converts to + For w:p (numbered list): adds to w:rPr in w:pPr, wraps content in + + Args: + elem: A w:r or w:p DOM element without existing tracked changes + + Returns: + Element: The modified element + + Raises: + ValueError: If element has existing tracked changes or invalid structure + """ + if elem.nodeName == "w:r": + # Check for existing w:delText + if elem.getElementsByTagName("w:delText"): + raise ValueError("w:r element already contains w:delText") + + # Convert w:t โ†’ w:delText + for t_elem in list(elem.getElementsByTagName("w:t")): + del_text = self.dom.createElement("w:delText") + # Copy ALL child nodes (not just firstChild) to handle entities + while t_elem.firstChild: + del_text.appendChild(t_elem.firstChild) + # Preserve attributes like xml:space + for i in range(t_elem.attributes.length): + attr = t_elem.attributes.item(i) + del_text.setAttribute(attr.name, attr.value) + t_elem.parentNode.replaceChild(del_text, t_elem) + + # Update run attributes: w:rsidR โ†’ w:rsidDel + if elem.hasAttribute("w:rsidR"): + elem.setAttribute("w:rsidDel", elem.getAttribute("w:rsidR")) + elem.removeAttribute("w:rsidR") + elif not elem.hasAttribute("w:rsidDel"): + elem.setAttribute("w:rsidDel", self.rsid) + + # Wrap in w:del + del_wrapper = self.dom.createElement("w:del") + parent = elem.parentNode + parent.insertBefore(del_wrapper, elem) + parent.removeChild(elem) + del_wrapper.appendChild(elem) + + # Inject attributes to the deletion wrapper + self._inject_attributes_to_nodes([del_wrapper]) + + return del_wrapper + + elif elem.nodeName == "w:p": + # Check for existing tracked changes + if elem.getElementsByTagName("w:ins") or elem.getElementsByTagName("w:del"): + raise ValueError("w:p element already contains tracked changes") + + # Check if it's a numbered list item + pPr_list = elem.getElementsByTagName("w:pPr") + is_numbered = pPr_list and pPr_list[0].getElementsByTagName("w:numPr") + + if is_numbered: + # Add to w:rPr in w:pPr + pPr = pPr_list[0] + rPr_list = pPr.getElementsByTagName("w:rPr") + + if not rPr_list: + rPr = self.dom.createElement("w:rPr") + pPr.appendChild(rPr) + else: + rPr = rPr_list[0] + + # Add marker + del_marker = self.dom.createElement("w:del") + rPr.insertBefore( + del_marker, rPr.firstChild + ) if rPr.firstChild else rPr.appendChild(del_marker) + + # Convert w:t โ†’ w:delText in all runs + for t_elem in list(elem.getElementsByTagName("w:t")): + del_text = self.dom.createElement("w:delText") + # Copy ALL child nodes (not just firstChild) to handle entities + while t_elem.firstChild: + del_text.appendChild(t_elem.firstChild) + # Preserve attributes like xml:space + for i in range(t_elem.attributes.length): + attr = t_elem.attributes.item(i) + del_text.setAttribute(attr.name, attr.value) + t_elem.parentNode.replaceChild(del_text, t_elem) + + # Update run attributes: w:rsidR โ†’ w:rsidDel + for run in elem.getElementsByTagName("w:r"): + if run.hasAttribute("w:rsidR"): + run.setAttribute("w:rsidDel", run.getAttribute("w:rsidR")) + run.removeAttribute("w:rsidR") + elif not run.hasAttribute("w:rsidDel"): + run.setAttribute("w:rsidDel", self.rsid) + + # Wrap all non-pPr children in + del_wrapper = self.dom.createElement("w:del") + for child in [c for c in elem.childNodes if c.nodeName != "w:pPr"]: + elem.removeChild(child) + del_wrapper.appendChild(child) + elem.appendChild(del_wrapper) + + # Inject attributes to the deletion wrapper + self._inject_attributes_to_nodes([del_wrapper]) + + return elem + + else: + raise ValueError(f"Element must be w:r or w:p, got {elem.nodeName}") + + +def _generate_hex_id() -> str: + """Generate random 8-character hex ID for para/durable IDs. + + Values are constrained to be less than 0x7FFFFFFF per OOXML spec: + - paraId must be < 0x80000000 + - durableId must be < 0x7FFFFFFF + We use the stricter constraint (0x7FFFFFFF) for both. + """ + return f"{random.randint(1, 0x7FFFFFFE):08X}" + + +def _generate_rsid() -> str: + """Generate random 8-character hex RSID.""" + return "".join(random.choices("0123456789ABCDEF", k=8)) + + +class Document: + """Manages comments in unpacked Word documents.""" + + def __init__( + self, + unpacked_dir, + rsid=None, + track_revisions=False, + author="Claude", + initials="C", + ): + """ + Initialize with path to unpacked Word document directory. + Automatically sets up comment infrastructure (people.xml, RSIDs). + + Args: + unpacked_dir: Path to unpacked DOCX directory (must contain word/ subdirectory) + rsid: Optional RSID to use for all comment elements. If not provided, one will be generated. + track_revisions: If True, enables track revisions in settings.xml (default: False) + author: Default author name for comments (default: "Claude") + initials: Default author initials for comments (default: "C") + """ + self.original_path = Path(unpacked_dir) + + if not self.original_path.exists() or not self.original_path.is_dir(): + raise ValueError(f"Directory not found: {unpacked_dir}") + + # Create temporary directory with subdirectories for unpacked content and baseline + self.temp_dir = tempfile.mkdtemp(prefix="docx_") + self.unpacked_path = Path(self.temp_dir) / "unpacked" + shutil.copytree(self.original_path, self.unpacked_path) + + # Pack original directory into temporary .docx for validation baseline (outside unpacked dir) + self.original_docx = Path(self.temp_dir) / "original.docx" + pack_document(self.original_path, self.original_docx, validate=False) + + self.word_path = self.unpacked_path / "word" + + # Generate RSID if not provided + self.rsid = rsid if rsid else _generate_rsid() + print(f"Using RSID: {self.rsid}") + + # Set default author and initials + self.author = author + self.initials = initials + + # Cache for lazy-loaded editors + self._editors = {} + + # Comment file paths + self.comments_path = self.word_path / "comments.xml" + self.comments_extended_path = self.word_path / "commentsExtended.xml" + self.comments_ids_path = self.word_path / "commentsIds.xml" + self.comments_extensible_path = self.word_path / "commentsExtensible.xml" + + # Load existing comments and determine next ID (before setup modifies files) + self.existing_comments = self._load_existing_comments() + self.next_comment_id = self._get_next_comment_id() + + # Convenient access to document.xml editor (semi-private) + self._document = self["word/document.xml"] + + # Setup tracked changes infrastructure + self._setup_tracking(track_revisions=track_revisions) + + # Add author to people.xml + self._add_author_to_people(author) + + def __getitem__(self, xml_path: str) -> DocxXMLEditor: + """ + Get or create a DocxXMLEditor for the specified XML file. + + Enables lazy-loaded editors with bracket notation: + node = doc["word/document.xml"].get_node(tag="w:p", line_number=42) + + Args: + xml_path: Relative path to XML file (e.g., "word/document.xml", "word/comments.xml") + + Returns: + DocxXMLEditor instance for the specified file + + Raises: + ValueError: If the file does not exist + + Example: + # Get node from document.xml + node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"}) + + # Get node from comments.xml + comment = doc["word/comments.xml"].get_node(tag="w:comment", attrs={"w:id": "0"}) + """ + if xml_path not in self._editors: + file_path = self.unpacked_path / xml_path + if not file_path.exists(): + raise ValueError(f"XML file not found: {xml_path}") + # Use DocxXMLEditor with RSID, author, and initials for all editors + self._editors[xml_path] = DocxXMLEditor( + file_path, rsid=self.rsid, author=self.author, initials=self.initials + ) + return self._editors[xml_path] + + def add_comment(self, start, end, text: str) -> int: + """ + Add a comment spanning from one element to another. + + Args: + start: DOM element for the starting point + end: DOM element for the ending point + text: Comment content + + Returns: + The comment ID that was created + + Example: + start_node = cm.get_document_node(tag="w:del", id="1") + end_node = cm.get_document_node(tag="w:ins", id="2") + cm.add_comment(start=start_node, end=end_node, text="Explanation") + """ + comment_id = self.next_comment_id + para_id = _generate_hex_id() + durable_id = _generate_hex_id() + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + # Add comment ranges to document.xml immediately + self._document.insert_before(start, self._comment_range_start_xml(comment_id)) + + # If end node is a paragraph, append comment markup inside it + # Otherwise insert after it (for run-level anchors) + if end.tagName == "w:p": + self._document.append_to(end, self._comment_range_end_xml(comment_id)) + else: + self._document.insert_after(end, self._comment_range_end_xml(comment_id)) + + # Add to comments.xml immediately + self._add_to_comments_xml( + comment_id, para_id, text, self.author, self.initials, timestamp + ) + + # Add to commentsExtended.xml immediately + self._add_to_comments_extended_xml(para_id, parent_para_id=None) + + # Add to commentsIds.xml immediately + self._add_to_comments_ids_xml(para_id, durable_id) + + # Add to commentsExtensible.xml immediately + self._add_to_comments_extensible_xml(durable_id) + + # Update existing_comments so replies work + self.existing_comments[comment_id] = {"para_id": para_id} + + self.next_comment_id += 1 + return comment_id + + def reply_to_comment( + self, + parent_comment_id: int, + text: str, + ) -> int: + """ + Add a reply to an existing comment. + + Args: + parent_comment_id: The w:id of the parent comment to reply to + text: Reply text + + Returns: + The comment ID that was created for the reply + + Example: + cm.reply_to_comment(parent_comment_id=0, text="I agree with this change") + """ + if parent_comment_id not in self.existing_comments: + raise ValueError(f"Parent comment with id={parent_comment_id} not found") + + parent_info = self.existing_comments[parent_comment_id] + comment_id = self.next_comment_id + para_id = _generate_hex_id() + durable_id = _generate_hex_id() + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + # Add comment ranges to document.xml immediately + parent_start_elem = self._document.get_node( + tag="w:commentRangeStart", attrs={"w:id": str(parent_comment_id)} + ) + parent_ref_elem = self._document.get_node( + tag="w:commentReference", attrs={"w:id": str(parent_comment_id)} + ) + + self._document.insert_after( + parent_start_elem, self._comment_range_start_xml(comment_id) + ) + parent_ref_run = parent_ref_elem.parentNode + self._document.insert_after( + parent_ref_run, f'' + ) + self._document.insert_after( + parent_ref_run, self._comment_ref_run_xml(comment_id) + ) + + # Add to comments.xml immediately + self._add_to_comments_xml( + comment_id, para_id, text, self.author, self.initials, timestamp + ) + + # Add to commentsExtended.xml immediately (with parent) + self._add_to_comments_extended_xml( + para_id, parent_para_id=parent_info["para_id"] + ) + + # Add to commentsIds.xml immediately + self._add_to_comments_ids_xml(para_id, durable_id) + + # Add to commentsExtensible.xml immediately + self._add_to_comments_extensible_xml(durable_id) + + # Update existing_comments so replies work + self.existing_comments[comment_id] = {"para_id": para_id} + + self.next_comment_id += 1 + return comment_id + + def __del__(self): + """Clean up temporary directory on deletion.""" + if hasattr(self, "temp_dir") and Path(self.temp_dir).exists(): + shutil.rmtree(self.temp_dir) + + def validate(self) -> None: + """ + Validate the document against XSD schema and redlining rules. + + Raises: + ValueError: If validation fails. + """ + # Create validators with current state + schema_validator = DOCXSchemaValidator( + self.unpacked_path, self.original_docx, verbose=False + ) + redlining_validator = RedliningValidator( + self.unpacked_path, self.original_docx, verbose=False + ) + + # Run validations + if not schema_validator.validate(): + raise ValueError("Schema validation failed") + if not redlining_validator.validate(): + raise ValueError("Redlining validation failed") + + def save(self, destination=None, validate=True) -> None: + """ + Save all modified XML files to disk and copy to destination directory. + + This persists all changes made via add_comment() and reply_to_comment(). + + Args: + destination: Optional path to save to. If None, saves back to original directory. + validate: If True, validates document before saving (default: True). + """ + # Only ensure comment relationships and content types if comment files exist + if self.comments_path.exists(): + self._ensure_comment_relationships() + self._ensure_comment_content_types() + + # Save all modified XML files in temp directory + for editor in self._editors.values(): + editor.save() + + # Validate by default + if validate: + self.validate() + + # Copy contents from temp directory to destination (or original directory) + target_path = Path(destination) if destination else self.original_path + shutil.copytree(self.unpacked_path, target_path, dirs_exist_ok=True) + + # ==================== Private: Initialization ==================== + + def _get_next_comment_id(self): + """Get the next available comment ID.""" + if not self.comments_path.exists(): + return 0 + + editor = self["word/comments.xml"] + max_id = -1 + for comment_elem in editor.dom.getElementsByTagName("w:comment"): + comment_id = comment_elem.getAttribute("w:id") + if comment_id: + try: + max_id = max(max_id, int(comment_id)) + except ValueError: + pass + return max_id + 1 + + def _load_existing_comments(self): + """Load existing comments from files to enable replies.""" + if not self.comments_path.exists(): + return {} + + editor = self["word/comments.xml"] + existing = {} + + for comment_elem in editor.dom.getElementsByTagName("w:comment"): + comment_id = comment_elem.getAttribute("w:id") + if not comment_id: + continue + + # Find para_id from the w:p element within the comment + para_id = None + for p_elem in comment_elem.getElementsByTagName("w:p"): + para_id = p_elem.getAttribute("w14:paraId") + if para_id: + break + + if not para_id: + continue + + existing[int(comment_id)] = {"para_id": para_id} + + return existing + + # ==================== Private: Setup Methods ==================== + + def _setup_tracking(self, track_revisions=False): + """Set up comment infrastructure in unpacked directory. + + Args: + track_revisions: If True, enables track revisions in settings.xml + """ + # Create or update word/people.xml + people_file = self.word_path / "people.xml" + self._update_people_xml(people_file) + + # Update XML files + self._add_content_type_for_people(self.unpacked_path / "[Content_Types].xml") + self._add_relationship_for_people( + self.word_path / "_rels" / "document.xml.rels" + ) + + # Always add RSID to settings.xml, optionally enable trackRevisions + self._update_settings( + self.word_path / "settings.xml", track_revisions=track_revisions + ) + + def _update_people_xml(self, path): + """Create people.xml if it doesn't exist.""" + if not path.exists(): + # Copy from template + shutil.copy(TEMPLATE_DIR / "people.xml", path) + + def _add_content_type_for_people(self, path): + """Add people.xml content type to [Content_Types].xml if not already present.""" + editor = self["[Content_Types].xml"] + + if self._has_override(editor, "/word/people.xml"): + return + + # Add Override element + root = editor.dom.documentElement + override_xml = '' + editor.append_to(root, override_xml) + + def _add_relationship_for_people(self, path): + """Add people.xml relationship to document.xml.rels if not already present.""" + editor = self["word/_rels/document.xml.rels"] + + if self._has_relationship(editor, "people.xml"): + return + + root = editor.dom.documentElement + root_tag = root.tagName # type: ignore + prefix = root_tag.split(":")[0] + ":" if ":" in root_tag else "" + next_rid = editor.get_next_rid() + + # Create the relationship entry + rel_xml = f'<{prefix}Relationship Id="{next_rid}" Type="http://schemas.microsoft.com/office/2011/relationships/people" Target="people.xml"/>' + editor.append_to(root, rel_xml) + + def _update_settings(self, path, track_revisions=False): + """Add RSID and optionally enable track revisions in settings.xml. + + Args: + path: Path to settings.xml + track_revisions: If True, adds trackRevisions element + + Places elements per OOXML schema order: + - trackRevisions: early (before defaultTabStop) + - rsids: late (after compat) + """ + editor = self["word/settings.xml"] + root = editor.get_node(tag="w:settings") + prefix = root.tagName.split(":")[0] if ":" in root.tagName else "w" + + # Conditionally add trackRevisions if requested + if track_revisions: + track_revisions_exists = any( + elem.tagName == f"{prefix}:trackRevisions" + for elem in editor.dom.getElementsByTagName(f"{prefix}:trackRevisions") + ) + + if not track_revisions_exists: + track_rev_xml = f"<{prefix}:trackRevisions/>" + # Try to insert before documentProtection, defaultTabStop, or at start + inserted = False + for tag in [f"{prefix}:documentProtection", f"{prefix}:defaultTabStop"]: + elements = editor.dom.getElementsByTagName(tag) + if elements: + editor.insert_before(elements[0], track_rev_xml) + inserted = True + break + if not inserted: + # Insert as first child of settings + if root.firstChild: + editor.insert_before(root.firstChild, track_rev_xml) + else: + editor.append_to(root, track_rev_xml) + + # Always check if rsids section exists + rsids_elements = editor.dom.getElementsByTagName(f"{prefix}:rsids") + + if not rsids_elements: + # Add new rsids section + rsids_xml = f'''<{prefix}:rsids> + <{prefix}:rsidRoot {prefix}:val="{self.rsid}"/> + <{prefix}:rsid {prefix}:val="{self.rsid}"/> +''' + + # Try to insert after compat, before clrSchemeMapping, or before closing tag + inserted = False + compat_elements = editor.dom.getElementsByTagName(f"{prefix}:compat") + if compat_elements: + editor.insert_after(compat_elements[0], rsids_xml) + inserted = True + + if not inserted: + clr_elements = editor.dom.getElementsByTagName( + f"{prefix}:clrSchemeMapping" + ) + if clr_elements: + editor.insert_before(clr_elements[0], rsids_xml) + inserted = True + + if not inserted: + editor.append_to(root, rsids_xml) + else: + # Check if this rsid already exists + rsids_elem = rsids_elements[0] + rsid_exists = any( + elem.getAttribute(f"{prefix}:val") == self.rsid + for elem in rsids_elem.getElementsByTagName(f"{prefix}:rsid") + ) + + if not rsid_exists: + rsid_xml = f'<{prefix}:rsid {prefix}:val="{self.rsid}"/>' + editor.append_to(rsids_elem, rsid_xml) + + # ==================== Private: XML File Creation ==================== + + def _add_to_comments_xml( + self, comment_id, para_id, text, author, initials, timestamp + ): + """Add a single comment to comments.xml.""" + if not self.comments_path.exists(): + shutil.copy(TEMPLATE_DIR / "comments.xml", self.comments_path) + + editor = self["word/comments.xml"] + root = editor.get_node(tag="w:comments") + + escaped_text = ( + text.replace("&", "&").replace("<", "<").replace(">", ">") + ) + # Note: w:rsidR, w:rsidRDefault, w:rsidP on w:p, w:rsidR on w:r, + # and w:author, w:date, w:initials on w:comment are automatically added by DocxXMLEditor + comment_xml = f''' + + + {escaped_text} + +''' + editor.append_to(root, comment_xml) + + def _add_to_comments_extended_xml(self, para_id, parent_para_id): + """Add a single comment to commentsExtended.xml.""" + if not self.comments_extended_path.exists(): + shutil.copy( + TEMPLATE_DIR / "commentsExtended.xml", self.comments_extended_path + ) + + editor = self["word/commentsExtended.xml"] + root = editor.get_node(tag="w15:commentsEx") + + if parent_para_id: + xml = f'' + else: + xml = f'' + editor.append_to(root, xml) + + def _add_to_comments_ids_xml(self, para_id, durable_id): + """Add a single comment to commentsIds.xml.""" + if not self.comments_ids_path.exists(): + shutil.copy(TEMPLATE_DIR / "commentsIds.xml", self.comments_ids_path) + + editor = self["word/commentsIds.xml"] + root = editor.get_node(tag="w16cid:commentsIds") + + xml = f'' + editor.append_to(root, xml) + + def _add_to_comments_extensible_xml(self, durable_id): + """Add a single comment to commentsExtensible.xml.""" + if not self.comments_extensible_path.exists(): + shutil.copy( + TEMPLATE_DIR / "commentsExtensible.xml", self.comments_extensible_path + ) + + editor = self["word/commentsExtensible.xml"] + root = editor.get_node(tag="w16cex:commentsExtensible") + + xml = f'' + editor.append_to(root, xml) + + # ==================== Private: XML Fragments ==================== + + def _comment_range_start_xml(self, comment_id): + """Generate XML for comment range start.""" + return f'' + + def _comment_range_end_xml(self, comment_id): + """Generate XML for comment range end with reference run. + + Note: w:rsidR is automatically added by DocxXMLEditor. + """ + return f''' + + + +''' + + def _comment_ref_run_xml(self, comment_id): + """Generate XML for comment reference run. + + Note: w:rsidR is automatically added by DocxXMLEditor. + """ + return f''' + + +''' + + # ==================== Private: Metadata Updates ==================== + + def _has_relationship(self, editor, target): + """Check if a relationship with given target exists.""" + for rel_elem in editor.dom.getElementsByTagName("Relationship"): + if rel_elem.getAttribute("Target") == target: + return True + return False + + def _has_override(self, editor, part_name): + """Check if an override with given part name exists.""" + for override_elem in editor.dom.getElementsByTagName("Override"): + if override_elem.getAttribute("PartName") == part_name: + return True + return False + + def _has_author(self, editor, author): + """Check if an author already exists in people.xml.""" + for person_elem in editor.dom.getElementsByTagName("w15:person"): + if person_elem.getAttribute("w15:author") == author: + return True + return False + + def _add_author_to_people(self, author): + """Add author to people.xml (called during initialization).""" + people_path = self.word_path / "people.xml" + + # people.xml should already exist from _setup_tracking + if not people_path.exists(): + raise ValueError("people.xml should exist after _setup_tracking") + + editor = self["word/people.xml"] + root = editor.get_node(tag="w15:people") + + # Check if author already exists + if self._has_author(editor, author): + return + + # Add author with proper XML escaping to prevent injection + escaped_author = html.escape(author, quote=True) + person_xml = f''' + +''' + editor.append_to(root, person_xml) + + def _ensure_comment_relationships(self): + """Ensure word/_rels/document.xml.rels has comment relationships.""" + editor = self["word/_rels/document.xml.rels"] + + if self._has_relationship(editor, "comments.xml"): + return + + root = editor.dom.documentElement + root_tag = root.tagName # type: ignore + prefix = root_tag.split(":")[0] + ":" if ":" in root_tag else "" + next_rid_num = int(editor.get_next_rid()[3:]) + + # Add relationship elements + rels = [ + ( + next_rid_num, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", + "comments.xml", + ), + ( + next_rid_num + 1, + "http://schemas.microsoft.com/office/2011/relationships/commentsExtended", + "commentsExtended.xml", + ), + ( + next_rid_num + 2, + "http://schemas.microsoft.com/office/2016/09/relationships/commentsIds", + "commentsIds.xml", + ), + ( + next_rid_num + 3, + "http://schemas.microsoft.com/office/2018/08/relationships/commentsExtensible", + "commentsExtensible.xml", + ), + ] + + for rel_id, rel_type, target in rels: + rel_xml = f'<{prefix}Relationship Id="rId{rel_id}" Type="{rel_type}" Target="{target}"/>' + editor.append_to(root, rel_xml) + + def _ensure_comment_content_types(self): + """Ensure [Content_Types].xml has comment content types.""" + editor = self["[Content_Types].xml"] + + if self._has_override(editor, "/word/comments.xml"): + return + + root = editor.dom.documentElement + + # Add Override elements + overrides = [ + ( + "/word/comments.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml", + ), + ( + "/word/commentsExtended.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml", + ), + ( + "/word/commentsIds.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml", + ), + ( + "/word/commentsExtensible.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml", + ), + ] + + for part_name, content_type in overrides: + override_xml = ( + f'' + ) + editor.append_to(root, override_xml) diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/templates/comments.xml b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/templates/comments.xml new file mode 100644 index 00000000..b5dace0e --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/templates/comments.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/templates/commentsExtended.xml b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/templates/commentsExtended.xml new file mode 100644 index 00000000..b4cf23e3 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/templates/commentsExtended.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/templates/commentsExtensible.xml b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/templates/commentsExtensible.xml new file mode 100644 index 00000000..e32a05e0 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/templates/commentsExtensible.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/templates/commentsIds.xml b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/templates/commentsIds.xml new file mode 100644 index 00000000..d04bc8e0 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/templates/commentsIds.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/templates/people.xml b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/templates/people.xml new file mode 100644 index 00000000..a839cafe --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/templates/people.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/utilities.py b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/utilities.py new file mode 100755 index 00000000..d92dae61 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/docx-official/scripts/utilities.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 +""" +Utilities for editing OOXML documents. + +This module provides XMLEditor, a tool for manipulating XML files with support for +line-number-based node finding and DOM manipulation. Each element is automatically +annotated with its original line and column position during parsing. + +Example usage: + editor = XMLEditor("document.xml") + + # Find node by line number or range + elem = editor.get_node(tag="w:r", line_number=519) + elem = editor.get_node(tag="w:p", line_number=range(100, 200)) + + # Find node by text content + elem = editor.get_node(tag="w:p", contains="specific text") + + # Find node by attributes + elem = editor.get_node(tag="w:r", attrs={"w:id": "target"}) + + # Combine filters + elem = editor.get_node(tag="w:p", line_number=range(1, 50), contains="text") + + # Replace, insert, or manipulate + new_elem = editor.replace_node(elem, "new text") + editor.insert_after(new_elem, "more") + + # Save changes + editor.save() +""" + +import html +from pathlib import Path +from typing import Optional, Union + +import defusedxml.minidom +import defusedxml.sax + + +class XMLEditor: + """ + Editor for manipulating OOXML XML files with line-number-based node finding. + + This class parses XML files and tracks the original line and column position + of each element. This enables finding nodes by their line number in the original + file, which is useful when working with Read tool output. + + Attributes: + xml_path: Path to the XML file being edited + encoding: Detected encoding of the XML file ('ascii' or 'utf-8') + dom: Parsed DOM tree with parse_position attributes on elements + """ + + def __init__(self, xml_path): + """ + Initialize with path to XML file and parse with line number tracking. + + Args: + xml_path: Path to XML file to edit (str or Path) + + Raises: + ValueError: If the XML file does not exist + """ + self.xml_path = Path(xml_path) + if not self.xml_path.exists(): + raise ValueError(f"XML file not found: {xml_path}") + + with open(self.xml_path, "rb") as f: + header = f.read(200).decode("utf-8", errors="ignore") + self.encoding = "ascii" if 'encoding="ascii"' in header else "utf-8" + + parser = _create_line_tracking_parser() + self.dom = defusedxml.minidom.parse(str(self.xml_path), parser) + + def get_node( + self, + tag: str, + attrs: Optional[dict[str, str]] = None, + line_number: Optional[Union[int, range]] = None, + contains: Optional[str] = None, + ): + """ + Get a DOM element by tag and identifier. + + Finds an element by either its line number in the original file or by + matching attribute values. Exactly one match must be found. + + Args: + tag: The XML tag name (e.g., "w:del", "w:ins", "w:r") + attrs: Dictionary of attribute name-value pairs to match (e.g., {"w:id": "1"}) + line_number: Line number (int) or line range (range) in original XML file (1-indexed) + contains: Text string that must appear in any text node within the element. + Supports both entity notation (“) and Unicode characters (\u201c). + + Returns: + defusedxml.minidom.Element: The matching DOM element + + Raises: + ValueError: If node not found or multiple matches found + + Example: + elem = editor.get_node(tag="w:r", line_number=519) + elem = editor.get_node(tag="w:r", line_number=range(100, 200)) + elem = editor.get_node(tag="w:del", attrs={"w:id": "1"}) + elem = editor.get_node(tag="w:p", attrs={"w14:paraId": "12345678"}) + elem = editor.get_node(tag="w:commentRangeStart", attrs={"w:id": "0"}) + elem = editor.get_node(tag="w:p", contains="specific text") + elem = editor.get_node(tag="w:t", contains="“Agreement") # Entity notation + elem = editor.get_node(tag="w:t", contains="\u201cAgreement") # Unicode character + """ + matches = [] + for elem in self.dom.getElementsByTagName(tag): + # Check line_number filter + if line_number is not None: + parse_pos = getattr(elem, "parse_position", (None,)) + elem_line = parse_pos[0] + + # Handle both single line number and range + if isinstance(line_number, range): + if elem_line not in line_number: + continue + else: + if elem_line != line_number: + continue + + # Check attrs filter + if attrs is not None: + if not all( + elem.getAttribute(attr_name) == attr_value + for attr_name, attr_value in attrs.items() + ): + continue + + # Check contains filter + if contains is not None: + elem_text = self._get_element_text(elem) + # Normalize the search string: convert HTML entities to Unicode characters + # This allows searching for both "“Rowan" and ""Rowan" + normalized_contains = html.unescape(contains) + if normalized_contains not in elem_text: + continue + + # If all applicable filters passed, this is a match + matches.append(elem) + + if not matches: + # Build descriptive error message + filters = [] + if line_number is not None: + line_str = ( + f"lines {line_number.start}-{line_number.stop - 1}" + if isinstance(line_number, range) + else f"line {line_number}" + ) + filters.append(f"at {line_str}") + if attrs is not None: + filters.append(f"with attributes {attrs}") + if contains is not None: + filters.append(f"containing '{contains}'") + + filter_desc = " ".join(filters) if filters else "" + base_msg = f"Node not found: <{tag}> {filter_desc}".strip() + + # Add helpful hint based on filters used + if contains: + hint = "Text may be split across elements or use different wording." + elif line_number: + hint = "Line numbers may have changed if document was modified." + elif attrs: + hint = "Verify attribute values are correct." + else: + hint = "Try adding filters (attrs, line_number, or contains)." + + raise ValueError(f"{base_msg}. {hint}") + if len(matches) > 1: + raise ValueError( + f"Multiple nodes found: <{tag}>. " + f"Add more filters (attrs, line_number, or contains) to narrow the search." + ) + return matches[0] + + def _get_element_text(self, elem): + """ + Recursively extract all text content from an element. + + Skips text nodes that contain only whitespace (spaces, tabs, newlines), + which typically represent XML formatting rather than document content. + + Args: + elem: defusedxml.minidom.Element to extract text from + + Returns: + str: Concatenated text from all non-whitespace text nodes within the element + """ + text_parts = [] + for node in elem.childNodes: + if node.nodeType == node.TEXT_NODE: + # Skip whitespace-only text nodes (XML formatting) + if node.data.strip(): + text_parts.append(node.data) + elif node.nodeType == node.ELEMENT_NODE: + text_parts.append(self._get_element_text(node)) + return "".join(text_parts) + + def replace_node(self, elem, new_content): + """ + Replace a DOM element with new XML content. + + Args: + elem: defusedxml.minidom.Element to replace + new_content: String containing XML to replace the node with + + Returns: + List[defusedxml.minidom.Node]: All inserted nodes + + Example: + new_nodes = editor.replace_node(old_elem, "text") + """ + parent = elem.parentNode + nodes = self._parse_fragment(new_content) + for node in nodes: + parent.insertBefore(node, elem) + parent.removeChild(elem) + return nodes + + def insert_after(self, elem, xml_content): + """ + Insert XML content after a DOM element. + + Args: + elem: defusedxml.minidom.Element to insert after + xml_content: String containing XML to insert + + Returns: + List[defusedxml.minidom.Node]: All inserted nodes + + Example: + new_nodes = editor.insert_after(elem, "text") + """ + parent = elem.parentNode + next_sibling = elem.nextSibling + nodes = self._parse_fragment(xml_content) + for node in nodes: + if next_sibling: + parent.insertBefore(node, next_sibling) + else: + parent.appendChild(node) + return nodes + + def insert_before(self, elem, xml_content): + """ + Insert XML content before a DOM element. + + Args: + elem: defusedxml.minidom.Element to insert before + xml_content: String containing XML to insert + + Returns: + List[defusedxml.minidom.Node]: All inserted nodes + + Example: + new_nodes = editor.insert_before(elem, "text") + """ + parent = elem.parentNode + nodes = self._parse_fragment(xml_content) + for node in nodes: + parent.insertBefore(node, elem) + return nodes + + def append_to(self, elem, xml_content): + """ + Append XML content as a child of a DOM element. + + Args: + elem: defusedxml.minidom.Element to append to + xml_content: String containing XML to append + + Returns: + List[defusedxml.minidom.Node]: All inserted nodes + + Example: + new_nodes = editor.append_to(elem, "text") + """ + nodes = self._parse_fragment(xml_content) + for node in nodes: + elem.appendChild(node) + return nodes + + def get_next_rid(self): + """Get the next available rId for relationships files.""" + max_id = 0 + for rel_elem in self.dom.getElementsByTagName("Relationship"): + rel_id = rel_elem.getAttribute("Id") + if rel_id.startswith("rId"): + try: + max_id = max(max_id, int(rel_id[3:])) + except ValueError: + pass + return f"rId{max_id + 1}" + + def save(self): + """ + Save the edited XML back to the file. + + Serializes the DOM tree and writes it back to the original file path, + preserving the original encoding (ascii or utf-8). + """ + content = self.dom.toxml(encoding=self.encoding) + self.xml_path.write_bytes(content) + + def _parse_fragment(self, xml_content): + """ + Parse XML fragment and return list of imported nodes. + + Args: + xml_content: String containing XML fragment + + Returns: + List of defusedxml.minidom.Node objects imported into this document + + Raises: + AssertionError: If fragment contains no element nodes + """ + # Extract namespace declarations from the root document element + root_elem = self.dom.documentElement + namespaces = [] + if root_elem and root_elem.attributes: + for i in range(root_elem.attributes.length): + attr = root_elem.attributes.item(i) + if attr.name.startswith("xmlns"): # type: ignore + namespaces.append(f'{attr.name}="{attr.value}"') # type: ignore + + ns_decl = " ".join(namespaces) + wrapper = f"{xml_content}" + fragment_doc = defusedxml.minidom.parseString(wrapper) + nodes = [ + self.dom.importNode(child, deep=True) + for child in fragment_doc.documentElement.childNodes # type: ignore + ] + elements = [n for n in nodes if n.nodeType == n.ELEMENT_NODE] + assert elements, "Fragment must contain at least one element" + return nodes + + +def _create_line_tracking_parser(): + """ + Create a SAX parser that tracks line and column numbers for each element. + + Monkey patches the SAX content handler to store the current line and column + position from the underlying expat parser onto each element as a parse_position + attribute (line, column) tuple. + + Returns: + defusedxml.sax.xmlreader.XMLReader: Configured SAX parser + """ + + def set_content_handler(dom_handler): + def startElementNS(name, tagName, attrs): + orig_start_cb(name, tagName, attrs) + cur_elem = dom_handler.elementStack[-1] + cur_elem.parse_position = ( + parser._parser.CurrentLineNumber, # type: ignore + parser._parser.CurrentColumnNumber, # type: ignore + ) + + orig_start_cb = dom_handler.startElementNS + dom_handler.startElementNS = startElementNS + orig_set_content_handler(dom_handler) + + parser = defusedxml.sax.make_parser() + orig_set_content_handler = parser.setContentHandler + parser.setContentHandler = set_content_handler # type: ignore + return parser diff --git a/plugins/antigravity-bundle-documents-presentations/skills/google-sheets-automation/SKILL.md b/plugins/antigravity-bundle-documents-presentations/skills/google-sheets-automation/SKILL.md new file mode 100644 index 00000000..c6d24707 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/google-sheets-automation/SKILL.md @@ -0,0 +1,144 @@ +--- +name: google-sheets-automation +description: "Lightweight Google Sheets integration with standalone OAuth authentication. No MCP server required. Full read/write access." +risk: unknown +source: community +license: Apache-2.0 +metadata: + author: sanjay3290 + version: "1.0" +--- + +# Google Sheets + +Lightweight Google Sheets integration with standalone OAuth authentication. No MCP server required. Full read/write access. + +> **Requires Google Workspace account.** Personal Gmail accounts are not supported. + +## First-Time Setup + +Authenticate with Google (opens browser): +```bash +python scripts/auth.py login +``` + +Check authentication status: +```bash +python scripts/auth.py status +``` + +Logout when needed: +```bash +python scripts/auth.py logout +``` + +## Read Commands + +All operations via `scripts/sheets.py`. Auto-authenticates on first use if not logged in. + +```bash +# Get spreadsheet content as plain text (default) +python scripts/sheets.py get-text SPREADSHEET_ID + +# Get spreadsheet content as CSV +python scripts/sheets.py get-text SPREADSHEET_ID --format csv + +# Get spreadsheet content as JSON +python scripts/sheets.py get-text SPREADSHEET_ID --format json + +# Get values from a specific range (A1 notation) +python scripts/sheets.py get-range SPREADSHEET_ID "Sheet1!A1:D10" +python scripts/sheets.py get-range SPREADSHEET_ID "A1:C5" + +# Find spreadsheets by search query +python scripts/sheets.py find "budget 2024" +python scripts/sheets.py find "sales report" --limit 5 + +# Get spreadsheet metadata (sheets, dimensions, etc.) +python scripts/sheets.py get-metadata SPREADSHEET_ID +``` + +## Write Commands + +```bash +# Update a range of cells with values (JSON 2D array) +python scripts/sheets.py update-range SPREADSHEET_ID "Sheet1!A1:B2" '[["Hello","World"],["Foo","Bar"]]' + +# Update with RAW input (no formula parsing, treats everything as literal text) +python scripts/sheets.py update-range SPREADSHEET_ID "Sheet1!A1:B1" '[["=SUM(A1:A5)","text"]]' --raw + +# Append rows after the last data row +python scripts/sheets.py append-rows SPREADSHEET_ID "Sheet1!A:Z" '[["New Row Col A","New Row Col B"]]' + +# Clear values from a range (keeps formatting) +python scripts/sheets.py clear-range SPREADSHEET_ID "Sheet1!A1:B10" + +# Batch update (advanced - for formatting, merging, etc.) +python scripts/sheets.py batch-update SPREADSHEET_ID '[{"updateCells":{"range":{"sheetId":0},"fields":"userEnteredValue"}}]' +``` + +## Spreadsheet ID + +You can use either: +- The spreadsheet ID: `1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms` +- The full URL: `https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit` + +The script automatically extracts the ID from URLs. + +## Output Formats + +### Text (default) +Human-readable format with pipe separators: +``` +Spreadsheet Title: Sales Data +Sheet Name: Q1 +Name | Revenue | Units +Product A | 10000 | 50 +Product B | 15000 | 75 +``` + +### CSV +Standard CSV format, suitable for further processing: +``` +Name,Revenue,Units +Product A,10000,50 +Product B,15000,75 +``` + +### JSON +Structured data format: +```json +{ + "Q1": [ + ["Name", "Revenue", "Units"], + ["Product A", "10000", "50"] + ] +} +``` + +## A1 Notation Examples + +- `Sheet1!A1:B10` - Range A1 to B10 on Sheet1 +- `Sheet1!A:A` - All of column A on Sheet1 +- `Sheet1!1:1` - All of row 1 on Sheet1 +- `A1:C5` - Range on the first sheet + +## Value Input Options + +- **USER_ENTERED** (default): Values are parsed as if typed by a user. Numbers, dates, and formulas are interpreted. +- **RAW** (`--raw` flag): Values are stored exactly as provided. No parsing of formulas or number formatting. + +## Token Management + +Tokens stored securely using the system keyring: +- **macOS**: Keychain +- **Windows**: Windows Credential Locker +- **Linux**: Secret Service API (GNOME Keyring, KDE Wallet, etc.) + +Service name: `google-sheets-skill-oauth` + +Tokens automatically refresh when expired using Google's cloud function. + + +## When to Use +Use this skill when tackling tasks related to its primary domain or functionality as described above. diff --git a/plugins/antigravity-bundle-documents-presentations/skills/google-slides-automation/SKILL.md b/plugins/antigravity-bundle-documents-presentations/skills/google-slides-automation/SKILL.md new file mode 100644 index 00000000..e0ac5d59 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/google-slides-automation/SKILL.md @@ -0,0 +1,140 @@ +--- +name: google-slides-automation +description: "Lightweight Google Slides integration with standalone OAuth authentication. No MCP server required. Full read/write access." +license: Apache-2.0 +risk: unknown +source: community +metadata: + author: sanjay3290 + version: "1.0" +--- + +# Google Slides + +Lightweight Google Slides integration with standalone OAuth authentication. No MCP server required. Full read/write access. + +> **Requires Google Workspace account.** Personal Gmail accounts are not supported. + +## First-Time Setup + +Authenticate with Google (opens browser): +```bash +python scripts/auth.py login +``` + +Check authentication status: +```bash +python scripts/auth.py status +``` + +Logout when needed: +```bash +python scripts/auth.py logout +``` + +## Read Commands + +All operations via `scripts/slides.py`. Auto-authenticates on first use if not logged in. + +```bash +# Get all text content from a presentation +python scripts/slides.py get-text "1abc123xyz789" +python scripts/slides.py get-text "https://docs.google.com/presentation/d/1abc123xyz789/edit" + +# Find presentations by search query +python scripts/slides.py find "quarterly report" +python scripts/slides.py find "project proposal" --limit 5 + +# Get presentation metadata (title, slide count, slide object IDs) +python scripts/slides.py get-metadata "1abc123xyz789" +``` + +## Write Commands + +```bash +# Create a new empty presentation +python scripts/slides.py create "Q4 Sales Report" + +# Add a blank slide to the end +python scripts/slides.py add-slide "1abc123xyz789" + +# Add a slide with a specific layout +python scripts/slides.py add-slide "1abc123xyz789" --layout TITLE_AND_BODY + +# Add a slide at a specific position (0-based index) +python scripts/slides.py add-slide "1abc123xyz789" --layout TITLE --at 0 + +# Find and replace text across all slides +python scripts/slides.py replace-text "1abc123xyz789" "old text" "new text" +python scripts/slides.py replace-text "1abc123xyz789" "Draft" "Final" --match-case + +# Delete a slide by object ID (use get-metadata to find IDs) +python scripts/slides.py delete-slide "1abc123xyz789" "g123abc456" + +# Batch update (advanced - for formatting, inserting shapes, images, etc.) +python scripts/slides.py batch-update "1abc123xyz789" '[{"replaceAllText":{"containsText":{"text":"foo"},"replaceText":"bar"}}]' +``` + +## Slide Layouts + +Available layouts for `add-slide --layout`: +- `BLANK` - Empty slide (default) +- `TITLE` - Title slide +- `TITLE_AND_BODY` - Title with body text +- `TITLE_AND_TWO_COLUMNS` - Title with two text columns +- `TITLE_ONLY` - Title bar only +- `SECTION_HEADER` - Section divider +- `ONE_COLUMN_TEXT` - Single column text +- `MAIN_POINT` - Main point highlight +- `BIG_NUMBER` - Large number display + +## Presentation ID Format + +You can use either: +- Direct presentation ID: `1abc123xyz789` +- Full Google Slides URL: `https://docs.google.com/presentation/d/1abc123xyz789/edit` + +The scripts automatically extract the ID from URLs. + +## Output Format + +### get-text +Returns extracted text from all slides, including: +- Presentation title +- Text from shapes/text boxes on each slide +- Table data with cell contents + +### find +Returns list of matching presentations: +```json +{ + "presentations": [ + {"id": "1abc...", "name": "Q4 Report", "modifiedTime": "2024-01-15T..."} + ], + "nextPageToken": "..." +} +``` + +### get-metadata +Returns presentation details: +```json +{ + "presentationId": "1abc...", + "title": "My Presentation", + "slideCount": 15, + "pageSize": {"width": {...}, "height": {...}}, + "hasMasters": true, + "hasLayouts": true +} +``` + +## Token Management + +Tokens stored securely using the system keyring: +- **macOS**: Keychain +- **Windows**: Windows Credential Locker +- **Linux**: Secret Service API (GNOME Keyring, KDE Wallet, etc.) + +Service name: `google-slides-skill-oauth` + +Automatically refreshes expired tokens using Google's cloud function. diff --git a/plugins/antigravity-bundle-documents-presentations/skills/office-productivity/SKILL.md b/plugins/antigravity-bundle-documents-presentations/skills/office-productivity/SKILL.md new file mode 100644 index 00000000..e18249cd --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/office-productivity/SKILL.md @@ -0,0 +1,218 @@ +--- +name: office-productivity +description: "Office productivity workflow covering document creation, spreadsheet automation, presentation generation, and integration with LibreOffice and Microsoft Office formats." +category: workflow-bundle +risk: safe +source: personal +date_added: "2026-02-27" +--- + +# Office Productivity Workflow Bundle + +## Overview + +Comprehensive office productivity workflow for document creation, spreadsheet automation, presentation generation, and format conversion using LibreOffice and Microsoft Office tools. + +## When to Use This Workflow + +Use this workflow when: +- Creating office documents programmatically +- Automating document workflows +- Converting between document formats +- Generating reports +- Creating presentations from data +- Processing spreadsheets + +## Workflow Phases + +### Phase 1: Document Creation + +#### Skills to Invoke +- `libreoffice-writer` - LibreOffice Writer +- `docx-official` - Microsoft Word +- `pdf-official` - PDF handling + +#### Actions +1. Design document template +2. Create document structure +3. Add content programmatically +4. Apply formatting +5. Export to required formats + +#### Copy-Paste Prompts +``` +Use @libreoffice-writer to create ODT documents +``` + +``` +Use @docx-official to create Word documents +``` + +### Phase 2: Spreadsheet Automation + +#### Skills to Invoke +- `libreoffice-calc` - LibreOffice Calc +- `xlsx-official` - Excel spreadsheets +- `googlesheets-automation` - Google Sheets + +#### Actions +1. Design spreadsheet structure +2. Create formulas +3. Import data +4. Generate charts +5. Export reports + +#### Copy-Paste Prompts +``` +Use @libreoffice-calc to create ODS spreadsheets +``` + +``` +Use @xlsx-official to create Excel reports +``` + +### Phase 3: Presentation Generation + +#### Skills to Invoke +- `libreoffice-impress` - LibreOffice Impress +- `pptx-official` - PowerPoint +- `frontend-slides` - HTML slides +- `nanobanana-ppt-skills` - AI PPT generation + +#### Actions +1. Design slide template +2. Generate slides from data +3. Add charts and graphics +4. Apply animations +5. Export presentations + +#### Copy-Paste Prompts +``` +Use @libreoffice-impress to create ODP presentations +``` + +``` +Use @pptx-official to create PowerPoint presentations +``` + +``` +Use @frontend-slides to create HTML presentations +``` + +### Phase 4: Format Conversion + +#### Skills to Invoke +- `libreoffice-writer` - Document conversion +- `libreoffice-calc` - Spreadsheet conversion +- `pdf-official` - PDF conversion + +#### Actions +1. Identify source format +2. Choose target format +3. Perform conversion +4. Verify quality +5. Batch process files + +#### Copy-Paste Prompts +``` +Use @libreoffice-writer to convert documents +``` + +### Phase 5: Document Automation + +#### Skills to Invoke +- `libreoffice-writer` - Mail merge +- `workflow-automation` - Workflow automation +- `file-organizer` - File organization + +#### Actions +1. Design automation workflow +2. Create templates +3. Set up data sources +4. Generate documents +5. Distribute outputs + +#### Copy-Paste Prompts +``` +Use @libreoffice-writer to perform mail merge +``` + +``` +Use @workflow-automation to automate document workflows +``` + +### Phase 6: Graphics and Diagrams + +#### Skills to Invoke +- `libreoffice-draw` - Vector graphics +- `canvas-design` - Canvas design +- `mermaid-expert` - Diagram generation + +#### Actions +1. Design graphics +2. Create diagrams +3. Generate charts +4. Export images +5. Integrate with documents + +#### Copy-Paste Prompts +``` +Use @libreoffice-draw to create vector graphics +``` + +``` +Use @mermaid-expert to create diagrams +``` + +### Phase 7: Database Integration + +#### Skills to Invoke +- `libreoffice-base` - LibreOffice Base +- `database-architect` - Database design + +#### Actions +1. Connect to data sources +2. Create forms +3. Design reports +4. Automate queries +5. Generate output + +#### Copy-Paste Prompts +``` +Use @libreoffice-base to create database reports +``` + +## Office Application Workflows + +### LibreOffice +``` +Skills: libreoffice-writer, libreoffice-calc, libreoffice-impress, libreoffice-draw, libreoffice-base +Formats: ODT, ODS, ODP, ODG, ODB +``` + +### Microsoft Office +``` +Skills: docx-official, xlsx-official, pptx-official +Formats: DOCX, XLSX, PPTX +``` + +### Google Workspace +``` +Skills: googlesheets-automation, google-drive-automation, gmail-automation +Formats: Google Docs, Sheets, Slides +``` + +## Quality Gates + +- [ ] Documents formatted correctly +- [ ] Formulas working +- [ ] Presentations complete +- [ ] Conversions successful +- [ ] Automation tested +- [ ] Files organized + +## Related Workflow Bundles + +- `development` - Application development +- `documentation` - Documentation generation +- `database` - Data integration diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/LICENSE.txt b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/LICENSE.txt new file mode 100644 index 00000000..c55ab422 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/LICENSE.txt @@ -0,0 +1,30 @@ +ยฉ 2025 Anthropic, PBC. All rights reserved. + +LICENSE: Use of these materials (including all code, prompts, assets, files, +and other components of this Skill) is governed by your agreement with +Anthropic regarding use of Anthropic's services. If no separate agreement +exists, use is governed by Anthropic's Consumer Terms of Service or +Commercial Terms of Service, as applicable: +https://www.anthropic.com/legal/consumer-terms +https://www.anthropic.com/legal/commercial-terms +Your applicable agreement is referred to as the "Agreement." "Services" are +as defined in the Agreement. + +ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the +contrary, users may not: + +- Extract these materials from the Services or retain copies of these + materials outside the Services +- Reproduce or copy these materials, except for temporary copies created + automatically during authorized use of the Services +- Create derivative works based on these materials +- Distribute, sublicense, or transfer these materials to any third party +- Make, offer to sell, sell, or import any inventions embodied in these + materials +- Reverse engineer, decompile, or disassemble these materials + +The receipt, viewing, or possession of these materials does not convey or +imply any license or right beyond those expressly granted above. + +Anthropic retains all right, title, and interest in these materials, +including all copyrights, patents, and other intellectual property rights. diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/SKILL.md b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/SKILL.md new file mode 100644 index 00000000..2d2a89f8 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/SKILL.md @@ -0,0 +1,299 @@ +--- +name: pdf-official +description: "This guide covers essential PDF processing operations using Python libraries and command-line tools. For advanced features, JavaScript libraries, and detailed examples, see reference.md. If you need to fill out a PDF form, read forms.md and follow its instructions." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# PDF Processing Guide + +## Overview + +This guide covers essential PDF processing operations using Python libraries and command-line tools. For advanced features, JavaScript libraries, and detailed examples, see reference.md. If you need to fill out a PDF form, read forms.md and follow its instructions. + +## Quick Start + +```python +from pypdf import PdfReader, PdfWriter + +# Read a PDF +reader = PdfReader("document.pdf") +print(f"Pages: {len(reader.pages)}") + +# Extract text +text = "" +for page in reader.pages: + text += page.extract_text() +``` + +## Python Libraries + +### pypdf - Basic Operations + +#### Merge PDFs +```python +from pypdf import PdfWriter, PdfReader + +writer = PdfWriter() +for pdf_file in ["doc1.pdf", "doc2.pdf", "doc3.pdf"]: + reader = PdfReader(pdf_file) + for page in reader.pages: + writer.add_page(page) + +with open("merged.pdf", "wb") as output: + writer.write(output) +``` + +#### Split PDF +```python +reader = PdfReader("input.pdf") +for i, page in enumerate(reader.pages): + writer = PdfWriter() + writer.add_page(page) + with open(f"page_{i+1}.pdf", "wb") as output: + writer.write(output) +``` + +#### Extract Metadata +```python +reader = PdfReader("document.pdf") +meta = reader.metadata +print(f"Title: {meta.title}") +print(f"Author: {meta.author}") +print(f"Subject: {meta.subject}") +print(f"Creator: {meta.creator}") +``` + +#### Rotate Pages +```python +reader = PdfReader("input.pdf") +writer = PdfWriter() + +page = reader.pages[0] +page.rotate(90) # Rotate 90 degrees clockwise +writer.add_page(page) + +with open("rotated.pdf", "wb") as output: + writer.write(output) +``` + +### pdfplumber - Text and Table Extraction + +#### Extract Text with Layout +```python +import pdfplumber + +with pdfplumber.open("document.pdf") as pdf: + for page in pdf.pages: + text = page.extract_text() + print(text) +``` + +#### Extract Tables +```python +with pdfplumber.open("document.pdf") as pdf: + for i, page in enumerate(pdf.pages): + tables = page.extract_tables() + for j, table in enumerate(tables): + print(f"Table {j+1} on page {i+1}:") + for row in table: + print(row) +``` + +#### Advanced Table Extraction +```python +import pandas as pd + +with pdfplumber.open("document.pdf") as pdf: + all_tables = [] + for page in pdf.pages: + tables = page.extract_tables() + for table in tables: + if table: # Check if table is not empty + df = pd.DataFrame(table[1:], columns=table[0]) + all_tables.append(df) + +# Combine all tables +if all_tables: + combined_df = pd.concat(all_tables, ignore_index=True) + combined_df.to_excel("extracted_tables.xlsx", index=False) +``` + +### reportlab - Create PDFs + +#### Basic PDF Creation +```python +from reportlab.lib.pagesizes import letter +from reportlab.pdfgen import canvas + +c = canvas.Canvas("hello.pdf", pagesize=letter) +width, height = letter + +# Add text +c.drawString(100, height - 100, "Hello World!") +c.drawString(100, height - 120, "This is a PDF created with reportlab") + +# Add a line +c.line(100, height - 140, 400, height - 140) + +# Save +c.save() +``` + +#### Create PDF with Multiple Pages +```python +from reportlab.lib.pagesizes import letter +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak +from reportlab.lib.styles import getSampleStyleSheet + +doc = SimpleDocTemplate("report.pdf", pagesize=letter) +styles = getSampleStyleSheet() +story = [] + +# Add content +title = Paragraph("Report Title", styles['Title']) +story.append(title) +story.append(Spacer(1, 12)) + +body = Paragraph("This is the body of the report. " * 20, styles['Normal']) +story.append(body) +story.append(PageBreak()) + +# Page 2 +story.append(Paragraph("Page 2", styles['Heading1'])) +story.append(Paragraph("Content for page 2", styles['Normal'])) + +# Build PDF +doc.build(story) +``` + +## Command-Line Tools + +### pdftotext (poppler-utils) +```bash +# Extract text +pdftotext input.pdf output.txt + +# Extract text preserving layout +pdftotext -layout input.pdf output.txt + +# Extract specific pages +pdftotext -f 1 -l 5 input.pdf output.txt # Pages 1-5 +``` + +### qpdf +```bash +# Merge PDFs +qpdf --empty --pages file1.pdf file2.pdf -- merged.pdf + +# Split pages +qpdf input.pdf --pages . 1-5 -- pages1-5.pdf +qpdf input.pdf --pages . 6-10 -- pages6-10.pdf + +# Rotate pages +qpdf input.pdf output.pdf --rotate=+90:1 # Rotate page 1 by 90 degrees + +# Remove password +qpdf --password=mypassword --decrypt encrypted.pdf decrypted.pdf +``` + +### pdftk (if available) +```bash +# Merge +pdftk file1.pdf file2.pdf cat output merged.pdf + +# Split +pdftk input.pdf burst + +# Rotate +pdftk input.pdf rotate 1east output rotated.pdf +``` + +## Common Tasks + +### Extract Text from Scanned PDFs +```python +# Requires: pip install pytesseract pdf2image +import pytesseract +from pdf2image import convert_from_path + +# Convert PDF to images +images = convert_from_path('scanned.pdf') + +# OCR each page +text = "" +for i, image in enumerate(images): + text += f"Page {i+1}:\n" + text += pytesseract.image_to_string(image) + text += "\n\n" + +print(text) +``` + +### Add Watermark +```python +from pypdf import PdfReader, PdfWriter + +# Create watermark (or load existing) +watermark = PdfReader("watermark.pdf").pages[0] + +# Apply to all pages +reader = PdfReader("document.pdf") +writer = PdfWriter() + +for page in reader.pages: + page.merge_page(watermark) + writer.add_page(page) + +with open("watermarked.pdf", "wb") as output: + writer.write(output) +``` + +### Extract Images +```bash +# Using pdfimages (poppler-utils) +pdfimages -j input.pdf output_prefix + +# This extracts all images as output_prefix-000.jpg, output_prefix-001.jpg, etc. +``` + +### Password Protection +```python +from pypdf import PdfReader, PdfWriter + +reader = PdfReader("input.pdf") +writer = PdfWriter() + +for page in reader.pages: + writer.add_page(page) + +# Add password +writer.encrypt("userpassword", "ownerpassword") + +with open("encrypted.pdf", "wb") as output: + writer.write(output) +``` + +## Quick Reference + +| Task | Best Tool | Command/Code | +|------|-----------|--------------| +| Merge PDFs | pypdf | `writer.add_page(page)` | +| Split PDFs | pypdf | One page per file | +| Extract text | pdfplumber | `page.extract_text()` | +| Extract tables | pdfplumber | `page.extract_tables()` | +| Create PDFs | reportlab | Canvas or Platypus | +| Command line merge | qpdf | `qpdf --empty --pages ...` | +| OCR scanned PDFs | pytesseract | Convert to image first | +| Fill PDF forms | pdf-lib or pypdf (see forms.md) | See forms.md | + +## Next Steps + +- For advanced pypdfium2 usage, see reference.md +- For JavaScript libraries (pdf-lib), see reference.md +- If you need to fill out a PDF form, follow the instructions in forms.md +- For troubleshooting guides, see reference.md + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/forms.md b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/forms.md new file mode 100644 index 00000000..4e234506 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/forms.md @@ -0,0 +1,205 @@ +**CRITICAL: You MUST complete these steps in order. Do not skip ahead to writing code.** + +If you need to fill out a PDF form, first check to see if the PDF has fillable form fields. Run this script from this file's directory: + `python scripts/check_fillable_fields `, and depending on the result go to either the "Fillable fields" or "Non-fillable fields" and follow those instructions. + +# Fillable fields +If the PDF has fillable form fields: +- Run this script from this file's directory: `python scripts/extract_form_field_info.py `. It will create a JSON file with a list of fields in this format: +``` +[ + { + "field_id": (unique ID for the field), + "page": (page number, 1-based), + "rect": ([left, bottom, right, top] bounding box in PDF coordinates, y=0 is the bottom of the page), + "type": ("text", "checkbox", "radio_group", or "choice"), + }, + // Checkboxes have "checked_value" and "unchecked_value" properties: + { + "field_id": (unique ID for the field), + "page": (page number, 1-based), + "type": "checkbox", + "checked_value": (Set the field to this value to check the checkbox), + "unchecked_value": (Set the field to this value to uncheck the checkbox), + }, + // Radio groups have a "radio_options" list with the possible choices. + { + "field_id": (unique ID for the field), + "page": (page number, 1-based), + "type": "radio_group", + "radio_options": [ + { + "value": (set the field to this value to select this radio option), + "rect": (bounding box for the radio button for this option) + }, + // Other radio options + ] + }, + // Multiple choice fields have a "choice_options" list with the possible choices: + { + "field_id": (unique ID for the field), + "page": (page number, 1-based), + "type": "choice", + "choice_options": [ + { + "value": (set the field to this value to select this option), + "text": (display text of the option) + }, + // Other choice options + ], + } +] +``` +- Convert the PDF to PNGs (one image for each page) with this script (run from this file's directory): +`python scripts/convert_pdf_to_images.py ` +Then analyze the images to determine the purpose of each form field (make sure to convert the bounding box PDF coordinates to image coordinates). +- Create a `field_values.json` file in this format with the values to be entered for each field: +``` +[ + { + "field_id": "last_name", // Must match the field_id from `extract_form_field_info.py` + "description": "The user's last name", + "page": 1, // Must match the "page" value in field_info.json + "value": "Simpson" + }, + { + "field_id": "Checkbox12", + "description": "Checkbox to be checked if the user is 18 or over", + "page": 1, + "value": "/On" // If this is a checkbox, use its "checked_value" value to check it. If it's a radio button group, use one of the "value" values in "radio_options". + }, + // more fields +] +``` +- Run the `fill_fillable_fields.py` script from this file's directory to create a filled-in PDF: +`python scripts/fill_fillable_fields.py ` +This script will verify that the field IDs and values you provide are valid; if it prints error messages, correct the appropriate fields and try again. + +# Non-fillable fields +If the PDF doesn't have fillable form fields, you'll need to visually determine where the data should be added and create text annotations. Follow the below steps *exactly*. You MUST perform all of these steps to ensure that the the form is accurately completed. Details for each step are below. +- Convert the PDF to PNG images and determine field bounding boxes. +- Create a JSON file with field information and validation images showing the bounding boxes. +- Validate the the bounding boxes. +- Use the bounding boxes to fill in the form. + +## Step 1: Visual Analysis (REQUIRED) +- Convert the PDF to PNG images. Run this script from this file's directory: +`python scripts/convert_pdf_to_images.py ` +The script will create a PNG image for each page in the PDF. +- Carefully examine each PNG image and identify all form fields and areas where the user should enter data. For each form field where the user should enter text, determine bounding boxes for both the form field label, and the area where the user should enter text. The label and entry bounding boxes MUST NOT INTERSECT; the text entry box should only include the area where data should be entered. Usually this area will be immediately to the side, above, or below its label. Entry bounding boxes must be tall and wide enough to contain their text. + +These are some examples of form structures that you might see: + +*Label inside box* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Name: โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` +The input area should be to the right of the "Name" label and extend to the edge of the box. + +*Label before line* +``` +Email: _______________________ +``` +The input area should be above the line and include its entire width. + +*Label under line* +``` +_________________________ +Name +``` +The input area should be above the line and include the entire width of the line. This is common for signature and date fields. + +*Label above line* +``` +Please enter any special requests: +________________________________________________ +``` +The input area should extend from the bottom of the label to the line, and should include the entire width of the line. + +*Checkboxes* +``` +Are you a US citizen? Yes โ–ก No โ–ก +``` +For checkboxes: +- Look for small square boxes (โ–ก) - these are the actual checkboxes to target. They may be to the left or right of their labels. +- Distinguish between label text ("Yes", "No") and the clickable checkbox squares. +- The entry bounding box should cover ONLY the small square, not the text label. + +### Step 2: Create fields.json and validation images (REQUIRED) +- Create a file named `fields.json` with information for the form fields and bounding boxes in this format: +``` +{ + "pages": [ + { + "page_number": 1, + "image_width": (first page image width in pixels), + "image_height": (first page image height in pixels), + }, + { + "page_number": 2, + "image_width": (second page image width in pixels), + "image_height": (second page image height in pixels), + } + // additional pages + ], + "form_fields": [ + // Example for a text field. + { + "page_number": 1, + "description": "The user's last name should be entered here", + // Bounding boxes are [left, top, right, bottom]. The bounding boxes for the label and text entry should not overlap. + "field_label": "Last name", + "label_bounding_box": [30, 125, 95, 142], + "entry_bounding_box": [100, 125, 280, 142], + "entry_text": { + "text": "Johnson", // This text will be added as an annotation at the entry_bounding_box location + "font_size": 14, // optional, defaults to 14 + "font_color": "000000", // optional, RRGGBB format, defaults to 000000 (black) + } + }, + // Example for a checkbox. TARGET THE SQUARE for the entry bounding box, NOT THE TEXT + { + "page_number": 2, + "description": "Checkbox that should be checked if the user is over 18", + "entry_bounding_box": [140, 525, 155, 540], // Small box over checkbox square + "field_label": "Yes", + "label_bounding_box": [100, 525, 132, 540], // Box containing "Yes" text + // Use "X" to check a checkbox. + "entry_text": { + "text": "X", + } + } + // additional form field entries + ] +} +``` + +Create validation images by running this script from this file's directory for each page: +`python scripts/create_validation_image.py + +The validation images will have red rectangles where text should be entered, and blue rectangles covering label text. + +### Step 3: Validate Bounding Boxes (REQUIRED) +#### Automated intersection check +- Verify that none of bounding boxes intersect and that the entry bounding boxes are tall enough by checking the fields.json file with the `check_bounding_boxes.py` script (run from this file's directory): +`python scripts/check_bounding_boxes.py ` + +If there are errors, reanalyze the relevant fields, adjust the bounding boxes, and iterate until there are no remaining errors. Remember: label (blue) bounding boxes should contain text labels, entry (red) boxes should not. + +#### Manual image inspection +**CRITICAL: Do not proceed without visually inspecting validation images** +- Red rectangles must ONLY cover input areas +- Red rectangles MUST NOT contain any text +- Blue rectangles should contain label text +- For checkboxes: + - Red rectangle MUST be centered on the checkbox square + - Blue rectangle should cover the text label for the checkbox + +- If any rectangles look wrong, fix fields.json, regenerate the validation images, and verify again. Repeat this process until the bounding boxes are fully accurate. + + +### Step 4: Add annotations to the PDF +Run this script from this file's directory to create a filled-out PDF using the information in fields.json: +`python scripts/fill_pdf_form_with_annotations.py diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/reference.md b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/reference.md new file mode 100644 index 00000000..41400bf4 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/reference.md @@ -0,0 +1,612 @@ +# PDF Processing Advanced Reference + +This document contains advanced PDF processing features, detailed examples, and additional libraries not covered in the main skill instructions. + +## pypdfium2 Library (Apache/BSD License) + +### Overview +pypdfium2 is a Python binding for PDFium (Chromium's PDF library). It's excellent for fast PDF rendering, image generation, and serves as a PyMuPDF replacement. + +### Render PDF to Images +```python +import pypdfium2 as pdfium +from PIL import Image + +# Load PDF +pdf = pdfium.PdfDocument("document.pdf") + +# Render page to image +page = pdf[0] # First page +bitmap = page.render( + scale=2.0, # Higher resolution + rotation=0 # No rotation +) + +# Convert to PIL Image +img = bitmap.to_pil() +img.save("page_1.png", "PNG") + +# Process multiple pages +for i, page in enumerate(pdf): + bitmap = page.render(scale=1.5) + img = bitmap.to_pil() + img.save(f"page_{i+1}.jpg", "JPEG", quality=90) +``` + +### Extract Text with pypdfium2 +```python +import pypdfium2 as pdfium + +pdf = pdfium.PdfDocument("document.pdf") +for i, page in enumerate(pdf): + text = page.get_text() + print(f"Page {i+1} text length: {len(text)} chars") +``` + +## JavaScript Libraries + +### pdf-lib (MIT License) + +pdf-lib is a powerful JavaScript library for creating and modifying PDF documents in any JavaScript environment. + +#### Load and Manipulate Existing PDF +```javascript +import { PDFDocument } from 'pdf-lib'; +import fs from 'fs'; + +async function manipulatePDF() { + // Load existing PDF + const existingPdfBytes = fs.readFileSync('input.pdf'); + const pdfDoc = await PDFDocument.load(existingPdfBytes); + + // Get page count + const pageCount = pdfDoc.getPageCount(); + console.log(`Document has ${pageCount} pages`); + + // Add new page + const newPage = pdfDoc.addPage([600, 400]); + newPage.drawText('Added by pdf-lib', { + x: 100, + y: 300, + size: 16 + }); + + // Save modified PDF + const pdfBytes = await pdfDoc.save(); + fs.writeFileSync('modified.pdf', pdfBytes); +} +``` + +#### Create Complex PDFs from Scratch +```javascript +import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'; +import fs from 'fs'; + +async function createPDF() { + const pdfDoc = await PDFDocument.create(); + + // Add fonts + const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica); + const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold); + + // Add page + const page = pdfDoc.addPage([595, 842]); // A4 size + const { width, height } = page.getSize(); + + // Add text with styling + page.drawText('Invoice #12345', { + x: 50, + y: height - 50, + size: 18, + font: helveticaBold, + color: rgb(0.2, 0.2, 0.8) + }); + + // Add rectangle (header background) + page.drawRectangle({ + x: 40, + y: height - 100, + width: width - 80, + height: 30, + color: rgb(0.9, 0.9, 0.9) + }); + + // Add table-like content + const items = [ + ['Item', 'Qty', 'Price', 'Total'], + ['Widget', '2', '$50', '$100'], + ['Gadget', '1', '$75', '$75'] + ]; + + let yPos = height - 150; + items.forEach(row => { + let xPos = 50; + row.forEach(cell => { + page.drawText(cell, { + x: xPos, + y: yPos, + size: 12, + font: helveticaFont + }); + xPos += 120; + }); + yPos -= 25; + }); + + const pdfBytes = await pdfDoc.save(); + fs.writeFileSync('created.pdf', pdfBytes); +} +``` + +#### Advanced Merge and Split Operations +```javascript +import { PDFDocument } from 'pdf-lib'; +import fs from 'fs'; + +async function mergePDFs() { + // Create new document + const mergedPdf = await PDFDocument.create(); + + // Load source PDFs + const pdf1Bytes = fs.readFileSync('doc1.pdf'); + const pdf2Bytes = fs.readFileSync('doc2.pdf'); + + const pdf1 = await PDFDocument.load(pdf1Bytes); + const pdf2 = await PDFDocument.load(pdf2Bytes); + + // Copy pages from first PDF + const pdf1Pages = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices()); + pdf1Pages.forEach(page => mergedPdf.addPage(page)); + + // Copy specific pages from second PDF (pages 0, 2, 4) + const pdf2Pages = await mergedPdf.copyPages(pdf2, [0, 2, 4]); + pdf2Pages.forEach(page => mergedPdf.addPage(page)); + + const mergedPdfBytes = await mergedPdf.save(); + fs.writeFileSync('merged.pdf', mergedPdfBytes); +} +``` + +### pdfjs-dist (Apache License) + +PDF.js is Mozilla's JavaScript library for rendering PDFs in the browser. + +#### Basic PDF Loading and Rendering +```javascript +import * as pdfjsLib from 'pdfjs-dist'; + +// Configure worker (important for performance) +pdfjsLib.GlobalWorkerOptions.workerSrc = './pdf.worker.js'; + +async function renderPDF() { + // Load PDF + const loadingTask = pdfjsLib.getDocument('document.pdf'); + const pdf = await loadingTask.promise; + + console.log(`Loaded PDF with ${pdf.numPages} pages`); + + // Get first page + const page = await pdf.getPage(1); + const viewport = page.getViewport({ scale: 1.5 }); + + // Render to canvas + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + canvas.height = viewport.height; + canvas.width = viewport.width; + + const renderContext = { + canvasContext: context, + viewport: viewport + }; + + await page.render(renderContext).promise; + document.body.appendChild(canvas); +} +``` + +#### Extract Text with Coordinates +```javascript +import * as pdfjsLib from 'pdfjs-dist'; + +async function extractText() { + const loadingTask = pdfjsLib.getDocument('document.pdf'); + const pdf = await loadingTask.promise; + + let fullText = ''; + + // Extract text from all pages + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const textContent = await page.getTextContent(); + + const pageText = textContent.items + .map(item => item.str) + .join(' '); + + fullText += `\n--- Page ${i} ---\n${pageText}`; + + // Get text with coordinates for advanced processing + const textWithCoords = textContent.items.map(item => ({ + text: item.str, + x: item.transform[4], + y: item.transform[5], + width: item.width, + height: item.height + })); + } + + console.log(fullText); + return fullText; +} +``` + +#### Extract Annotations and Forms +```javascript +import * as pdfjsLib from 'pdfjs-dist'; + +async function extractAnnotations() { + const loadingTask = pdfjsLib.getDocument('annotated.pdf'); + const pdf = await loadingTask.promise; + + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const annotations = await page.getAnnotations(); + + annotations.forEach(annotation => { + console.log(`Annotation type: ${annotation.subtype}`); + console.log(`Content: ${annotation.contents}`); + console.log(`Coordinates: ${JSON.stringify(annotation.rect)}`); + }); + } +} +``` + +## Advanced Command-Line Operations + +### poppler-utils Advanced Features + +#### Extract Text with Bounding Box Coordinates +```bash +# Extract text with bounding box coordinates (essential for structured data) +pdftotext -bbox-layout document.pdf output.xml + +# The XML output contains precise coordinates for each text element +``` + +#### Advanced Image Conversion +```bash +# Convert to PNG images with specific resolution +pdftoppm -png -r 300 document.pdf output_prefix + +# Convert specific page range with high resolution +pdftoppm -png -r 600 -f 1 -l 3 document.pdf high_res_pages + +# Convert to JPEG with quality setting +pdftoppm -jpeg -jpegopt quality=85 -r 200 document.pdf jpeg_output +``` + +#### Extract Embedded Images +```bash +# Extract all embedded images with metadata +pdfimages -j -p document.pdf page_images + +# List image info without extracting +pdfimages -list document.pdf + +# Extract images in their original format +pdfimages -all document.pdf images/img +``` + +### qpdf Advanced Features + +#### Complex Page Manipulation +```bash +# Split PDF into groups of pages +qpdf --split-pages=3 input.pdf output_group_%02d.pdf + +# Extract specific pages with complex ranges +qpdf input.pdf --pages input.pdf 1,3-5,8,10-end -- extracted.pdf + +# Merge specific pages from multiple PDFs +qpdf --empty --pages doc1.pdf 1-3 doc2.pdf 5-7 doc3.pdf 2,4 -- combined.pdf +``` + +#### PDF Optimization and Repair +```bash +# Optimize PDF for web (linearize for streaming) +qpdf --linearize input.pdf optimized.pdf + +# Remove unused objects and compress +qpdf --optimize-level=all input.pdf compressed.pdf + +# Attempt to repair corrupted PDF structure +qpdf --check input.pdf +qpdf --fix-qdf damaged.pdf repaired.pdf + +# Show detailed PDF structure for debugging +qpdf --show-all-pages input.pdf > structure.txt +``` + +#### Advanced Encryption +```bash +# Add password protection with specific permissions +qpdf --encrypt user_pass owner_pass 256 --print=none --modify=none -- input.pdf encrypted.pdf + +# Check encryption status +qpdf --show-encryption encrypted.pdf + +# Remove password protection (requires password) +qpdf --password=secret123 --decrypt encrypted.pdf decrypted.pdf +``` + +## Advanced Python Techniques + +### pdfplumber Advanced Features + +#### Extract Text with Precise Coordinates +```python +import pdfplumber + +with pdfplumber.open("document.pdf") as pdf: + page = pdf.pages[0] + + # Extract all text with coordinates + chars = page.chars + for char in chars[:10]: # First 10 characters + print(f"Char: '{char['text']}' at x:{char['x0']:.1f} y:{char['y0']:.1f}") + + # Extract text by bounding box (left, top, right, bottom) + bbox_text = page.within_bbox((100, 100, 400, 200)).extract_text() +``` + +#### Advanced Table Extraction with Custom Settings +```python +import pdfplumber +import pandas as pd + +with pdfplumber.open("complex_table.pdf") as pdf: + page = pdf.pages[0] + + # Extract tables with custom settings for complex layouts + table_settings = { + "vertical_strategy": "lines", + "horizontal_strategy": "lines", + "snap_tolerance": 3, + "intersection_tolerance": 15 + } + tables = page.extract_tables(table_settings) + + # Visual debugging for table extraction + img = page.to_image(resolution=150) + img.save("debug_layout.png") +``` + +### reportlab Advanced Features + +#### Create Professional Reports with Tables +```python +from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib import colors + +# Sample data +data = [ + ['Product', 'Q1', 'Q2', 'Q3', 'Q4'], + ['Widgets', '120', '135', '142', '158'], + ['Gadgets', '85', '92', '98', '105'] +] + +# Create PDF with table +doc = SimpleDocTemplate("report.pdf") +elements = [] + +# Add title +styles = getSampleStyleSheet() +title = Paragraph("Quarterly Sales Report", styles['Title']) +elements.append(title) + +# Add table with advanced styling +table = Table(data) +table.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, 0), colors.grey), + ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), + ('ALIGN', (0, 0), (-1, -1), 'CENTER'), + ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), + ('FONTSIZE', (0, 0), (-1, 0), 14), + ('BOTTOMPADDING', (0, 0), (-1, 0), 12), + ('BACKGROUND', (0, 1), (-1, -1), colors.beige), + ('GRID', (0, 0), (-1, -1), 1, colors.black) +])) +elements.append(table) + +doc.build(elements) +``` + +## Complex Workflows + +### Extract Figures/Images from PDF + +#### Method 1: Using pdfimages (fastest) +```bash +# Extract all images with original quality +pdfimages -all document.pdf images/img +``` + +#### Method 2: Using pypdfium2 + Image Processing +```python +import pypdfium2 as pdfium +from PIL import Image +import numpy as np + +def extract_figures(pdf_path, output_dir): + pdf = pdfium.PdfDocument(pdf_path) + + for page_num, page in enumerate(pdf): + # Render high-resolution page + bitmap = page.render(scale=3.0) + img = bitmap.to_pil() + + # Convert to numpy for processing + img_array = np.array(img) + + # Simple figure detection (non-white regions) + mask = np.any(img_array != [255, 255, 255], axis=2) + + # Find contours and extract bounding boxes + # (This is simplified - real implementation would need more sophisticated detection) + + # Save detected figures + # ... implementation depends on specific needs +``` + +### Batch PDF Processing with Error Handling +```python +import os +import glob +from pypdf import PdfReader, PdfWriter +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def batch_process_pdfs(input_dir, operation='merge'): + pdf_files = glob.glob(os.path.join(input_dir, "*.pdf")) + + if operation == 'merge': + writer = PdfWriter() + for pdf_file in pdf_files: + try: + reader = PdfReader(pdf_file) + for page in reader.pages: + writer.add_page(page) + logger.info(f"Processed: {pdf_file}") + except Exception as e: + logger.error(f"Failed to process {pdf_file}: {e}") + continue + + with open("batch_merged.pdf", "wb") as output: + writer.write(output) + + elif operation == 'extract_text': + for pdf_file in pdf_files: + try: + reader = PdfReader(pdf_file) + text = "" + for page in reader.pages: + text += page.extract_text() + + output_file = pdf_file.replace('.pdf', '.txt') + with open(output_file, 'w', encoding='utf-8') as f: + f.write(text) + logger.info(f"Extracted text from: {pdf_file}") + + except Exception as e: + logger.error(f"Failed to extract text from {pdf_file}: {e}") + continue +``` + +### Advanced PDF Cropping +```python +from pypdf import PdfWriter, PdfReader + +reader = PdfReader("input.pdf") +writer = PdfWriter() + +# Crop page (left, bottom, right, top in points) +page = reader.pages[0] +page.mediabox.left = 50 +page.mediabox.bottom = 50 +page.mediabox.right = 550 +page.mediabox.top = 750 + +writer.add_page(page) +with open("cropped.pdf", "wb") as output: + writer.write(output) +``` + +## Performance Optimization Tips + +### 1. For Large PDFs +- Use streaming approaches instead of loading entire PDF in memory +- Use `qpdf --split-pages` for splitting large files +- Process pages individually with pypdfium2 + +### 2. For Text Extraction +- `pdftotext -bbox-layout` is fastest for plain text extraction +- Use pdfplumber for structured data and tables +- Avoid `pypdf.extract_text()` for very large documents + +### 3. For Image Extraction +- `pdfimages` is much faster than rendering pages +- Use low resolution for previews, high resolution for final output + +### 4. For Form Filling +- pdf-lib maintains form structure better than most alternatives +- Pre-validate form fields before processing + +### 5. Memory Management +```python +# Process PDFs in chunks +def process_large_pdf(pdf_path, chunk_size=10): + reader = PdfReader(pdf_path) + total_pages = len(reader.pages) + + for start_idx in range(0, total_pages, chunk_size): + end_idx = min(start_idx + chunk_size, total_pages) + writer = PdfWriter() + + for i in range(start_idx, end_idx): + writer.add_page(reader.pages[i]) + + # Process chunk + with open(f"chunk_{start_idx//chunk_size}.pdf", "wb") as output: + writer.write(output) +``` + +## Troubleshooting Common Issues + +### Encrypted PDFs +```python +# Handle password-protected PDFs +from pypdf import PdfReader + +try: + reader = PdfReader("encrypted.pdf") + if reader.is_encrypted: + reader.decrypt("password") +except Exception as e: + print(f"Failed to decrypt: {e}") +``` + +### Corrupted PDFs +```bash +# Use qpdf to repair +qpdf --check corrupted.pdf +qpdf --replace-input corrupted.pdf +``` + +### Text Extraction Issues +```python +# Fallback to OCR for scanned PDFs +import pytesseract +from pdf2image import convert_from_path + +def extract_text_with_ocr(pdf_path): + images = convert_from_path(pdf_path) + text = "" + for i, image in enumerate(images): + text += pytesseract.image_to_string(image) + return text +``` + +## License Information + +- **pypdf**: BSD License +- **pdfplumber**: MIT License +- **pypdfium2**: Apache/BSD License +- **reportlab**: BSD License +- **poppler-utils**: GPL-2 License +- **qpdf**: Apache License +- **pdf-lib**: MIT License +- **pdfjs-dist**: Apache License \ No newline at end of file diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/check_bounding_boxes.py b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/check_bounding_boxes.py new file mode 100644 index 00000000..7443660c --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/check_bounding_boxes.py @@ -0,0 +1,70 @@ +from dataclasses import dataclass +import json +import sys + + +# Script to check that the `fields.json` file that Claude creates when analyzing PDFs +# does not have overlapping bounding boxes. See forms.md. + + +@dataclass +class RectAndField: + rect: list[float] + rect_type: str + field: dict + + +# Returns a list of messages that are printed to stdout for Claude to read. +def get_bounding_box_messages(fields_json_stream) -> list[str]: + messages = [] + fields = json.load(fields_json_stream) + messages.append(f"Read {len(fields['form_fields'])} fields") + + def rects_intersect(r1, r2): + disjoint_horizontal = r1[0] >= r2[2] or r1[2] <= r2[0] + disjoint_vertical = r1[1] >= r2[3] or r1[3] <= r2[1] + return not (disjoint_horizontal or disjoint_vertical) + + rects_and_fields = [] + for f in fields["form_fields"]: + rects_and_fields.append(RectAndField(f["label_bounding_box"], "label", f)) + rects_and_fields.append(RectAndField(f["entry_bounding_box"], "entry", f)) + + has_error = False + for i, ri in enumerate(rects_and_fields): + # This is O(N^2); we can optimize if it becomes a problem. + for j in range(i + 1, len(rects_and_fields)): + rj = rects_and_fields[j] + if ri.field["page_number"] == rj.field["page_number"] and rects_intersect(ri.rect, rj.rect): + has_error = True + if ri.field is rj.field: + messages.append(f"FAILURE: intersection between label and entry bounding boxes for `{ri.field['description']}` ({ri.rect}, {rj.rect})") + else: + messages.append(f"FAILURE: intersection between {ri.rect_type} bounding box for `{ri.field['description']}` ({ri.rect}) and {rj.rect_type} bounding box for `{rj.field['description']}` ({rj.rect})") + if len(messages) >= 20: + messages.append("Aborting further checks; fix bounding boxes and try again") + return messages + if ri.rect_type == "entry": + if "entry_text" in ri.field: + font_size = ri.field["entry_text"].get("font_size", 14) + entry_height = ri.rect[3] - ri.rect[1] + if entry_height < font_size: + has_error = True + messages.append(f"FAILURE: entry bounding box height ({entry_height}) for `{ri.field['description']}` is too short for the text content (font size: {font_size}). Increase the box height or decrease the font size.") + if len(messages) >= 20: + messages.append("Aborting further checks; fix bounding boxes and try again") + return messages + + if not has_error: + messages.append("SUCCESS: All bounding boxes are valid") + return messages + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: check_bounding_boxes.py [fields.json]") + sys.exit(1) + # Input file should be in the `fields.json` format described in forms.md. + with open(sys.argv[1]) as f: + messages = get_bounding_box_messages(f) + for msg in messages: + print(msg) diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/check_bounding_boxes_test.py b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/check_bounding_boxes_test.py new file mode 100644 index 00000000..1dbb463c --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/check_bounding_boxes_test.py @@ -0,0 +1,226 @@ +import unittest +import json +import io +from check_bounding_boxes import get_bounding_box_messages + + +# Currently this is not run automatically in CI; it's just for documentation and manual checking. +class TestGetBoundingBoxMessages(unittest.TestCase): + + def create_json_stream(self, data): + """Helper to create a JSON stream from data""" + return io.StringIO(json.dumps(data)) + + def test_no_intersections(self): + """Test case with no bounding box intersections""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [60, 10, 150, 30] + }, + { + "description": "Email", + "page_number": 1, + "label_bounding_box": [10, 40, 50, 60], + "entry_bounding_box": [60, 40, 150, 60] + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("SUCCESS" in msg for msg in messages)) + self.assertFalse(any("FAILURE" in msg for msg in messages)) + + def test_label_entry_intersection_same_field(self): + """Test intersection between label and entry of the same field""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 60, 30], + "entry_bounding_box": [50, 10, 150, 30] # Overlaps with label + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("FAILURE" in msg and "intersection" in msg for msg in messages)) + self.assertFalse(any("SUCCESS" in msg for msg in messages)) + + def test_intersection_between_different_fields(self): + """Test intersection between bounding boxes of different fields""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [60, 10, 150, 30] + }, + { + "description": "Email", + "page_number": 1, + "label_bounding_box": [40, 20, 80, 40], # Overlaps with Name's boxes + "entry_bounding_box": [160, 10, 250, 30] + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("FAILURE" in msg and "intersection" in msg for msg in messages)) + self.assertFalse(any("SUCCESS" in msg for msg in messages)) + + def test_different_pages_no_intersection(self): + """Test that boxes on different pages don't count as intersecting""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [60, 10, 150, 30] + }, + { + "description": "Email", + "page_number": 2, + "label_bounding_box": [10, 10, 50, 30], # Same coordinates but different page + "entry_bounding_box": [60, 10, 150, 30] + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("SUCCESS" in msg for msg in messages)) + self.assertFalse(any("FAILURE" in msg for msg in messages)) + + def test_entry_height_too_small(self): + """Test that entry box height is checked against font size""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [60, 10, 150, 20], # Height is 10 + "entry_text": { + "font_size": 14 # Font size larger than height + } + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("FAILURE" in msg and "height" in msg for msg in messages)) + self.assertFalse(any("SUCCESS" in msg for msg in messages)) + + def test_entry_height_adequate(self): + """Test that adequate entry box height passes""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [60, 10, 150, 30], # Height is 20 + "entry_text": { + "font_size": 14 # Font size smaller than height + } + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("SUCCESS" in msg for msg in messages)) + self.assertFalse(any("FAILURE" in msg for msg in messages)) + + def test_default_font_size(self): + """Test that default font size is used when not specified""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [60, 10, 150, 20], # Height is 10 + "entry_text": {} # No font_size specified, should use default 14 + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("FAILURE" in msg and "height" in msg for msg in messages)) + self.assertFalse(any("SUCCESS" in msg for msg in messages)) + + def test_no_entry_text(self): + """Test that missing entry_text doesn't cause height check""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [60, 10, 150, 20] # Small height but no entry_text + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("SUCCESS" in msg for msg in messages)) + self.assertFalse(any("FAILURE" in msg for msg in messages)) + + def test_multiple_errors_limit(self): + """Test that error messages are limited to prevent excessive output""" + fields = [] + # Create many overlapping fields + for i in range(25): + fields.append({ + "description": f"Field{i}", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], # All overlap + "entry_bounding_box": [20, 15, 60, 35] # All overlap + }) + + data = {"form_fields": fields} + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + # Should abort after ~20 messages + self.assertTrue(any("Aborting" in msg for msg in messages)) + # Should have some FAILURE messages but not hundreds + failure_count = sum(1 for msg in messages if "FAILURE" in msg) + self.assertGreater(failure_count, 0) + self.assertLess(len(messages), 30) # Should be limited + + def test_edge_touching_boxes(self): + """Test that boxes touching at edges don't count as intersecting""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [50, 10, 150, 30] # Touches at x=50 + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("SUCCESS" in msg for msg in messages)) + self.assertFalse(any("FAILURE" in msg for msg in messages)) + + +if __name__ == '__main__': + unittest.main() diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/check_fillable_fields.py b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/check_fillable_fields.py new file mode 100644 index 00000000..dc43d182 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/check_fillable_fields.py @@ -0,0 +1,12 @@ +import sys +from pypdf import PdfReader + + +# Script for Claude to run to determine whether a PDF has fillable form fields. See forms.md. + + +reader = PdfReader(sys.argv[1]) +if (reader.get_fields()): + print("This PDF has fillable form fields") +else: + print("This PDF does not have fillable form fields; you will need to visually determine where to enter data") diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/convert_pdf_to_images.py b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/convert_pdf_to_images.py new file mode 100644 index 00000000..f8a4ec52 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/convert_pdf_to_images.py @@ -0,0 +1,35 @@ +import os +import sys + +from pdf2image import convert_from_path + + +# Converts each page of a PDF to a PNG image. + + +def convert(pdf_path, output_dir, max_dim=1000): + images = convert_from_path(pdf_path, dpi=200) + + for i, image in enumerate(images): + # Scale image if needed to keep width/height under `max_dim` + width, height = image.size + if width > max_dim or height > max_dim: + scale_factor = min(max_dim / width, max_dim / height) + new_width = int(width * scale_factor) + new_height = int(height * scale_factor) + image = image.resize((new_width, new_height)) + + image_path = os.path.join(output_dir, f"page_{i+1}.png") + image.save(image_path) + print(f"Saved page {i+1} as {image_path} (size: {image.size})") + + print(f"Converted {len(images)} pages to PNG images") + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: convert_pdf_to_images.py [input pdf] [output directory]") + sys.exit(1) + pdf_path = sys.argv[1] + output_directory = sys.argv[2] + convert(pdf_path, output_directory) diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/create_validation_image.py b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/create_validation_image.py new file mode 100644 index 00000000..4913f8f8 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/create_validation_image.py @@ -0,0 +1,41 @@ +import json +import sys + +from PIL import Image, ImageDraw + + +# Creates "validation" images with rectangles for the bounding box information that +# Claude creates when determining where to add text annotations in PDFs. See forms.md. + + +def create_validation_image(page_number, fields_json_path, input_path, output_path): + # Input file should be in the `fields.json` format described in forms.md. + with open(fields_json_path, 'r') as f: + data = json.load(f) + + img = Image.open(input_path) + draw = ImageDraw.Draw(img) + num_boxes = 0 + + for field in data["form_fields"]: + if field["page_number"] == page_number: + entry_box = field['entry_bounding_box'] + label_box = field['label_bounding_box'] + # Draw red rectangle over entry bounding box and blue rectangle over the label. + draw.rectangle(entry_box, outline='red', width=2) + draw.rectangle(label_box, outline='blue', width=2) + num_boxes += 2 + + img.save(output_path) + print(f"Created validation image at {output_path} with {num_boxes} bounding boxes") + + +if __name__ == "__main__": + if len(sys.argv) != 5: + print("Usage: create_validation_image.py [page number] [fields.json file] [input image path] [output image path]") + sys.exit(1) + page_number = int(sys.argv[1]) + fields_json_path = sys.argv[2] + input_image_path = sys.argv[3] + output_image_path = sys.argv[4] + create_validation_image(page_number, fields_json_path, input_image_path, output_image_path) diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/extract_form_field_info.py b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/extract_form_field_info.py new file mode 100644 index 00000000..f42a2df8 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/extract_form_field_info.py @@ -0,0 +1,152 @@ +import json +import sys + +from pypdf import PdfReader + + +# Extracts data for the fillable form fields in a PDF and outputs JSON that +# Claude uses to fill the fields. See forms.md. + + +# This matches the format used by PdfReader `get_fields` and `update_page_form_field_values` methods. +def get_full_annotation_field_id(annotation): + components = [] + while annotation: + field_name = annotation.get('/T') + if field_name: + components.append(field_name) + annotation = annotation.get('/Parent') + return ".".join(reversed(components)) if components else None + + +def make_field_dict(field, field_id): + field_dict = {"field_id": field_id} + ft = field.get('/FT') + if ft == "/Tx": + field_dict["type"] = "text" + elif ft == "/Btn": + field_dict["type"] = "checkbox" # radio groups handled separately + states = field.get("/_States_", []) + if len(states) == 2: + # "/Off" seems to always be the unchecked value, as suggested by + # https://opensource.adobe.com/dc-acrobat-sdk-docs/standards/pdfstandards/pdf/PDF32000_2008.pdf#page=448 + # It can be either first or second in the "/_States_" list. + if "/Off" in states: + field_dict["checked_value"] = states[0] if states[0] != "/Off" else states[1] + field_dict["unchecked_value"] = "/Off" + else: + print(f"Unexpected state values for checkbox `${field_id}`. Its checked and unchecked values may not be correct; if you're trying to check it, visually verify the results.") + field_dict["checked_value"] = states[0] + field_dict["unchecked_value"] = states[1] + elif ft == "/Ch": + field_dict["type"] = "choice" + states = field.get("/_States_", []) + field_dict["choice_options"] = [{ + "value": state[0], + "text": state[1], + } for state in states] + else: + field_dict["type"] = f"unknown ({ft})" + return field_dict + + +# Returns a list of fillable PDF fields: +# [ +# { +# "field_id": "name", +# "page": 1, +# "type": ("text", "checkbox", "radio_group", or "choice") +# // Per-type additional fields described in forms.md +# }, +# ] +def get_field_info(reader: PdfReader): + fields = reader.get_fields() + + field_info_by_id = {} + possible_radio_names = set() + + for field_id, field in fields.items(): + # Skip if this is a container field with children, except that it might be + # a parent group for radio button options. + if field.get("/Kids"): + if field.get("/FT") == "/Btn": + possible_radio_names.add(field_id) + continue + field_info_by_id[field_id] = make_field_dict(field, field_id) + + # Bounding rects are stored in annotations in page objects. + + # Radio button options have a separate annotation for each choice; + # all choices have the same field name. + # See https://westhealth.github.io/exploring-fillable-forms-with-pdfrw.html + radio_fields_by_id = {} + + for page_index, page in enumerate(reader.pages): + annotations = page.get('/Annots', []) + for ann in annotations: + field_id = get_full_annotation_field_id(ann) + if field_id in field_info_by_id: + field_info_by_id[field_id]["page"] = page_index + 1 + field_info_by_id[field_id]["rect"] = ann.get('/Rect') + elif field_id in possible_radio_names: + try: + # ann['/AP']['/N'] should have two items. One of them is '/Off', + # the other is the active value. + on_values = [v for v in ann["/AP"]["/N"] if v != "/Off"] + except KeyError: + continue + if len(on_values) == 1: + rect = ann.get("/Rect") + if field_id not in radio_fields_by_id: + radio_fields_by_id[field_id] = { + "field_id": field_id, + "type": "radio_group", + "page": page_index + 1, + "radio_options": [], + } + # Note: at least on macOS 15.7, Preview.app doesn't show selected + # radio buttons correctly. (It does if you remove the leading slash + # from the value, but that causes them not to appear correctly in + # Chrome/Firefox/Acrobat/etc). + radio_fields_by_id[field_id]["radio_options"].append({ + "value": on_values[0], + "rect": rect, + }) + + # Some PDFs have form field definitions without corresponding annotations, + # so we can't tell where they are. Ignore these fields for now. + fields_with_location = [] + for field_info in field_info_by_id.values(): + if "page" in field_info: + fields_with_location.append(field_info) + else: + print(f"Unable to determine location for field id: {field_info.get('field_id')}, ignoring") + + # Sort by page number, then Y position (flipped in PDF coordinate system), then X. + def sort_key(f): + if "radio_options" in f: + rect = f["radio_options"][0]["rect"] or [0, 0, 0, 0] + else: + rect = f.get("rect") or [0, 0, 0, 0] + adjusted_position = [-rect[1], rect[0]] + return [f.get("page"), adjusted_position] + + sorted_fields = fields_with_location + list(radio_fields_by_id.values()) + sorted_fields.sort(key=sort_key) + + return sorted_fields + + +def write_field_info(pdf_path: str, json_output_path: str): + reader = PdfReader(pdf_path) + field_info = get_field_info(reader) + with open(json_output_path, "w") as f: + json.dump(field_info, f, indent=2) + print(f"Wrote {len(field_info)} fields to {json_output_path}") + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: extract_form_field_info.py [input pdf] [output json]") + sys.exit(1) + write_field_info(sys.argv[1], sys.argv[2]) diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/fill_fillable_fields.py b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/fill_fillable_fields.py new file mode 100644 index 00000000..ac35753c --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/fill_fillable_fields.py @@ -0,0 +1,114 @@ +import json +import sys + +from pypdf import PdfReader, PdfWriter + +from extract_form_field_info import get_field_info + + +# Fills fillable form fields in a PDF. See forms.md. + + +def fill_pdf_fields(input_pdf_path: str, fields_json_path: str, output_pdf_path: str): + with open(fields_json_path) as f: + fields = json.load(f) + # Group by page number. + fields_by_page = {} + for field in fields: + if "value" in field: + field_id = field["field_id"] + page = field["page"] + if page not in fields_by_page: + fields_by_page[page] = {} + fields_by_page[page][field_id] = field["value"] + + reader = PdfReader(input_pdf_path) + + has_error = False + field_info = get_field_info(reader) + fields_by_ids = {f["field_id"]: f for f in field_info} + for field in fields: + existing_field = fields_by_ids.get(field["field_id"]) + if not existing_field: + has_error = True + print(f"ERROR: `{field['field_id']}` is not a valid field ID") + elif field["page"] != existing_field["page"]: + has_error = True + print(f"ERROR: Incorrect page number for `{field['field_id']}` (got {field['page']}, expected {existing_field['page']})") + else: + if "value" in field: + err = validation_error_for_field_value(existing_field, field["value"]) + if err: + print(err) + has_error = True + if has_error: + sys.exit(1) + + writer = PdfWriter(clone_from=reader) + for page, field_values in fields_by_page.items(): + writer.update_page_form_field_values(writer.pages[page - 1], field_values, auto_regenerate=False) + + # This seems to be necessary for many PDF viewers to format the form values correctly. + # It may cause the viewer to show a "save changes" dialog even if the user doesn't make any changes. + writer.set_need_appearances_writer(True) + + with open(output_pdf_path, "wb") as f: + writer.write(f) + + +def validation_error_for_field_value(field_info, field_value): + field_type = field_info["type"] + field_id = field_info["field_id"] + if field_type == "checkbox": + checked_val = field_info["checked_value"] + unchecked_val = field_info["unchecked_value"] + if field_value != checked_val and field_value != unchecked_val: + return f'ERROR: Invalid value "{field_value}" for checkbox field "{field_id}". The checked value is "{checked_val}" and the unchecked value is "{unchecked_val}"' + elif field_type == "radio_group": + option_values = [opt["value"] for opt in field_info["radio_options"]] + if field_value not in option_values: + return f'ERROR: Invalid value "{field_value}" for radio group field "{field_id}". Valid values are: {option_values}' + elif field_type == "choice": + choice_values = [opt["value"] for opt in field_info["choice_options"]] + if field_value not in choice_values: + return f'ERROR: Invalid value "{field_value}" for choice field "{field_id}". Valid values are: {choice_values}' + return None + + +# pypdf (at least version 5.7.0) has a bug when setting the value for a selection list field. +# In _writer.py around line 966: +# +# if field.get(FA.FT, "/Tx") == "/Ch" and field_flags & FA.FfBits.Combo == 0: +# txt = "\n".join(annotation.get_inherited(FA.Opt, [])) +# +# The problem is that for selection lists, `get_inherited` returns a list of two-element lists like +# [["value1", "Text 1"], ["value2", "Text 2"], ...] +# This causes `join` to throw a TypeError because it expects an iterable of strings. +# The horrible workaround is to patch `get_inherited` to return a list of the value strings. +# We call the original method and adjust the return value only if the argument to `get_inherited` +# is `FA.Opt` and if the return value is a list of two-element lists. +def monkeypatch_pydpf_method(): + from pypdf.generic import DictionaryObject + from pypdf.constants import FieldDictionaryAttributes + + original_get_inherited = DictionaryObject.get_inherited + + def patched_get_inherited(self, key: str, default = None): + result = original_get_inherited(self, key, default) + if key == FieldDictionaryAttributes.Opt: + if isinstance(result, list) and all(isinstance(v, list) and len(v) == 2 for v in result): + result = [r[0] for r in result] + return result + + DictionaryObject.get_inherited = patched_get_inherited + + +if __name__ == "__main__": + if len(sys.argv) != 4: + print("Usage: fill_fillable_fields.py [input pdf] [field_values.json] [output pdf]") + sys.exit(1) + monkeypatch_pydpf_method() + input_pdf = sys.argv[1] + fields_json = sys.argv[2] + output_pdf = sys.argv[3] + fill_pdf_fields(input_pdf, fields_json, output_pdf) diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/fill_pdf_form_with_annotations.py b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/fill_pdf_form_with_annotations.py new file mode 100644 index 00000000..f9805313 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pdf-official/scripts/fill_pdf_form_with_annotations.py @@ -0,0 +1,108 @@ +import json +import sys + +from pypdf import PdfReader, PdfWriter +from pypdf.annotations import FreeText + + +# Fills a PDF by adding text annotations defined in `fields.json`. See forms.md. + + +def transform_coordinates(bbox, image_width, image_height, pdf_width, pdf_height): + """Transform bounding box from image coordinates to PDF coordinates""" + # Image coordinates: origin at top-left, y increases downward + # PDF coordinates: origin at bottom-left, y increases upward + x_scale = pdf_width / image_width + y_scale = pdf_height / image_height + + left = bbox[0] * x_scale + right = bbox[2] * x_scale + + # Flip Y coordinates for PDF + top = pdf_height - (bbox[1] * y_scale) + bottom = pdf_height - (bbox[3] * y_scale) + + return left, bottom, right, top + + +def fill_pdf_form(input_pdf_path, fields_json_path, output_pdf_path): + """Fill the PDF form with data from fields.json""" + + # `fields.json` format described in forms.md. + with open(fields_json_path, "r") as f: + fields_data = json.load(f) + + # Open the PDF + reader = PdfReader(input_pdf_path) + writer = PdfWriter() + + # Copy all pages to writer + writer.append(reader) + + # Get PDF dimensions for each page + pdf_dimensions = {} + for i, page in enumerate(reader.pages): + mediabox = page.mediabox + pdf_dimensions[i + 1] = [mediabox.width, mediabox.height] + + # Process each form field + annotations = [] + for field in fields_data["form_fields"]: + page_num = field["page_number"] + + # Get page dimensions and transform coordinates. + page_info = next(p for p in fields_data["pages"] if p["page_number"] == page_num) + image_width = page_info["image_width"] + image_height = page_info["image_height"] + pdf_width, pdf_height = pdf_dimensions[page_num] + + transformed_entry_box = transform_coordinates( + field["entry_bounding_box"], + image_width, image_height, + pdf_width, pdf_height + ) + + # Skip empty fields + if "entry_text" not in field or "text" not in field["entry_text"]: + continue + entry_text = field["entry_text"] + text = entry_text["text"] + if not text: + continue + + font_name = entry_text.get("font", "Arial") + font_size = str(entry_text.get("font_size", 14)) + "pt" + font_color = entry_text.get("font_color", "000000") + + # Font size/color seems to not work reliably across viewers: + # https://github.com/py-pdf/pypdf/issues/2084 + annotation = FreeText( + text=text, + rect=transformed_entry_box, + font=font_name, + font_size=font_size, + font_color=font_color, + border_color=None, + background_color=None, + ) + annotations.append(annotation) + # page_number is 0-based for pypdf + writer.add_annotation(page_number=page_num - 1, annotation=annotation) + + # Save the filled PDF + with open(output_pdf_path, "wb") as output: + writer.write(output) + + print(f"Successfully filled PDF form and saved to {output_pdf_path}") + print(f"Added {len(annotations)} text annotations") + + +if __name__ == "__main__": + if len(sys.argv) != 4: + print("Usage: fill_pdf_form_with_annotations.py [input pdf] [fields.json] [output pdf]") + sys.exit(1) + input_pdf = sys.argv[1] + fields_json = sys.argv[2] + output_pdf = sys.argv[3] + + fill_pdf_form(input_pdf, fields_json, output_pdf) \ No newline at end of file diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/LICENSE.txt b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/LICENSE.txt new file mode 100644 index 00000000..c55ab422 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/LICENSE.txt @@ -0,0 +1,30 @@ +ยฉ 2025 Anthropic, PBC. All rights reserved. + +LICENSE: Use of these materials (including all code, prompts, assets, files, +and other components of this Skill) is governed by your agreement with +Anthropic regarding use of Anthropic's services. If no separate agreement +exists, use is governed by Anthropic's Consumer Terms of Service or +Commercial Terms of Service, as applicable: +https://www.anthropic.com/legal/consumer-terms +https://www.anthropic.com/legal/commercial-terms +Your applicable agreement is referred to as the "Agreement." "Services" are +as defined in the Agreement. + +ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the +contrary, users may not: + +- Extract these materials from the Services or retain copies of these + materials outside the Services +- Reproduce or copy these materials, except for temporary copies created + automatically during authorized use of the Services +- Create derivative works based on these materials +- Distribute, sublicense, or transfer these materials to any third party +- Make, offer to sell, sell, or import any inventions embodied in these + materials +- Reverse engineer, decompile, or disassemble these materials + +The receipt, viewing, or possession of these materials does not convey or +imply any license or right beyond those expressly granted above. + +Anthropic retains all right, title, and interest in these materials, +including all copyrights, patents, and other intellectual property rights. diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/SKILL.md b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/SKILL.md new file mode 100644 index 00000000..ac8756a1 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/SKILL.md @@ -0,0 +1,489 @@ +--- +name: pptx-official +description: "A user may ask you to create, edit, or analyze the contents of a .pptx file. A .pptx file is essentially a ZIP archive containing XML files and other resources that you can read or edit. You have different tools and workflows available for different tasks." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# PPTX creation, editing, and analysis + +## Overview + +A user may ask you to create, edit, or analyze the contents of a .pptx file. A .pptx file is essentially a ZIP archive containing XML files and other resources that you can read or edit. You have different tools and workflows available for different tasks. + +## Reading and analyzing content + +### Text extraction +If you just need to read the text contents of a presentation, you should convert the document to markdown: + +```bash +# Convert document to markdown +python -m markitdown path-to-file.pptx +``` + +### Raw XML access +You need raw XML access for: comments, speaker notes, slide layouts, animations, design elements, and complex formatting. For any of these features, you'll need to unpack a presentation and read its raw XML contents. + +#### Unpacking a file +`python ooxml/scripts/unpack.py ` + +**Note**: The unpack.py script is located at `skills/pptx/ooxml/scripts/unpack.py` relative to the project root. If the script doesn't exist at this path, use `find . -name "unpack.py"` to locate it. + +#### Key file structures +* `ppt/presentation.xml` - Main presentation metadata and slide references +* `ppt/slides/slide{N}.xml` - Individual slide contents (slide1.xml, slide2.xml, etc.) +* `ppt/notesSlides/notesSlide{N}.xml` - Speaker notes for each slide +* `ppt/comments/modernComment_*.xml` - Comments for specific slides +* `ppt/slideLayouts/` - Layout templates for slides +* `ppt/slideMasters/` - Master slide templates +* `ppt/theme/` - Theme and styling information +* `ppt/media/` - Images and other media files + +#### Typography and color extraction +**When given an example design to emulate**: Always analyze the presentation's typography and colors first using the methods below: +1. **Read theme file**: Check `ppt/theme/theme1.xml` for colors (``) and fonts (``) +2. **Sample slide content**: Examine `ppt/slides/slide1.xml` for actual font usage (``) and colors +3. **Search for patterns**: Use grep to find color (``, ``) and font references across all XML files + +## Creating a new PowerPoint presentation **without a template** + +When creating a new PowerPoint presentation from scratch, use the **html2pptx** workflow to convert HTML slides to PowerPoint with accurate positioning. + +### Design Principles + +**CRITICAL**: Before creating any presentation, analyze the content and choose appropriate design elements: +1. **Consider the subject matter**: What is this presentation about? What tone, industry, or mood does it suggest? +2. **Check for branding**: If the user mentions a company/organization, consider their brand colors and identity +3. **Match palette to content**: Select colors that reflect the subject +4. **State your approach**: Explain your design choices before writing code + +**Requirements**: +- โœ… State your content-informed design approach BEFORE writing code +- โœ… Use web-safe fonts only: Arial, Helvetica, Times New Roman, Georgia, Courier New, Verdana, Tahoma, Trebuchet MS, Impact +- โœ… Create clear visual hierarchy through size, weight, and color +- โœ… Ensure readability: strong contrast, appropriately sized text, clean alignment +- โœ… Be consistent: repeat patterns, spacing, and visual language across slides + +#### Color Palette Selection + +**Choosing colors creatively**: +- **Think beyond defaults**: What colors genuinely match this specific topic? Avoid autopilot choices. +- **Consider multiple angles**: Topic, industry, mood, energy level, target audience, brand identity (if mentioned) +- **Be adventurous**: Try unexpected combinations - a healthcare presentation doesn't have to be green, finance doesn't have to be navy +- **Build your palette**: Pick 3-5 colors that work together (dominant colors + supporting tones + accent) +- **Ensure contrast**: Text must be clearly readable on backgrounds + +**Example color palettes** (use these to spark creativity - choose one, adapt it, or create your own): + +1. **Classic Blue**: Deep navy (#1C2833), slate gray (#2E4053), silver (#AAB7B8), off-white (#F4F6F6) +2. **Teal & Coral**: Teal (#5EA8A7), deep teal (#277884), coral (#FE4447), white (#FFFFFF) +3. **Bold Red**: Red (#C0392B), bright red (#E74C3C), orange (#F39C12), yellow (#F1C40F), green (#2ECC71) +4. **Warm Blush**: Mauve (#A49393), blush (#EED6D3), rose (#E8B4B8), cream (#FAF7F2) +5. **Burgundy Luxury**: Burgundy (#5D1D2E), crimson (#951233), rust (#C15937), gold (#997929) +6. **Deep Purple & Emerald**: Purple (#B165FB), dark blue (#181B24), emerald (#40695B), white (#FFFFFF) +7. **Cream & Forest Green**: Cream (#FFE1C7), forest green (#40695B), white (#FCFCFC) +8. **Pink & Purple**: Pink (#F8275B), coral (#FF574A), rose (#FF737D), purple (#3D2F68) +9. **Lime & Plum**: Lime (#C5DE82), plum (#7C3A5F), coral (#FD8C6E), blue-gray (#98ACB5) +10. **Black & Gold**: Gold (#BF9A4A), black (#000000), cream (#F4F6F6) +11. **Sage & Terracotta**: Sage (#87A96B), terracotta (#E07A5F), cream (#F4F1DE), charcoal (#2C2C2C) +12. **Charcoal & Red**: Charcoal (#292929), red (#E33737), light gray (#CCCBCB) +13. **Vibrant Orange**: Orange (#F96D00), light gray (#F2F2F2), charcoal (#222831) +14. **Forest Green**: Black (#191A19), green (#4E9F3D), dark green (#1E5128), white (#FFFFFF) +15. **Retro Rainbow**: Purple (#722880), pink (#D72D51), orange (#EB5C18), amber (#F08800), gold (#DEB600) +16. **Vintage Earthy**: Mustard (#E3B448), sage (#CBD18F), forest green (#3A6B35), cream (#F4F1DE) +17. **Coastal Rose**: Old rose (#AD7670), beaver (#B49886), eggshell (#F3ECDC), ash gray (#BFD5BE) +18. **Orange & Turquoise**: Light orange (#FC993E), grayish turquoise (#667C6F), white (#FCFCFC) + +#### Visual Details Options + +**Geometric Patterns**: +- Diagonal section dividers instead of horizontal +- Asymmetric column widths (30/70, 40/60, 25/75) +- Rotated text headers at 90ยฐ or 270ยฐ +- Circular/hexagonal frames for images +- Triangular accent shapes in corners +- Overlapping shapes for depth + +**Border & Frame Treatments**: +- Thick single-color borders (10-20pt) on one side only +- Double-line borders with contrasting colors +- Corner brackets instead of full frames +- L-shaped borders (top+left or bottom+right) +- Underline accents beneath headers (3-5pt thick) + +**Typography Treatments**: +- Extreme size contrast (72pt headlines vs 11pt body) +- All-caps headers with wide letter spacing +- Numbered sections in oversized display type +- Monospace (Courier New) for data/stats/technical content +- Condensed fonts (Arial Narrow) for dense information +- Outlined text for emphasis + +**Chart & Data Styling**: +- Monochrome charts with single accent color for key data +- Horizontal bar charts instead of vertical +- Dot plots instead of bar charts +- Minimal gridlines or none at all +- Data labels directly on elements (no legends) +- Oversized numbers for key metrics + +**Layout Innovations**: +- Full-bleed images with text overlays +- Sidebar column (20-30% width) for navigation/context +- Modular grid systems (3ร—3, 4ร—4 blocks) +- Z-pattern or F-pattern content flow +- Floating text boxes over colored shapes +- Magazine-style multi-column layouts + +**Background Treatments**: +- Solid color blocks occupying 40-60% of slide +- Gradient fills (vertical or diagonal only) +- Split backgrounds (two colors, diagonal or vertical) +- Edge-to-edge color bands +- Negative space as a design element + +### Layout Tips +**When creating slides with charts or tables:** +- **Two-column layout (PREFERRED)**: Use a header spanning the full width, then two columns below - text/bullets in one column and the featured content in the other. This provides better balance and makes charts/tables more readable. Use flexbox with unequal column widths (e.g., 40%/60% split) to optimize space for each content type. +- **Full-slide layout**: Let the featured content (chart/table) take up the entire slide for maximum impact and readability +- **NEVER vertically stack**: Do not place charts/tables below text in a single column - this causes poor readability and layout issues + +### Workflow +1. **MANDATORY - READ ENTIRE FILE**: Read [`html2pptx.md`](html2pptx.md) completely from start to finish. **NEVER set any range limits when reading this file.** Read the full file content for detailed syntax, critical formatting rules, and best practices before proceeding with presentation creation. +2. Create an HTML file for each slide with proper dimensions (e.g., 720pt ร— 405pt for 16:9) + - Use `

`, `

`-`

`, `
    `, `
      ` for all text content + - Use `class="placeholder"` for areas where charts/tables will be added (render with gray background for visibility) + - **CRITICAL**: Rasterize gradients and icons as PNG images FIRST using Sharp, then reference in HTML + - **LAYOUT**: For slides with charts/tables/images, use either full-slide layout or two-column layout for better readability +3. Create and run a JavaScript file using the [`html2pptx.js`](scripts/html2pptx.js) library to convert HTML slides to PowerPoint and save the presentation + - Use the `html2pptx()` function to process each HTML file + - Add charts and tables to placeholder areas using PptxGenJS API + - Save the presentation using `pptx.writeFile()` +4. **Visual validation**: Generate thumbnails and inspect for layout issues + - Create thumbnail grid: `python scripts/thumbnail.py output.pptx workspace/thumbnails --cols 4` + - Read and carefully examine the thumbnail image for: + - **Text cutoff**: Text being cut off by header bars, shapes, or slide edges + - **Text overlap**: Text overlapping with other text or shapes + - **Positioning issues**: Content too close to slide boundaries or other elements + - **Contrast issues**: Insufficient contrast between text and backgrounds + - If issues found, adjust HTML margins/spacing/colors and regenerate the presentation + - Repeat until all slides are visually correct + +## Editing an existing PowerPoint presentation + +When edit slides in an existing PowerPoint presentation, you need to work with the raw Office Open XML (OOXML) format. This involves unpacking the .pptx file, editing the XML content, and repacking it. + +### Workflow +1. **MANDATORY - READ ENTIRE FILE**: Read [`ooxml.md`](ooxml.md) (~500 lines) completely from start to finish. **NEVER set any range limits when reading this file.** Read the full file content for detailed guidance on OOXML structure and editing workflows before any presentation editing. +2. Unpack the presentation: `python ooxml/scripts/unpack.py ` +3. Edit the XML files (primarily `ppt/slides/slide{N}.xml` and related files) +4. **CRITICAL**: Validate immediately after each edit and fix any validation errors before proceeding: `python ooxml/scripts/validate.py --original ` +5. Pack the final presentation: `python ooxml/scripts/pack.py ` + +## Creating a new PowerPoint presentation **using a template** + +When you need to create a presentation that follows an existing template's design, you'll need to duplicate and re-arrange template slides before then replacing placeholder context. + +### Workflow +1. **Extract template text AND create visual thumbnail grid**: + * Extract text: `python -m markitdown template.pptx > template-content.md` + * Read `template-content.md`: Read the entire file to understand the contents of the template presentation. **NEVER set any range limits when reading this file.** + * Create thumbnail grids: `python scripts/thumbnail.py template.pptx` + * See [Creating Thumbnail Grids](#creating-thumbnail-grids) section for more details + +2. **Analyze template and save inventory to a file**: + * **Visual Analysis**: Review thumbnail grid(s) to understand slide layouts, design patterns, and visual structure + * Create and save a template inventory file at `template-inventory.md` containing: + ```markdown + # Template Inventory Analysis + **Total Slides: [count]** + **IMPORTANT: Slides are 0-indexed (first slide = 0, last slide = count-1)** + + ## [Category Name] + - Slide 0: [Layout code if available] - Description/purpose + - Slide 1: [Layout code] - Description/purpose + - Slide 2: [Layout code] - Description/purpose + [... EVERY slide must be listed individually with its index ...] + ``` + * **Using the thumbnail grid**: Reference the visual thumbnails to identify: + - Layout patterns (title slides, content layouts, section dividers) + - Image placeholder locations and counts + - Design consistency across slide groups + - Visual hierarchy and structure + * This inventory file is REQUIRED for selecting appropriate templates in the next step + +3. **Create presentation outline based on template inventory**: + * Review available templates from step 2. + * Choose an intro or title template for the first slide. This should be one of the first templates. + * Choose safe, text-based layouts for the other slides. + * **CRITICAL: Match layout structure to actual content**: + - Single-column layouts: Use for unified narrative or single topic + - Two-column layouts: Use ONLY when you have exactly 2 distinct items/concepts + - Three-column layouts: Use ONLY when you have exactly 3 distinct items/concepts + - Image + text layouts: Use ONLY when you have actual images to insert + - Quote layouts: Use ONLY for actual quotes from people (with attribution), never for emphasis + - Never use layouts with more placeholders than you have content + - If you have 2 items, don't force them into a 3-column layout + - If you have 4+ items, consider breaking into multiple slides or using a list format + * Count your actual content pieces BEFORE selecting the layout + * Verify each placeholder in the chosen layout will be filled with meaningful content + * Select one option representing the **best** layout for each content section. + * Save `outline.md` with content AND template mapping that leverages available designs + * Example template mapping: + ``` + # Template slides to use (0-based indexing) + # WARNING: Verify indices are within range! Template with 73 slides has indices 0-72 + # Mapping: slide numbers from outline -> template slide indices + template_mapping = [ + 0, # Use slide 0 (Title/Cover) + 34, # Use slide 34 (B1: Title and body) + 34, # Use slide 34 again (duplicate for second B1) + 50, # Use slide 50 (E1: Quote) + 54, # Use slide 54 (F2: Closing + Text) + ] + ``` + +4. **Duplicate, reorder, and delete slides using `rearrange.py`**: + * Use the `scripts/rearrange.py` script to create a new presentation with slides in the desired order: + ```bash + python scripts/rearrange.py template.pptx working.pptx 0,34,34,50,52 + ``` + * The script handles duplicating repeated slides, deleting unused slides, and reordering automatically + * Slide indices are 0-based (first slide is 0, second is 1, etc.) + * The same slide index can appear multiple times to duplicate that slide + +5. **Extract ALL text using the `inventory.py` script**: + * **Run inventory extraction**: + ```bash + python scripts/inventory.py working.pptx text-inventory.json + ``` + * **Read text-inventory.json**: Read the entire text-inventory.json file to understand all shapes and their properties. **NEVER set any range limits when reading this file.** + + * The inventory JSON structure: + ```json + { + "slide-0": { + "shape-0": { + "placeholder_type": "TITLE", // or null for non-placeholders + "left": 1.5, // position in inches + "top": 2.0, + "width": 7.5, + "height": 1.2, + "paragraphs": [ + { + "text": "Paragraph text", + // Optional properties (only included when non-default): + "bullet": true, // explicit bullet detected + "level": 0, // only included when bullet is true + "alignment": "CENTER", // CENTER, RIGHT (not LEFT) + "space_before": 10.0, // space before paragraph in points + "space_after": 6.0, // space after paragraph in points + "line_spacing": 22.4, // line spacing in points + "font_name": "Arial", // from first run + "font_size": 14.0, // in points + "bold": true, + "italic": false, + "underline": false, + "color": "FF0000" // RGB color + } + ] + } + } + } + ``` + + * Key features: + - **Slides**: Named as "slide-0", "slide-1", etc. + - **Shapes**: Ordered by visual position (top-to-bottom, left-to-right) as "shape-0", "shape-1", etc. + - **Placeholder types**: TITLE, CENTER_TITLE, SUBTITLE, BODY, OBJECT, or null + - **Default font size**: `default_font_size` in points extracted from layout placeholders (when available) + - **Slide numbers are filtered**: Shapes with SLIDE_NUMBER placeholder type are automatically excluded from inventory + - **Bullets**: When `bullet: true`, `level` is always included (even if 0) + - **Spacing**: `space_before`, `space_after`, and `line_spacing` in points (only included when set) + - **Colors**: `color` for RGB (e.g., "FF0000"), `theme_color` for theme colors (e.g., "DARK_1") + - **Properties**: Only non-default values are included in the output + +6. **Generate replacement text and save the data to a JSON file** + Based on the text inventory from the previous step: + - **CRITICAL**: First verify which shapes exist in the inventory - only reference shapes that are actually present + - **VALIDATION**: The replace.py script will validate that all shapes in your replacement JSON exist in the inventory + - If you reference a non-existent shape, you'll get an error showing available shapes + - If you reference a non-existent slide, you'll get an error indicating the slide doesn't exist + - All validation errors are shown at once before the script exits + - **IMPORTANT**: The replace.py script uses inventory.py internally to identify ALL text shapes + - **AUTOMATIC CLEARING**: ALL text shapes from the inventory will be cleared unless you provide "paragraphs" for them + - Add a "paragraphs" field to shapes that need content (not "replacement_paragraphs") + - Shapes without "paragraphs" in the replacement JSON will have their text cleared automatically + - Paragraphs with bullets will be automatically left aligned. Don't set the `alignment` property on when `"bullet": true` + - Generate appropriate replacement content for placeholder text + - Use shape size to determine appropriate content length + - **CRITICAL**: Include paragraph properties from the original inventory - don't just provide text + - **IMPORTANT**: When bullet: true, do NOT include bullet symbols (โ€ข, -, *) in text - they're added automatically + - **ESSENTIAL FORMATTING RULES**: + - Headers/titles should typically have `"bold": true` + - List items should have `"bullet": true, "level": 0` (level is required when bullet is true) + - Preserve any alignment properties (e.g., `"alignment": "CENTER"` for centered text) + - Include font properties when different from default (e.g., `"font_size": 14.0`, `"font_name": "Lora"`) + - Colors: Use `"color": "FF0000"` for RGB or `"theme_color": "DARK_1"` for theme colors + - The replacement script expects **properly formatted paragraphs**, not just text strings + - **Overlapping shapes**: Prefer shapes with larger default_font_size or more appropriate placeholder_type + - Save the updated inventory with replacements to `replacement-text.json` + - **WARNING**: Different template layouts have different shape counts - always check the actual inventory before creating replacements + + Example paragraphs field showing proper formatting: + ```json + "paragraphs": [ + { + "text": "New presentation title text", + "alignment": "CENTER", + "bold": true + }, + { + "text": "Section Header", + "bold": true + }, + { + "text": "First bullet point without bullet symbol", + "bullet": true, + "level": 0 + }, + { + "text": "Red colored text", + "color": "FF0000" + }, + { + "text": "Theme colored text", + "theme_color": "DARK_1" + }, + { + "text": "Regular paragraph text without special formatting" + } + ] + ``` + + **Shapes not listed in the replacement JSON are automatically cleared**: + ```json + { + "slide-0": { + "shape-0": { + "paragraphs": [...] // This shape gets new text + } + // shape-1 and shape-2 from inventory will be cleared automatically + } + } + ``` + + **Common formatting patterns for presentations**: + - Title slides: Bold text, sometimes centered + - Section headers within slides: Bold text + - Bullet lists: Each item needs `"bullet": true, "level": 0` + - Body text: Usually no special properties needed + - Quotes: May have special alignment or font properties + +7. **Apply replacements using the `replace.py` script** + ```bash + python scripts/replace.py working.pptx replacement-text.json output.pptx + ``` + + The script will: + - First extract the inventory of ALL text shapes using functions from inventory.py + - Validate that all shapes in the replacement JSON exist in the inventory + - Clear text from ALL shapes identified in the inventory + - Apply new text only to shapes with "paragraphs" defined in the replacement JSON + - Preserve formatting by applying paragraph properties from the JSON + - Handle bullets, alignment, font properties, and colors automatically + - Save the updated presentation + + Example validation errors: + ``` + ERROR: Invalid shapes in replacement JSON: + - Shape 'shape-99' not found on 'slide-0'. Available shapes: shape-0, shape-1, shape-4 + - Slide 'slide-999' not found in inventory + ``` + + ``` + ERROR: Replacement text made overflow worse in these shapes: + - slide-0/shape-2: overflow worsened by 1.25" (was 0.00", now 1.25") + ``` + +## Creating Thumbnail Grids + +To create visual thumbnail grids of PowerPoint slides for quick analysis and reference: + +```bash +python scripts/thumbnail.py template.pptx [output_prefix] +``` + +**Features**: +- Creates: `thumbnails.jpg` (or `thumbnails-1.jpg`, `thumbnails-2.jpg`, etc. for large decks) +- Default: 5 columns, max 30 slides per grid (5ร—6) +- Custom prefix: `python scripts/thumbnail.py template.pptx my-grid` + - Note: The output prefix should include the path if you want output in a specific directory (e.g., `workspace/my-grid`) +- Adjust columns: `--cols 4` (range: 3-6, affects slides per grid) +- Grid limits: 3 cols = 12 slides/grid, 4 cols = 20, 5 cols = 30, 6 cols = 42 +- Slides are zero-indexed (Slide 0, Slide 1, etc.) + +**Use cases**: +- Template analysis: Quickly understand slide layouts and design patterns +- Content review: Visual overview of entire presentation +- Navigation reference: Find specific slides by their visual appearance +- Quality check: Verify all slides are properly formatted + +**Examples**: +```bash +# Basic usage +python scripts/thumbnail.py presentation.pptx + +# Combine options: custom name, columns +python scripts/thumbnail.py template.pptx analysis --cols 4 +``` + +## Converting Slides to Images + +To visually analyze PowerPoint slides, convert them to images using a two-step process: + +1. **Convert PPTX to PDF**: + ```bash + soffice --headless --convert-to pdf template.pptx + ``` + +2. **Convert PDF pages to JPEG images**: + ```bash + pdftoppm -jpeg -r 150 template.pdf slide + ``` + This creates files like `slide-1.jpg`, `slide-2.jpg`, etc. + +Options: +- `-r 150`: Sets resolution to 150 DPI (adjust for quality/size balance) +- `-jpeg`: Output JPEG format (use `-png` for PNG if preferred) +- `-f N`: First page to convert (e.g., `-f 2` starts from page 2) +- `-l N`: Last page to convert (e.g., `-l 5` stops at page 5) +- `slide`: Prefix for output files + +Example for specific range: +```bash +pdftoppm -jpeg -r 150 -f 2 -l 5 template.pdf slide # Converts only pages 2-5 +``` + +## Code Style Guidelines +**IMPORTANT**: When generating code for PPTX operations: +- Write concise code +- Avoid verbose variable names and redundant operations +- Avoid unnecessary print statements + +## Dependencies + +Required dependencies (should already be installed): + +- **markitdown**: `pip install "markitdown[pptx]"` (for text extraction from presentations) +- **pptxgenjs**: `npm install -g pptxgenjs` (for creating presentations via html2pptx) +- **playwright**: `npm install -g playwright` (for HTML rendering in html2pptx) +- **react-icons**: `npm install -g react-icons react react-dom` (for icons) +- **sharp**: `npm install -g sharp` (for SVG rasterization and image processing) +- **LibreOffice**: `sudo apt-get install libreoffice` (for PDF conversion) +- **Poppler**: `sudo apt-get install poppler-utils` (for pdftoppm to convert PDF to images) +- **defusedxml**: `pip install defusedxml` (for secure XML parsing) + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/html2pptx.md b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/html2pptx.md new file mode 100644 index 00000000..106adf72 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/html2pptx.md @@ -0,0 +1,625 @@ +# HTML to PowerPoint Guide + +Convert HTML slides to PowerPoint presentations with accurate positioning using the `html2pptx.js` library. + +## Table of Contents + +1. [Creating HTML Slides](#creating-html-slides) +2. [Using the html2pptx Library](#using-the-html2pptx-library) +3. [Using PptxGenJS](#using-pptxgenjs) + +--- + +## Creating HTML Slides + +Every HTML slide must include proper body dimensions: + +### Layout Dimensions + +- **16:9** (default): `width: 720pt; height: 405pt` +- **4:3**: `width: 720pt; height: 540pt` +- **16:10**: `width: 720pt; height: 450pt` + +### Supported Elements + +- `

      `, `

      `-`

      ` - Text with styling +- `
        `, `
          ` - Lists (never use manual bullets โ€ข, -, *) +- ``, `` - Bold text (inline formatting) +- ``, `` - Italic text (inline formatting) +- `` - Underlined text (inline formatting) +- `` - Inline formatting with CSS styles (bold, italic, underline, color) +- `
          ` - Line breaks +- `
          ` with bg/border - Becomes shape +- `` - Images +- `class="placeholder"` - Reserved space for charts (returns `{ id, x, y, w, h }`) + +### Critical Text Rules + +**ALL text MUST be inside `

          `, `

          `-`

          `, `
            `, or `
              ` tags:** +- โœ… Correct: `

              Text here

              ` +- โŒ Wrong: `
              Text here
              ` - **Text will NOT appear in PowerPoint** +- โŒ Wrong: `Text` - **Text will NOT appear in PowerPoint** +- Text in `
              ` or `` without a text tag will be silently ignored + +**NEVER use manual bullet symbols (โ€ข, -, *, etc.)** - Use `
                ` or `
                  ` lists instead + +**ONLY use web-safe fonts that are universally available:** +- โœ… Web-safe fonts: `Arial`, `Helvetica`, `Times New Roman`, `Georgia`, `Courier New`, `Verdana`, `Tahoma`, `Trebuchet MS`, `Impact`, `Comic Sans MS` +- โŒ Wrong: `'Segoe UI'`, `'SF Pro'`, `'Roboto'`, custom fonts - **Might cause rendering issues** + +### Styling + +- Use `display: flex` on body to prevent margin collapse from breaking overflow validation +- Use `margin` for spacing (padding included in size) +- Inline formatting: Use ``, ``, `` tags OR `` with CSS styles + - `` supports: `font-weight: bold`, `font-style: italic`, `text-decoration: underline`, `color: #rrggbb` + - `` does NOT support: `margin`, `padding` (not supported in PowerPoint text runs) + - Example: `Bold blue text` +- Flexbox works - positions calculated from rendered layout +- Use hex colors with `#` prefix in CSS +- **Text alignment**: Use CSS `text-align` (`center`, `right`, etc.) when needed as a hint to PptxGenJS for text formatting if text lengths are slightly off + +### Shape Styling (DIV elements only) + +**IMPORTANT: Backgrounds, borders, and shadows only work on `
                  ` elements, NOT on text elements (`

                  `, `

                  `-`

                  `, `
                    `, `
                      `)** + +- **Backgrounds**: CSS `background` or `background-color` on `
                      ` elements only + - Example: `
                      ` - Creates a shape with background +- **Borders**: CSS `border` on `
                      ` elements converts to PowerPoint shape borders + - Supports uniform borders: `border: 2px solid #333333` + - Supports partial borders: `border-left`, `border-right`, `border-top`, `border-bottom` (rendered as line shapes) + - Example: `
                      ` +- **Border radius**: CSS `border-radius` on `
                      ` elements for rounded corners + - `border-radius: 50%` or higher creates circular shape + - Percentages <50% calculated relative to shape's smaller dimension + - Supports px and pt units (e.g., `border-radius: 8pt;`, `border-radius: 12px;`) + - Example: `
                      ` on 100x200px box = 25% of 100px = 25px radius +- **Box shadows**: CSS `box-shadow` on `
                      ` elements converts to PowerPoint shadows + - Supports outer shadows only (inset shadows are ignored to prevent corruption) + - Example: `
                      ` + - Note: Inset/inner shadows are not supported by PowerPoint and will be skipped + +### Icons & Gradients + +- **CRITICAL: Never use CSS gradients (`linear-gradient`, `radial-gradient`)** - They don't convert to PowerPoint +- **ALWAYS create gradient/icon PNGs FIRST using Sharp, then reference in HTML** +- For gradients: Rasterize SVG to PNG background images +- For icons: Rasterize react-icons SVG to PNG images +- All visual effects must be pre-rendered as raster images before HTML rendering + +**Rasterizing Icons with Sharp:** + +```javascript +const React = require('react'); +const ReactDOMServer = require('react-dom/server'); +const sharp = require('sharp'); +const { FaHome } = require('react-icons/fa'); + +async function rasterizeIconPng(IconComponent, color, size = "256", filename) { + const svgString = ReactDOMServer.renderToStaticMarkup( + React.createElement(IconComponent, { color: `#${color}`, size: size }) + ); + + // Convert SVG to PNG using Sharp + await sharp(Buffer.from(svgString)) + .png() + .toFile(filename); + + return filename; +} + +// Usage: Rasterize icon before using in HTML +const iconPath = await rasterizeIconPng(FaHome, "4472c4", "256", "home-icon.png"); +// Then reference in HTML: +``` + +**Rasterizing Gradients with Sharp:** + +```javascript +const sharp = require('sharp'); + +async function createGradientBackground(filename) { + const svg = ` + + + + + + + + `; + + await sharp(Buffer.from(svg)) + .png() + .toFile(filename); + + return filename; +} + +// Usage: Create gradient background before HTML +const bgPath = await createGradientBackground("gradient-bg.png"); +// Then in HTML: +``` + +### Example + +```html + + + + + + +
                      +

                      Recipe Title

                      +
                        +
                      • Item: Description
                      • +
                      +

                      Text with bold, italic, underline.

                      +
                      + + +
                      +

                      5

                      +
                      +
                      + + +``` + +## Using the html2pptx Library + +### Dependencies + +These libraries have been globally installed and are available to use: +- `pptxgenjs` +- `playwright` +- `sharp` + +### Basic Usage + +```javascript +const pptxgen = require('pptxgenjs'); +const html2pptx = require('./html2pptx'); + +const pptx = new pptxgen(); +pptx.layout = 'LAYOUT_16x9'; // Must match HTML body dimensions + +const { slide, placeholders } = await html2pptx('slide1.html', pptx); + +// Add chart to placeholder area +if (placeholders.length > 0) { + slide.addChart(pptx.charts.LINE, chartData, placeholders[0]); +} + +await pptx.writeFile('output.pptx'); +``` + +### API Reference + +#### Function Signature +```javascript +await html2pptx(htmlFile, pres, options) +``` + +#### Parameters +- `htmlFile` (string): Path to HTML file (absolute or relative) +- `pres` (pptxgen): PptxGenJS presentation instance with layout already set +- `options` (object, optional): + - `tmpDir` (string): Temporary directory for generated files (default: `process.env.TMPDIR || '/tmp'`) + - `slide` (object): Existing slide to reuse (default: creates new slide) + +#### Returns +```javascript +{ + slide: pptxgenSlide, // The created/updated slide + placeholders: [ // Array of placeholder positions + { id: string, x: number, y: number, w: number, h: number }, + ... + ] +} +``` + +### Validation + +The library automatically validates and collects all errors before throwing: + +1. **HTML dimensions must match presentation layout** - Reports dimension mismatches +2. **Content must not overflow body** - Reports overflow with exact measurements +3. **CSS gradients** - Reports unsupported gradient usage +4. **Text element styling** - Reports backgrounds/borders/shadows on text elements (only allowed on divs) + +**All validation errors are collected and reported together** in a single error message, allowing you to fix all issues at once instead of one at a time. + +### Working with Placeholders + +```javascript +const { slide, placeholders } = await html2pptx('slide.html', pptx); + +// Use first placeholder +slide.addChart(pptx.charts.BAR, data, placeholders[0]); + +// Find by ID +const chartArea = placeholders.find(p => p.id === 'chart-area'); +slide.addChart(pptx.charts.LINE, data, chartArea); +``` + +### Complete Example + +```javascript +const pptxgen = require('pptxgenjs'); +const html2pptx = require('./html2pptx'); + +async function createPresentation() { + const pptx = new pptxgen(); + pptx.layout = 'LAYOUT_16x9'; + pptx.author = 'Your Name'; + pptx.title = 'My Presentation'; + + // Slide 1: Title + const { slide: slide1 } = await html2pptx('slides/title.html', pptx); + + // Slide 2: Content with chart + const { slide: slide2, placeholders } = await html2pptx('slides/data.html', pptx); + + const chartData = [{ + name: 'Sales', + labels: ['Q1', 'Q2', 'Q3', 'Q4'], + values: [4500, 5500, 6200, 7100] + }]; + + slide2.addChart(pptx.charts.BAR, chartData, { + ...placeholders[0], + showTitle: true, + title: 'Quarterly Sales', + showCatAxisTitle: true, + catAxisTitle: 'Quarter', + showValAxisTitle: true, + valAxisTitle: 'Sales ($000s)' + }); + + // Save + await pptx.writeFile({ fileName: 'presentation.pptx' }); + console.log('Presentation created successfully!'); +} + +createPresentation().catch(console.error); +``` + +## Using PptxGenJS + +After converting HTML to slides with `html2pptx`, you'll use PptxGenJS to add dynamic content like charts, images, and additional elements. + +### โš ๏ธ Critical Rules + +#### Colors +- **NEVER use `#` prefix** with hex colors in PptxGenJS - causes file corruption +- โœ… Correct: `color: "FF0000"`, `fill: { color: "0066CC" }` +- โŒ Wrong: `color: "#FF0000"` (breaks document) + +### Adding Images + +Always calculate aspect ratios from actual image dimensions: + +```javascript +// Get image dimensions: identify image.png | grep -o '[0-9]* x [0-9]*' +const imgWidth = 1860, imgHeight = 1519; // From actual file +const aspectRatio = imgWidth / imgHeight; + +const h = 3; // Max height +const w = h * aspectRatio; +const x = (10 - w) / 2; // Center on 16:9 slide + +slide.addImage({ path: "chart.png", x, y: 1.5, w, h }); +``` + +### Adding Text + +```javascript +// Rich text with formatting +slide.addText([ + { text: "Bold ", options: { bold: true } }, + { text: "Italic ", options: { italic: true } }, + { text: "Normal" } +], { + x: 1, y: 2, w: 8, h: 1 +}); +``` + +### Adding Shapes + +```javascript +// Rectangle +slide.addShape(pptx.shapes.RECTANGLE, { + x: 1, y: 1, w: 3, h: 2, + fill: { color: "4472C4" }, + line: { color: "000000", width: 2 } +}); + +// Circle +slide.addShape(pptx.shapes.OVAL, { + x: 5, y: 1, w: 2, h: 2, + fill: { color: "ED7D31" } +}); + +// Rounded rectangle +slide.addShape(pptx.shapes.ROUNDED_RECTANGLE, { + x: 1, y: 4, w: 3, h: 1.5, + fill: { color: "70AD47" }, + rectRadius: 0.2 +}); +``` + +### Adding Charts + +**Required for most charts:** Axis labels using `catAxisTitle` (category) and `valAxisTitle` (value). + +**Chart Data Format:** +- Use **single series with all labels** for simple bar/line charts +- Each series creates a separate legend entry +- Labels array defines X-axis values + +**Time Series Data - Choose Correct Granularity:** +- **< 30 days**: Use daily grouping (e.g., "10-01", "10-02") - avoid monthly aggregation that creates single-point charts +- **30-365 days**: Use monthly grouping (e.g., "2024-01", "2024-02") +- **> 365 days**: Use yearly grouping (e.g., "2023", "2024") +- **Validate**: Charts with only 1 data point likely indicate incorrect aggregation for the time period + +```javascript +const { slide, placeholders } = await html2pptx('slide.html', pptx); + +// CORRECT: Single series with all labels +slide.addChart(pptx.charts.BAR, [{ + name: "Sales 2024", + labels: ["Q1", "Q2", "Q3", "Q4"], + values: [4500, 5500, 6200, 7100] +}], { + ...placeholders[0], // Use placeholder position + barDir: 'col', // 'col' = vertical bars, 'bar' = horizontal + showTitle: true, + title: 'Quarterly Sales', + showLegend: false, // No legend needed for single series + // Required axis labels + showCatAxisTitle: true, + catAxisTitle: 'Quarter', + showValAxisTitle: true, + valAxisTitle: 'Sales ($000s)', + // Optional: Control scaling (adjust min based on data range for better visualization) + valAxisMaxVal: 8000, + valAxisMinVal: 0, // Use 0 for counts/amounts; for clustered data (e.g., 4500-7100), consider starting closer to min value + valAxisMajorUnit: 2000, // Control y-axis label spacing to prevent crowding + catAxisLabelRotate: 45, // Rotate labels if crowded + dataLabelPosition: 'outEnd', + dataLabelColor: '000000', + // Use single color for single-series charts + chartColors: ["4472C4"] // All bars same color +}); +``` + +#### Scatter Chart + +**IMPORTANT**: Scatter chart data format is unusual - first series contains X-axis values, subsequent series contain Y-values: + +```javascript +// Prepare data +const data1 = [{ x: 10, y: 20 }, { x: 15, y: 25 }, { x: 20, y: 30 }]; +const data2 = [{ x: 12, y: 18 }, { x: 18, y: 22 }]; + +const allXValues = [...data1.map(d => d.x), ...data2.map(d => d.x)]; + +slide.addChart(pptx.charts.SCATTER, [ + { name: 'X-Axis', values: allXValues }, // First series = X values + { name: 'Series 1', values: data1.map(d => d.y) }, // Y values only + { name: 'Series 2', values: data2.map(d => d.y) } // Y values only +], { + x: 1, y: 1, w: 8, h: 4, + lineSize: 0, // 0 = no connecting lines + lineDataSymbol: 'circle', + lineDataSymbolSize: 6, + showCatAxisTitle: true, + catAxisTitle: 'X Axis', + showValAxisTitle: true, + valAxisTitle: 'Y Axis', + chartColors: ["4472C4", "ED7D31"] +}); +``` + +#### Line Chart + +```javascript +slide.addChart(pptx.charts.LINE, [{ + name: "Temperature", + labels: ["Jan", "Feb", "Mar", "Apr"], + values: [32, 35, 42, 55] +}], { + x: 1, y: 1, w: 8, h: 4, + lineSize: 4, + lineSmooth: true, + // Required axis labels + showCatAxisTitle: true, + catAxisTitle: 'Month', + showValAxisTitle: true, + valAxisTitle: 'Temperature (ยฐF)', + // Optional: Y-axis range (set min based on data range for better visualization) + valAxisMinVal: 0, // For ranges starting at 0 (counts, percentages, etc.) + valAxisMaxVal: 60, + valAxisMajorUnit: 20, // Control y-axis label spacing to prevent crowding (e.g., 10, 20, 25) + // valAxisMinVal: 30, // PREFERRED: For data clustered in a range (e.g., 32-55 or ratings 3-5), start axis closer to min value to show variation + // Optional: Chart colors + chartColors: ["4472C4", "ED7D31", "A5A5A5"] +}); +``` + +#### Pie Chart (No Axis Labels Required) + +**CRITICAL**: Pie charts require a **single data series** with all categories in the `labels` array and corresponding values in the `values` array. + +```javascript +slide.addChart(pptx.charts.PIE, [{ + name: "Market Share", + labels: ["Product A", "Product B", "Other"], // All categories in one array + values: [35, 45, 20] // All values in one array +}], { + x: 2, y: 1, w: 6, h: 4, + showPercent: true, + showLegend: true, + legendPos: 'r', // right + chartColors: ["4472C4", "ED7D31", "A5A5A5"] +}); +``` + +#### Multiple Data Series + +```javascript +slide.addChart(pptx.charts.LINE, [ + { + name: "Product A", + labels: ["Q1", "Q2", "Q3", "Q4"], + values: [10, 20, 30, 40] + }, + { + name: "Product B", + labels: ["Q1", "Q2", "Q3", "Q4"], + values: [15, 25, 20, 35] + } +], { + x: 1, y: 1, w: 8, h: 4, + showCatAxisTitle: true, + catAxisTitle: 'Quarter', + showValAxisTitle: true, + valAxisTitle: 'Revenue ($M)' +}); +``` + +### Chart Colors + +**CRITICAL**: Use hex colors **without** the `#` prefix - including `#` causes file corruption. + +**Align chart colors with your chosen design palette**, ensuring sufficient contrast and distinctiveness for data visualization. Adjust colors for: +- Strong contrast between adjacent series +- Readability against slide backgrounds +- Accessibility (avoid red-green only combinations) + +```javascript +// Example: Ocean palette-inspired chart colors (adjusted for contrast) +const chartColors = ["16A085", "FF6B9D", "2C3E50", "F39C12", "9B59B6"]; + +// Single-series chart: Use one color for all bars/points +slide.addChart(pptx.charts.BAR, [{ + name: "Sales", + labels: ["Q1", "Q2", "Q3", "Q4"], + values: [4500, 5500, 6200, 7100] +}], { + ...placeholders[0], + chartColors: ["16A085"], // All bars same color + showLegend: false +}); + +// Multi-series chart: Each series gets a different color +slide.addChart(pptx.charts.LINE, [ + { name: "Product A", labels: ["Q1", "Q2", "Q3"], values: [10, 20, 30] }, + { name: "Product B", labels: ["Q1", "Q2", "Q3"], values: [15, 25, 20] } +], { + ...placeholders[0], + chartColors: ["16A085", "FF6B9D"] // One color per series +}); +``` + +### Adding Tables + +Tables can be added with basic or advanced formatting: + +#### Basic Table + +```javascript +slide.addTable([ + ["Header 1", "Header 2", "Header 3"], + ["Row 1, Col 1", "Row 1, Col 2", "Row 1, Col 3"], + ["Row 2, Col 1", "Row 2, Col 2", "Row 2, Col 3"] +], { + x: 0.5, + y: 1, + w: 9, + h: 3, + border: { pt: 1, color: "999999" }, + fill: { color: "F1F1F1" } +}); +``` + +#### Table with Custom Formatting + +```javascript +const tableData = [ + // Header row with custom styling + [ + { text: "Product", options: { fill: { color: "4472C4" }, color: "FFFFFF", bold: true } }, + { text: "Revenue", options: { fill: { color: "4472C4" }, color: "FFFFFF", bold: true } }, + { text: "Growth", options: { fill: { color: "4472C4" }, color: "FFFFFF", bold: true } } + ], + // Data rows + ["Product A", "$50M", "+15%"], + ["Product B", "$35M", "+22%"], + ["Product C", "$28M", "+8%"] +]; + +slide.addTable(tableData, { + x: 1, + y: 1.5, + w: 8, + h: 3, + colW: [3, 2.5, 2.5], // Column widths + rowH: [0.5, 0.6, 0.6, 0.6], // Row heights + border: { pt: 1, color: "CCCCCC" }, + align: "center", + valign: "middle", + fontSize: 14 +}); +``` + +#### Table with Merged Cells + +```javascript +const mergedTableData = [ + [ + { text: "Q1 Results", options: { colspan: 3, fill: { color: "4472C4" }, color: "FFFFFF", bold: true } } + ], + ["Product", "Sales", "Market Share"], + ["Product A", "$25M", "35%"], + ["Product B", "$18M", "25%"] +]; + +slide.addTable(mergedTableData, { + x: 1, + y: 1, + w: 8, + h: 2.5, + colW: [3, 2.5, 2.5], + border: { pt: 1, color: "DDDDDD" } +}); +``` + +### Table Options + +Common table options: +- `x, y, w, h` - Position and size +- `colW` - Array of column widths (in inches) +- `rowH` - Array of row heights (in inches) +- `border` - Border style: `{ pt: 1, color: "999999" }` +- `fill` - Background color (no # prefix) +- `align` - Text alignment: "left", "center", "right" +- `valign` - Vertical alignment: "top", "middle", "bottom" +- `fontSize` - Text size +- `autoPage` - Auto-create new slides if content overflows \ No newline at end of file diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml.md b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml.md new file mode 100644 index 00000000..951b3cf6 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml.md @@ -0,0 +1,427 @@ +# Office Open XML Technical Reference for PowerPoint + +**Important: Read this entire document before starting.** Critical XML schema rules and formatting requirements are covered throughout. Incorrect implementation can create invalid PPTX files that PowerPoint cannot open. + +## Technical Guidelines + +### Schema Compliance +- **Element ordering in ``**: ``, ``, `` +- **Whitespace**: Add `xml:space='preserve'` to `` elements with leading/trailing spaces +- **Unicode**: Escape characters in ASCII content: `"` becomes `“` +- **Images**: Add to `ppt/media/`, reference in slide XML, set dimensions to fit slide bounds +- **Relationships**: Update `ppt/slides/_rels/slideN.xml.rels` for each slide's resources +- **Dirty attribute**: Add `dirty="0"` to `` and `` elements to indicate clean state + +## Presentation Structure + +### Basic Slide Structure +```xml + + + + + ... + ... + + + + +``` + +### Text Box / Shape with Text +```xml + + + + + + + + + + + + + + + + + + + + + + Slide Title + + + + +``` + +### Text Formatting +```xml + + + + Bold Text + + + + + + Italic Text + + + + + + Underlined + + + + + + + + + + Highlighted Text + + + + + + + + + + Colored Arial 24pt + + + + + + + + + + Formatted text + +``` + +### Lists +```xml + + + + + + + First bullet point + + + + + + + + + + First numbered item + + + + + + + + + + Indented bullet + + +``` + +### Shapes +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### Images +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### Tables +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + Cell 1 + + + + + + + + + + + Cell 2 + + + + + + + + + +``` + +### Slide Layouts + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +## File Updates + +When adding content, update these files: + +**`ppt/_rels/presentation.xml.rels`:** +```xml + + +``` + +**`ppt/slides/_rels/slide1.xml.rels`:** +```xml + + +``` + +**`[Content_Types].xml`:** +```xml + + + +``` + +**`ppt/presentation.xml`:** +```xml + + + + +``` + +**`docProps/app.xml`:** Update slide count and statistics +```xml +2 +10 +50 +``` + +## Slide Operations + +### Adding a New Slide +When adding a slide to the end of the presentation: + +1. **Create the slide file** (`ppt/slides/slideN.xml`) +2. **Update `[Content_Types].xml`**: Add Override for the new slide +3. **Update `ppt/_rels/presentation.xml.rels`**: Add relationship for the new slide +4. **Update `ppt/presentation.xml`**: Add slide ID to `` +5. **Create slide relationships** (`ppt/slides/_rels/slideN.xml.rels`) if needed +6. **Update `docProps/app.xml`**: Increment slide count and update statistics (if present) + +### Duplicating a Slide +1. Copy the source slide XML file with a new name +2. Update all IDs in the new slide to be unique +3. Follow the "Adding a New Slide" steps above +4. **CRITICAL**: Remove or update any notes slide references in `_rels` files +5. Remove references to unused media files + +### Reordering Slides +1. **Update `ppt/presentation.xml`**: Reorder `` elements in `` +2. The order of `` elements determines slide order +3. Keep slide IDs and relationship IDs unchanged + +Example: +```xml + + + + + + + + + + + + + +``` + +### Deleting a Slide +1. **Remove from `ppt/presentation.xml`**: Delete the `` entry +2. **Remove from `ppt/_rels/presentation.xml.rels`**: Delete the relationship +3. **Remove from `[Content_Types].xml`**: Delete the Override entry +4. **Delete files**: Remove `ppt/slides/slideN.xml` and `ppt/slides/_rels/slideN.xml.rels` +5. **Update `docProps/app.xml`**: Decrement slide count and update statistics +6. **Clean up unused media**: Remove orphaned images from `ppt/media/` + +Note: Don't renumber remaining slides - keep their original IDs and filenames. + + +## Common Errors to Avoid + +- **Encodings**: Escape unicode characters in ASCII content: `"` becomes `“` +- **Images**: Add to `ppt/media/` and update relationship files +- **Lists**: Omit bullets from list headers +- **IDs**: Use valid hexadecimal values for UUIDs +- **Themes**: Check all themes in `theme` directory for colors + +## Validation Checklist for Template-Based Presentations + +### Before Packing, Always: +- **Clean unused resources**: Remove unreferenced media, fonts, and notes directories +- **Fix Content_Types.xml**: Declare ALL slides, layouts, and themes present in the package +- **Fix relationship IDs**: + - Remove font embed references if not using embedded fonts +- **Remove broken references**: Check all `_rels` files for references to deleted resources + +### Common Template Duplication Pitfalls: +- Multiple slides referencing the same notes slide after duplication +- Image/media references from template slides that no longer exist +- Font embedding references when fonts aren't included +- Missing slideLayout declarations for layouts 12-25 +- docProps directory may not unpack - this is optional \ No newline at end of file diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file mode 100644 index 00000000..6454ef9a --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd @@ -0,0 +1,1499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file mode 100644 index 00000000..afa4f463 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file mode 100644 index 00000000..64e66b8a --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd @@ -0,0 +1,1085 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file mode 100644 index 00000000..687eea82 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd @@ -0,0 +1,11 @@ + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file mode 100644 index 00000000..6ac81b06 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd @@ -0,0 +1,3081 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file mode 100644 index 00000000..1dbf0514 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file mode 100644 index 00000000..f1af17db --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file mode 100644 index 00000000..0a185ab6 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd new file mode 100644 index 00000000..14ef4888 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd @@ -0,0 +1,1676 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file mode 100644 index 00000000..c20f3bf1 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file mode 100644 index 00000000..ac602522 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file mode 100644 index 00000000..424b8ba8 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file mode 100644 index 00000000..2bddce29 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file mode 100644 index 00000000..8a8c18ba --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file mode 100644 index 00000000..5c42706a --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file mode 100644 index 00000000..853c341c --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file mode 100644 index 00000000..da835ee8 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file mode 100644 index 00000000..87ad2658 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file mode 100644 index 00000000..9e86f1b2 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd new file mode 100644 index 00000000..d0be42e7 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd @@ -0,0 +1,4439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file mode 100644 index 00000000..8821dd18 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd @@ -0,0 +1,570 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file mode 100644 index 00000000..ca2575c7 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file mode 100644 index 00000000..dd079e60 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file mode 100644 index 00000000..3dd6cf62 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file mode 100644 index 00000000..f1041e34 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd new file mode 100644 index 00000000..9c5b7a63 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd @@ -0,0 +1,3646 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd new file mode 100644 index 00000000..0f13678d --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd @@ -0,0 +1,116 @@ + + + + + + See http://www.w3.org/XML/1998/namespace.html and + http://www.w3.org/TR/REC-xml for information about this namespace. + + This schema document describes the XML namespace, in a form + suitable for import by other schema documents. + + Note that local names in this namespace are intended to be defined + only by the World Wide Web Consortium or its subgroups. The + following names are currently defined in this namespace and should + not be used with conflicting semantics by any Working Group, + specification, or document instance: + + base (as an attribute name): denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification. + + lang (as an attribute name): denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification. + + space (as an attribute name): denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification. + + Father (in any context at all): denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: + + In appreciation for his vision, leadership and dedication + the W3C XML Plenary on this 10th day of February, 2000 + reserves for Jon Bosak in perpetuity the XML name + xml:Father + + + + + This schema defines attributes and an attribute group + suitable for use by + schemas wishing to allow xml:base, xml:lang or xml:space attributes + on elements they define. + + To enable this, such a schema must import this schema + for the XML namespace, e.g. as follows: + <schema . . .> + . . . + <import namespace="http://www.w3.org/XML/1998/namespace" + schemaLocation="http://www.w3.org/2001/03/xml.xsd"/> + + Subsequently, qualified reference to any of the attributes + or the group defined below will have the desired effect, e.g. + + <type . . .> + . . . + <attributeGroup ref="xml:specialAttrs"/> + + will define a type which will schema-validate an instance + element with any of those attributes + + + + In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + http://www.w3.org/2001/03/xml.xsd. + At the date of issue it can also be found at + http://www.w3.org/2001/xml.xsd. + The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML Schema + itself. In other words, if the XML Schema namespace changes, the version + of this document at + http://www.w3.org/2001/xml.xsd will change + accordingly; the version at + http://www.w3.org/2001/03/xml.xsd will not change. + + + + + + In due course, we should install the relevant ISO 2- and 3-letter + codes as the enumerated possible values . . . + + + + + + + + + + + + + + + See http://www.w3.org/TR/xmlbase/ for + information about this attribute. + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file mode 100644 index 00000000..a6de9d27 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd @@ -0,0 +1,42 @@ +๏ปฟ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file mode 100644 index 00000000..10e978b6 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd @@ -0,0 +1,50 @@ +๏ปฟ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd new file mode 100644 index 00000000..4248bf7a --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd new file mode 100644 index 00000000..56497467 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd @@ -0,0 +1,33 @@ +๏ปฟ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/mce/mc.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/mce/mc.xsd new file mode 100644 index 00000000..ef725457 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/mce/mc.xsd @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-2010.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-2010.xsd new file mode 100644 index 00000000..f65f7777 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-2010.xsd @@ -0,0 +1,560 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-2012.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-2012.xsd new file mode 100644 index 00000000..6b00755a --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-2012.xsd @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-2018.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-2018.xsd new file mode 100644 index 00000000..f321d333 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-2018.xsd @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-cex-2018.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-cex-2018.xsd new file mode 100644 index 00000000..364c6a9b --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-cex-2018.xsd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-cid-2016.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-cid-2016.xsd new file mode 100644 index 00000000..fed9d15b --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-cid-2016.xsd @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd new file mode 100644 index 00000000..680cf154 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-symex-2015.xsd b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-symex-2015.xsd new file mode 100644 index 00000000..89ada908 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/schemas/microsoft/wml-symex-2015.xsd @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/pack.py b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/pack.py new file mode 100755 index 00000000..68bc0886 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/pack.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +""" +Tool to pack a directory into a .docx, .pptx, or .xlsx file with XML formatting undone. + +Example usage: + python pack.py [--force] +""" + +import argparse +import shutil +import subprocess +import sys +import tempfile +import defusedxml.minidom +import zipfile +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser(description="Pack a directory into an Office file") + parser.add_argument("input_directory", help="Unpacked Office document directory") + parser.add_argument("output_file", help="Output Office file (.docx/.pptx/.xlsx)") + parser.add_argument("--force", action="store_true", help="Skip validation") + args = parser.parse_args() + + try: + success = pack_document( + args.input_directory, args.output_file, validate=not args.force + ) + + # Show warning if validation was skipped + if args.force: + print("Warning: Skipped validation, file may be corrupt", file=sys.stderr) + # Exit with error if validation failed + elif not success: + print("Contents would produce a corrupt file.", file=sys.stderr) + print("Please validate XML before repacking.", file=sys.stderr) + print("Use --force to skip validation and pack anyway.", file=sys.stderr) + sys.exit(1) + + except ValueError as e: + sys.exit(f"Error: {e}") + + +def pack_document(input_dir, output_file, validate=False): + """Pack a directory into an Office file (.docx/.pptx/.xlsx). + + Args: + input_dir: Path to unpacked Office document directory + output_file: Path to output Office file + validate: If True, validates with soffice (default: False) + + Returns: + bool: True if successful, False if validation failed + """ + input_dir = Path(input_dir) + output_file = Path(output_file) + + if not input_dir.is_dir(): + raise ValueError(f"{input_dir} is not a directory") + if output_file.suffix.lower() not in {".docx", ".pptx", ".xlsx"}: + raise ValueError(f"{output_file} must be a .docx, .pptx, or .xlsx file") + + # Work in temporary directory to avoid modifying original + with tempfile.TemporaryDirectory() as temp_dir: + temp_content_dir = Path(temp_dir) / "content" + shutil.copytree(input_dir, temp_content_dir) + + # Process XML files to remove pretty-printing whitespace + for pattern in ["*.xml", "*.rels"]: + for xml_file in temp_content_dir.rglob(pattern): + condense_xml(xml_file) + + # Create final Office file as zip archive + output_file.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf: + for f in temp_content_dir.rglob("*"): + if f.is_file(): + zf.write(f, f.relative_to(temp_content_dir)) + + # Validate if requested + if validate: + if not validate_document(output_file): + output_file.unlink() # Delete the corrupt file + return False + + return True + + +def validate_document(doc_path): + """Validate document by converting to HTML with soffice.""" + # Determine the correct filter based on file extension + match doc_path.suffix.lower(): + case ".docx": + filter_name = "html:HTML" + case ".pptx": + filter_name = "html:impress_html_Export" + case ".xlsx": + filter_name = "html:HTML (StarCalc)" + + with tempfile.TemporaryDirectory() as temp_dir: + try: + result = subprocess.run( + [ + "soffice", + "--headless", + "--convert-to", + filter_name, + "--outdir", + temp_dir, + str(doc_path), + ], + capture_output=True, + timeout=10, + text=True, + ) + if not (Path(temp_dir) / f"{doc_path.stem}.html").exists(): + error_msg = result.stderr.strip() or "Document validation failed" + print(f"Validation error: {error_msg}", file=sys.stderr) + return False + return True + except FileNotFoundError: + print("Warning: soffice not found. Skipping validation.", file=sys.stderr) + return True + except subprocess.TimeoutExpired: + print("Validation error: Timeout during conversion", file=sys.stderr) + return False + except Exception as e: + print(f"Validation error: {e}", file=sys.stderr) + return False + + +def condense_xml(xml_file): + """Strip unnecessary whitespace and remove comments.""" + with open(xml_file, "r", encoding="utf-8") as f: + dom = defusedxml.minidom.parse(f) + + # Process each element to remove whitespace and comments + for element in dom.getElementsByTagName("*"): + # Skip w:t elements and their processing + if element.tagName.endswith(":t"): + continue + + # Remove whitespace-only text nodes and comment nodes + for child in list(element.childNodes): + if ( + child.nodeType == child.TEXT_NODE + and child.nodeValue + and child.nodeValue.strip() == "" + ) or child.nodeType == child.COMMENT_NODE: + element.removeChild(child) + + # Write back the condensed XML + with open(xml_file, "wb") as f: + f.write(dom.toxml(encoding="UTF-8")) + + +if __name__ == "__main__": + main() diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/unpack.py b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/unpack.py new file mode 100755 index 00000000..96cb94ba --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/unpack.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Unpack and format XML contents of Office files (.docx, .pptx, .xlsx)""" + +import random +import sys +import zipfile +from pathlib import Path + + +def extract_archive_safely(input_file: str | Path, output_dir: str | Path): + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + with zipfile.ZipFile(input_file) as archive: + for member in archive.infolist(): + destination = output_path / member.filename + if not destination.resolve().is_relative_to(output_path.resolve()): + raise ValueError(f"Unsafe archive entry: {member.filename}") + + archive.extractall(output_path) + + +def pretty_print_xml(output_path: Path): + import defusedxml.minidom + + xml_files = list(output_path.rglob("*.xml")) + list(output_path.rglob("*.rels")) + for xml_file in xml_files: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + xml_file.write_bytes(dom.toprettyxml(indent=" ", encoding="ascii")) + + +def main(argv: list[str] | None = None): + argv = argv or sys.argv[1:] + if len(argv) != 2: + raise SystemExit("Usage: python unpack.py ") + + input_file, output_dir = argv + output_path = Path(output_dir) + extract_archive_safely(input_file, output_path) + pretty_print_xml(output_path) + + if input_file.endswith(".docx"): + suggested_rsid = "".join(random.choices("0123456789ABCDEF", k=8)) + print(f"Suggested RSID for edit session: {suggested_rsid}") + + +if __name__ == "__main__": + main() diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validate.py b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validate.py new file mode 100755 index 00000000..508c5891 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validate.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +Command line tool to validate Office document XML files against XSD schemas and tracked changes. + +Usage: + python validate.py --original +""" + +import argparse +import sys +from pathlib import Path + +from validation import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator + + +def main(): + parser = argparse.ArgumentParser(description="Validate Office document XML files") + parser.add_argument( + "unpacked_dir", + help="Path to unpacked Office document directory", + ) + parser.add_argument( + "--original", + required=True, + help="Path to original file (.docx/.pptx/.xlsx)", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Enable verbose output", + ) + args = parser.parse_args() + + # Validate paths + unpacked_dir = Path(args.unpacked_dir) + original_file = Path(args.original) + file_extension = original_file.suffix.lower() + assert unpacked_dir.is_dir(), f"Error: {unpacked_dir} is not a directory" + assert original_file.is_file(), f"Error: {original_file} is not a file" + assert file_extension in [".docx", ".pptx", ".xlsx"], ( + f"Error: {original_file} must be a .docx, .pptx, or .xlsx file" + ) + + # Run validations + match file_extension: + case ".docx": + validators = [DOCXSchemaValidator, RedliningValidator] + case ".pptx": + validators = [PPTXSchemaValidator] + case _: + print(f"Error: Validation not supported for file type {file_extension}") + sys.exit(1) + + # Run validators + success = True + for V in validators: + validator = V(unpacked_dir, original_file, verbose=args.verbose) + if not validator.validate(): + success = False + + if success: + print("All validations PASSED!") + + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validation/__init__.py b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validation/__init__.py new file mode 100644 index 00000000..db092ece --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validation/__init__.py @@ -0,0 +1,15 @@ +""" +Validation modules for Word document processing. +""" + +from .base import BaseSchemaValidator +from .docx import DOCXSchemaValidator +from .pptx import PPTXSchemaValidator +from .redlining import RedliningValidator + +__all__ = [ + "BaseSchemaValidator", + "DOCXSchemaValidator", + "PPTXSchemaValidator", + "RedliningValidator", +] diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validation/base.py b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validation/base.py new file mode 100644 index 00000000..0681b199 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validation/base.py @@ -0,0 +1,951 @@ +""" +Base validator with common validation logic for document files. +""" + +import re +from pathlib import Path + +import lxml.etree + + +class BaseSchemaValidator: + """Base validator with common validation logic for document files.""" + + # Elements whose 'id' attributes must be unique within their file + # Format: element_name -> (attribute_name, scope) + # scope can be 'file' (unique within file) or 'global' (unique across all files) + UNIQUE_ID_REQUIREMENTS = { + # Word elements + "comment": ("id", "file"), # Comment IDs in comments.xml + "commentrangestart": ("id", "file"), # Must match comment IDs + "commentrangeend": ("id", "file"), # Must match comment IDs + "bookmarkstart": ("id", "file"), # Bookmark start IDs + "bookmarkend": ("id", "file"), # Bookmark end IDs + # Note: ins and del (track changes) can share IDs when part of same revision + # PowerPoint elements + "sldid": ("id", "file"), # Slide IDs in presentation.xml + "sldmasterid": ("id", "global"), # Slide master IDs must be globally unique + "sldlayoutid": ("id", "global"), # Slide layout IDs must be globally unique + "cm": ("authorid", "file"), # Comment author IDs + # Excel elements + "sheet": ("sheetid", "file"), # Sheet IDs in workbook.xml + "definedname": ("id", "file"), # Named range IDs + # Drawing/Shape elements (all formats) + "cxnsp": ("id", "file"), # Connection shape IDs + "sp": ("id", "file"), # Shape IDs + "pic": ("id", "file"), # Picture IDs + "grpsp": ("id", "file"), # Group shape IDs + } + + # Mapping of element names to expected relationship types + # Subclasses should override this with format-specific mappings + ELEMENT_RELATIONSHIP_TYPES = {} + + # Unified schema mappings for all Office document types + SCHEMA_MAPPINGS = { + # Document type specific schemas + "word": "ISO-IEC29500-4_2016/wml.xsd", # Word documents + "ppt": "ISO-IEC29500-4_2016/pml.xsd", # PowerPoint presentations + "xl": "ISO-IEC29500-4_2016/sml.xsd", # Excel spreadsheets + # Common file types + "[Content_Types].xml": "ecma/fouth-edition/opc-contentTypes.xsd", + "app.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd", + "core.xml": "ecma/fouth-edition/opc-coreProperties.xsd", + "custom.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd", + ".rels": "ecma/fouth-edition/opc-relationships.xsd", + # Word-specific files + "people.xml": "microsoft/wml-2012.xsd", + "commentsIds.xml": "microsoft/wml-cid-2016.xsd", + "commentsExtensible.xml": "microsoft/wml-cex-2018.xsd", + "commentsExtended.xml": "microsoft/wml-2012.xsd", + # Chart files (common across document types) + "chart": "ISO-IEC29500-4_2016/dml-chart.xsd", + # Theme files (common across document types) + "theme": "ISO-IEC29500-4_2016/dml-main.xsd", + # Drawing and media files + "drawing": "ISO-IEC29500-4_2016/dml-main.xsd", + } + + # Unified namespace constants + MC_NAMESPACE = "http://schemas.openxmlformats.org/markup-compatibility/2006" + XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace" + + # Common OOXML namespaces used across validators + PACKAGE_RELATIONSHIPS_NAMESPACE = ( + "http://schemas.openxmlformats.org/package/2006/relationships" + ) + OFFICE_RELATIONSHIPS_NAMESPACE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + ) + CONTENT_TYPES_NAMESPACE = ( + "http://schemas.openxmlformats.org/package/2006/content-types" + ) + + # Folders where we should clean ignorable namespaces + MAIN_CONTENT_FOLDERS = {"word", "ppt", "xl"} + + # All allowed OOXML namespaces (superset of all document types) + OOXML_NAMESPACES = { + "http://schemas.openxmlformats.org/officeDocument/2006/math", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "http://schemas.openxmlformats.org/schemaLibrary/2006/main", + "http://schemas.openxmlformats.org/drawingml/2006/main", + "http://schemas.openxmlformats.org/drawingml/2006/chart", + "http://schemas.openxmlformats.org/drawingml/2006/chartDrawing", + "http://schemas.openxmlformats.org/drawingml/2006/diagram", + "http://schemas.openxmlformats.org/drawingml/2006/picture", + "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing", + "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "http://schemas.openxmlformats.org/presentationml/2006/main", + "http://schemas.openxmlformats.org/spreadsheetml/2006/main", + "http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes", + "http://www.w3.org/XML/1998/namespace", + } + + def __init__(self, unpacked_dir, original_file, verbose=False): + self.unpacked_dir = Path(unpacked_dir).resolve() + self.original_file = Path(original_file) + self.verbose = verbose + + # Set schemas directory + self.schemas_dir = Path(__file__).parent.parent.parent / "schemas" + + # Get all XML and .rels files + patterns = ["*.xml", "*.rels"] + self.xml_files = [ + f for pattern in patterns for f in self.unpacked_dir.rglob(pattern) + ] + + if not self.xml_files: + print(f"Warning: No XML files found in {self.unpacked_dir}") + + def validate(self): + """Run all validation checks and return True if all pass.""" + raise NotImplementedError("Subclasses must implement the validate method") + + def validate_xml(self): + """Validate that all XML files are well-formed.""" + errors = [] + + for xml_file in self.xml_files: + try: + # Try to parse the XML file + lxml.etree.parse(str(xml_file)) + except lxml.etree.XMLSyntaxError as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {e.lineno}: {e.msg}" + ) + except Exception as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Unexpected error: {str(e)}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} XML violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All XML files are well-formed") + return True + + def validate_namespaces(self): + """Validate that namespace prefixes in Ignorable attributes are declared.""" + errors = [] + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + declared = set(root.nsmap.keys()) - {None} # Exclude default namespace + + for attr_val in [ + v for k, v in root.attrib.items() if k.endswith("Ignorable") + ]: + undeclared = set(attr_val.split()) - declared + errors.extend( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Namespace '{ns}' in Ignorable but not declared" + for ns in undeclared + ) + except lxml.etree.XMLSyntaxError: + continue + + if errors: + print(f"FAILED - {len(errors)} namespace issues:") + for error in errors: + print(error) + return False + if self.verbose: + print("PASSED - All namespace prefixes properly declared") + return True + + def validate_unique_ids(self): + """Validate that specific IDs are unique according to OOXML requirements.""" + errors = [] + global_ids = {} # Track globally unique IDs across all files + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + file_ids = {} # Track IDs that must be unique within this file + + # Remove all mc:AlternateContent elements from the tree + mc_elements = root.xpath( + ".//mc:AlternateContent", namespaces={"mc": self.MC_NAMESPACE} + ) + for elem in mc_elements: + elem.getparent().remove(elem) + + # Now check IDs in the cleaned tree + for elem in root.iter(): + # Get the element name without namespace + tag = ( + elem.tag.split("}")[-1].lower() + if "}" in elem.tag + else elem.tag.lower() + ) + + # Check if this element type has ID uniqueness requirements + if tag in self.UNIQUE_ID_REQUIREMENTS: + attr_name, scope = self.UNIQUE_ID_REQUIREMENTS[tag] + + # Look for the specified attribute + id_value = None + for attr, value in elem.attrib.items(): + attr_local = ( + attr.split("}")[-1].lower() + if "}" in attr + else attr.lower() + ) + if attr_local == attr_name: + id_value = value + break + + if id_value is not None: + if scope == "global": + # Check global uniqueness + if id_value in global_ids: + prev_file, prev_line, prev_tag = global_ids[ + id_value + ] + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: Global ID '{id_value}' in <{tag}> " + f"already used in {prev_file} at line {prev_line} in <{prev_tag}>" + ) + else: + global_ids[id_value] = ( + xml_file.relative_to(self.unpacked_dir), + elem.sourceline, + tag, + ) + elif scope == "file": + # Check file-level uniqueness + key = (tag, attr_name) + if key not in file_ids: + file_ids[key] = {} + + if id_value in file_ids[key]: + prev_line = file_ids[key][id_value] + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: Duplicate {attr_name}='{id_value}' in <{tag}> " + f"(first occurrence at line {prev_line})" + ) + else: + file_ids[key][id_value] = elem.sourceline + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} ID uniqueness violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All required IDs are unique") + return True + + def validate_file_references(self): + """ + Validate that all .rels files properly reference files and that all files are referenced. + """ + errors = [] + + # Find all .rels files + rels_files = list(self.unpacked_dir.rglob("*.rels")) + + if not rels_files: + if self.verbose: + print("PASSED - No .rels files found") + return True + + # Get all files in the unpacked directory (excluding reference files) + all_files = [] + for file_path in self.unpacked_dir.rglob("*"): + if ( + file_path.is_file() + and file_path.name != "[Content_Types].xml" + and not file_path.name.endswith(".rels") + ): # This file is not referenced by .rels + all_files.append(file_path.resolve()) + + # Track all files that are referenced by any .rels file + all_referenced_files = set() + + if self.verbose: + print( + f"Found {len(rels_files)} .rels files and {len(all_files)} target files" + ) + + # Check each .rels file + for rels_file in rels_files: + try: + # Parse relationships file + rels_root = lxml.etree.parse(str(rels_file)).getroot() + + # Get the directory where this .rels file is located + rels_dir = rels_file.parent + + # Find all relationships and their targets + referenced_files = set() + broken_refs = [] + + for rel in rels_root.findall( + ".//ns:Relationship", + namespaces={"ns": self.PACKAGE_RELATIONSHIPS_NAMESPACE}, + ): + target = rel.get("Target") + if target and not target.startswith( + ("http", "mailto:") + ): # Skip external URLs + # Resolve the target path relative to the .rels file location + if rels_file.name == ".rels": + # Root .rels file - targets are relative to unpacked_dir + target_path = self.unpacked_dir / target + else: + # Other .rels files - targets are relative to their parent's parent + # e.g., word/_rels/document.xml.rels -> targets relative to word/ + base_dir = rels_dir.parent + target_path = base_dir / target + + # Normalize the path and check if it exists + try: + target_path = target_path.resolve() + if target_path.exists() and target_path.is_file(): + referenced_files.add(target_path) + all_referenced_files.add(target_path) + else: + broken_refs.append((target, rel.sourceline)) + except (OSError, ValueError): + broken_refs.append((target, rel.sourceline)) + + # Report broken references + if broken_refs: + rel_path = rels_file.relative_to(self.unpacked_dir) + for broken_ref, line_num in broken_refs: + errors.append( + f" {rel_path}: Line {line_num}: Broken reference to {broken_ref}" + ) + + except Exception as e: + rel_path = rels_file.relative_to(self.unpacked_dir) + errors.append(f" Error parsing {rel_path}: {e}") + + # Check for unreferenced files (files that exist but are not referenced anywhere) + unreferenced_files = set(all_files) - all_referenced_files + + if unreferenced_files: + for unref_file in sorted(unreferenced_files): + unref_rel_path = unref_file.relative_to(self.unpacked_dir) + errors.append(f" Unreferenced file: {unref_rel_path}") + + if errors: + print(f"FAILED - Found {len(errors)} relationship validation errors:") + for error in errors: + print(error) + print( + "CRITICAL: These errors will cause the document to appear corrupt. " + + "Broken references MUST be fixed, " + + "and unreferenced files MUST be referenced or removed." + ) + return False + else: + if self.verbose: + print( + "PASSED - All references are valid and all files are properly referenced" + ) + return True + + def validate_all_relationship_ids(self): + """ + Validate that all r:id attributes in XML files reference existing IDs + in their corresponding .rels files, and optionally validate relationship types. + """ + import lxml.etree + + errors = [] + + # Process each XML file that might contain r:id references + for xml_file in self.xml_files: + # Skip .rels files themselves + if xml_file.suffix == ".rels": + continue + + # Determine the corresponding .rels file + # For dir/file.xml, it's dir/_rels/file.xml.rels + rels_dir = xml_file.parent / "_rels" + rels_file = rels_dir / f"{xml_file.name}.rels" + + # Skip if there's no corresponding .rels file (that's okay) + if not rels_file.exists(): + continue + + try: + # Parse the .rels file to get valid relationship IDs and their types + rels_root = lxml.etree.parse(str(rels_file)).getroot() + rid_to_type = {} + + for rel in rels_root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rid = rel.get("Id") + rel_type = rel.get("Type", "") + if rid: + # Check for duplicate rIds + if rid in rid_to_type: + rels_rel_path = rels_file.relative_to(self.unpacked_dir) + errors.append( + f" {rels_rel_path}: Line {rel.sourceline}: " + f"Duplicate relationship ID '{rid}' (IDs must be unique)" + ) + # Extract just the type name from the full URL + type_name = ( + rel_type.split("/")[-1] if "/" in rel_type else rel_type + ) + rid_to_type[rid] = type_name + + # Parse the XML file to find all r:id references + xml_root = lxml.etree.parse(str(xml_file)).getroot() + + # Find all elements with r:id attributes + for elem in xml_root.iter(): + # Check for r:id attribute (relationship ID) + rid_attr = elem.get(f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id") + if rid_attr: + xml_rel_path = xml_file.relative_to(self.unpacked_dir) + elem_name = ( + elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag + ) + + # Check if the ID exists + if rid_attr not in rid_to_type: + errors.append( + f" {xml_rel_path}: Line {elem.sourceline}: " + f"<{elem_name}> references non-existent relationship '{rid_attr}' " + f"(valid IDs: {', '.join(sorted(rid_to_type.keys())[:5])}{'...' if len(rid_to_type) > 5 else ''})" + ) + # Check if we have type expectations for this element + elif self.ELEMENT_RELATIONSHIP_TYPES: + expected_type = self._get_expected_relationship_type( + elem_name + ) + if expected_type: + actual_type = rid_to_type[rid_attr] + # Check if the actual type matches or contains the expected type + if expected_type not in actual_type.lower(): + errors.append( + f" {xml_rel_path}: Line {elem.sourceline}: " + f"<{elem_name}> references '{rid_attr}' which points to '{actual_type}' " + f"but should point to a '{expected_type}' relationship" + ) + + except Exception as e: + xml_rel_path = xml_file.relative_to(self.unpacked_dir) + errors.append(f" Error processing {xml_rel_path}: {e}") + + if errors: + print(f"FAILED - Found {len(errors)} relationship ID reference errors:") + for error in errors: + print(error) + print("\nThese ID mismatches will cause the document to appear corrupt!") + return False + else: + if self.verbose: + print("PASSED - All relationship ID references are valid") + return True + + def _get_expected_relationship_type(self, element_name): + """ + Get the expected relationship type for an element. + First checks the explicit mapping, then tries pattern detection. + """ + # Normalize element name to lowercase + elem_lower = element_name.lower() + + # Check explicit mapping first + if elem_lower in self.ELEMENT_RELATIONSHIP_TYPES: + return self.ELEMENT_RELATIONSHIP_TYPES[elem_lower] + + # Try pattern detection for common patterns + # Pattern 1: Elements ending in "Id" often expect a relationship of the prefix type + if elem_lower.endswith("id") and len(elem_lower) > 2: + # e.g., "sldId" -> "sld", "sldMasterId" -> "sldMaster" + prefix = elem_lower[:-2] # Remove "id" + # Check if this might be a compound like "sldMasterId" + if prefix.endswith("master"): + return prefix.lower() + elif prefix.endswith("layout"): + return prefix.lower() + else: + # Simple case like "sldId" -> "slide" + # Common transformations + if prefix == "sld": + return "slide" + return prefix.lower() + + # Pattern 2: Elements ending in "Reference" expect a relationship of the prefix type + if elem_lower.endswith("reference") and len(elem_lower) > 9: + prefix = elem_lower[:-9] # Remove "reference" + return prefix.lower() + + return None + + def validate_content_types(self): + """Validate that all content files are properly declared in [Content_Types].xml.""" + errors = [] + + # Find [Content_Types].xml file + content_types_file = self.unpacked_dir / "[Content_Types].xml" + if not content_types_file.exists(): + print("FAILED - [Content_Types].xml file not found") + return False + + try: + # Parse and get all declared parts and extensions + root = lxml.etree.parse(str(content_types_file)).getroot() + declared_parts = set() + declared_extensions = set() + + # Get Override declarations (specific files) + for override in root.findall( + f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Override" + ): + part_name = override.get("PartName") + if part_name is not None: + declared_parts.add(part_name.lstrip("/")) + + # Get Default declarations (by extension) + for default in root.findall( + f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Default" + ): + extension = default.get("Extension") + if extension is not None: + declared_extensions.add(extension.lower()) + + # Root elements that require content type declaration + declarable_roots = { + "sld", + "sldLayout", + "sldMaster", + "presentation", # PowerPoint + "document", # Word + "workbook", + "worksheet", # Excel + "theme", # Common + } + + # Common media file extensions that should be declared + media_extensions = { + "png": "image/png", + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "gif": "image/gif", + "bmp": "image/bmp", + "tiff": "image/tiff", + "wmf": "image/x-wmf", + "emf": "image/x-emf", + } + + # Get all files in the unpacked directory + all_files = list(self.unpacked_dir.rglob("*")) + all_files = [f for f in all_files if f.is_file()] + + # Check all XML files for Override declarations + for xml_file in self.xml_files: + path_str = str(xml_file.relative_to(self.unpacked_dir)).replace( + "\\", "/" + ) + + # Skip non-content files + if any( + skip in path_str + for skip in [".rels", "[Content_Types]", "docProps/", "_rels/"] + ): + continue + + try: + root_tag = lxml.etree.parse(str(xml_file)).getroot().tag + root_name = root_tag.split("}")[-1] if "}" in root_tag else root_tag + + if root_name in declarable_roots and path_str not in declared_parts: + errors.append( + f" {path_str}: File with <{root_name}> root not declared in [Content_Types].xml" + ) + + except Exception: + continue # Skip unparseable files + + # Check all non-XML files for Default extension declarations + for file_path in all_files: + # Skip XML files and metadata files (already checked above) + if file_path.suffix.lower() in {".xml", ".rels"}: + continue + if file_path.name == "[Content_Types].xml": + continue + if "_rels" in file_path.parts or "docProps" in file_path.parts: + continue + + extension = file_path.suffix.lstrip(".").lower() + if extension and extension not in declared_extensions: + # Check if it's a known media extension that should be declared + if extension in media_extensions: + relative_path = file_path.relative_to(self.unpacked_dir) + errors.append( + f' {relative_path}: File with extension \'{extension}\' not declared in [Content_Types].xml - should add: ' + ) + + except Exception as e: + errors.append(f" Error parsing [Content_Types].xml: {e}") + + if errors: + print(f"FAILED - Found {len(errors)} content type declaration errors:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print( + "PASSED - All content files are properly declared in [Content_Types].xml" + ) + return True + + def validate_file_against_xsd(self, xml_file, verbose=False): + """Validate a single XML file against XSD schema, comparing with original. + + Args: + xml_file: Path to XML file to validate + verbose: Enable verbose output + + Returns: + tuple: (is_valid, new_errors_set) where is_valid is True/False/None (skipped) + """ + # Resolve both paths to handle symlinks + xml_file = Path(xml_file).resolve() + unpacked_dir = self.unpacked_dir.resolve() + + # Validate current file + is_valid, current_errors = self._validate_single_file_xsd( + xml_file, unpacked_dir + ) + + if is_valid is None: + return None, set() # Skipped + elif is_valid: + return True, set() # Valid, no errors + + # Get errors from original file for this specific file + original_errors = self._get_original_file_errors(xml_file) + + # Compare with original (both are guaranteed to be sets here) + assert current_errors is not None + new_errors = current_errors - original_errors + + if new_errors: + if verbose: + relative_path = xml_file.relative_to(unpacked_dir) + print(f"FAILED - {relative_path}: {len(new_errors)} new error(s)") + for error in list(new_errors)[:3]: + truncated = error[:250] + "..." if len(error) > 250 else error + print(f" - {truncated}") + return False, new_errors + else: + # All errors existed in original + if verbose: + print( + f"PASSED - No new errors (original had {len(current_errors)} errors)" + ) + return True, set() + + def validate_against_xsd(self): + """Validate XML files against XSD schemas, showing only new errors compared to original.""" + new_errors = [] + original_error_count = 0 + valid_count = 0 + skipped_count = 0 + + for xml_file in self.xml_files: + relative_path = str(xml_file.relative_to(self.unpacked_dir)) + is_valid, new_file_errors = self.validate_file_against_xsd( + xml_file, verbose=False + ) + + if is_valid is None: + skipped_count += 1 + continue + elif is_valid and not new_file_errors: + valid_count += 1 + continue + elif is_valid: + # Had errors but all existed in original + original_error_count += 1 + valid_count += 1 + continue + + # Has new errors + new_errors.append(f" {relative_path}: {len(new_file_errors)} new error(s)") + for error in list(new_file_errors)[:3]: # Show first 3 errors + new_errors.append( + f" - {error[:250]}..." if len(error) > 250 else f" - {error}" + ) + + # Print summary + if self.verbose: + print(f"Validated {len(self.xml_files)} files:") + print(f" - Valid: {valid_count}") + print(f" - Skipped (no schema): {skipped_count}") + if original_error_count: + print(f" - With original errors (ignored): {original_error_count}") + print( + f" - With NEW errors: {len(new_errors) > 0 and len([e for e in new_errors if not e.startswith(' ')]) or 0}" + ) + + if new_errors: + print("\nFAILED - Found NEW validation errors:") + for error in new_errors: + print(error) + return False + else: + if self.verbose: + print("\nPASSED - No new XSD validation errors introduced") + return True + + def _get_schema_path(self, xml_file): + """Determine the appropriate schema path for an XML file.""" + # Check exact filename match + if xml_file.name in self.SCHEMA_MAPPINGS: + return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.name] + + # Check .rels files + if xml_file.suffix == ".rels": + return self.schemas_dir / self.SCHEMA_MAPPINGS[".rels"] + + # Check chart files + if "charts/" in str(xml_file) and xml_file.name.startswith("chart"): + return self.schemas_dir / self.SCHEMA_MAPPINGS["chart"] + + # Check theme files + if "theme/" in str(xml_file) and xml_file.name.startswith("theme"): + return self.schemas_dir / self.SCHEMA_MAPPINGS["theme"] + + # Check if file is in a main content folder and use appropriate schema + if xml_file.parent.name in self.MAIN_CONTENT_FOLDERS: + return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.parent.name] + + return None + + def _clean_ignorable_namespaces(self, xml_doc): + """Remove attributes and elements not in allowed namespaces.""" + # Create a clean copy + xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") + xml_copy = lxml.etree.fromstring(xml_string) + + # Remove attributes not in allowed namespaces + for elem in xml_copy.iter(): + attrs_to_remove = [] + + for attr in elem.attrib: + # Check if attribute is from a namespace other than allowed ones + if "{" in attr: + ns = attr.split("}")[0][1:] + if ns not in self.OOXML_NAMESPACES: + attrs_to_remove.append(attr) + + # Remove collected attributes + for attr in attrs_to_remove: + del elem.attrib[attr] + + # Remove elements not in allowed namespaces + self._remove_ignorable_elements(xml_copy) + + return lxml.etree.ElementTree(xml_copy) + + def _remove_ignorable_elements(self, root): + """Recursively remove all elements not in allowed namespaces.""" + elements_to_remove = [] + + # Find elements to remove + for elem in list(root): + # Skip non-element nodes (comments, processing instructions, etc.) + if not hasattr(elem, "tag") or callable(elem.tag): + continue + + tag_str = str(elem.tag) + if tag_str.startswith("{"): + ns = tag_str.split("}")[0][1:] + if ns not in self.OOXML_NAMESPACES: + elements_to_remove.append(elem) + continue + + # Recursively clean child elements + self._remove_ignorable_elements(elem) + + # Remove collected elements + for elem in elements_to_remove: + root.remove(elem) + + def _preprocess_for_mc_ignorable(self, xml_doc): + """Preprocess XML to handle mc:Ignorable attribute properly.""" + # Remove mc:Ignorable attributes before validation + root = xml_doc.getroot() + + # Remove mc:Ignorable attribute from root + if f"{{{self.MC_NAMESPACE}}}Ignorable" in root.attrib: + del root.attrib[f"{{{self.MC_NAMESPACE}}}Ignorable"] + + return xml_doc + + def _validate_single_file_xsd(self, xml_file, base_path): + """Validate a single XML file against XSD schema. Returns (is_valid, errors_set).""" + schema_path = self._get_schema_path(xml_file) + if not schema_path: + return None, None # Skip file + + try: + # Load schema + with open(schema_path, "rb") as xsd_file: + parser = lxml.etree.XMLParser() + xsd_doc = lxml.etree.parse( + xsd_file, parser=parser, base_url=str(schema_path) + ) + schema = lxml.etree.XMLSchema(xsd_doc) + + # Load and preprocess XML + with open(xml_file, "r") as f: + xml_doc = lxml.etree.parse(f) + + xml_doc, _ = self._remove_template_tags_from_text_nodes(xml_doc) + xml_doc = self._preprocess_for_mc_ignorable(xml_doc) + + # Clean ignorable namespaces if needed + relative_path = xml_file.relative_to(base_path) + if ( + relative_path.parts + and relative_path.parts[0] in self.MAIN_CONTENT_FOLDERS + ): + xml_doc = self._clean_ignorable_namespaces(xml_doc) + + # Validate + if schema.validate(xml_doc): + return True, set() + else: + errors = set() + for error in schema.error_log: + # Store normalized error message (without line numbers for comparison) + errors.add(error.message) + return False, errors + + except Exception as e: + return False, {str(e)} + + def _get_original_file_errors(self, xml_file): + """Get XSD validation errors from a single file in the original document. + + Args: + xml_file: Path to the XML file in unpacked_dir to check + + Returns: + set: Set of error messages from the original file + """ + import tempfile + import zipfile + + # Resolve both paths to handle symlinks (e.g., /var vs /private/var on macOS) + xml_file = Path(xml_file).resolve() + unpacked_dir = self.unpacked_dir.resolve() + relative_path = xml_file.relative_to(unpacked_dir) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Extract original file + with zipfile.ZipFile(self.original_file, "r") as zip_ref: + zip_ref.extractall(temp_path) + + # Find corresponding file in original + original_xml_file = temp_path / relative_path + + if not original_xml_file.exists(): + # File didn't exist in original, so no original errors + return set() + + # Validate the specific file in original + is_valid, errors = self._validate_single_file_xsd( + original_xml_file, temp_path + ) + return errors if errors else set() + + def _remove_template_tags_from_text_nodes(self, xml_doc): + """Remove template tags from XML text nodes and collect warnings. + + Template tags follow the pattern {{ ... }} and are used as placeholders + for content replacement. They should be removed from text content before + XSD validation while preserving XML structure. + + Returns: + tuple: (cleaned_xml_doc, warnings_list) + """ + warnings = [] + template_pattern = re.compile(r"\{\{[^}]*\}\}") + + # Create a copy of the document to avoid modifying the original + xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") + xml_copy = lxml.etree.fromstring(xml_string) + + def process_text_content(text, content_type): + if not text: + return text + matches = list(template_pattern.finditer(text)) + if matches: + for match in matches: + warnings.append( + f"Found template tag in {content_type}: {match.group()}" + ) + return template_pattern.sub("", text) + return text + + # Process all text nodes in the document + for elem in xml_copy.iter(): + # Skip processing if this is a w:t element + if not hasattr(elem, "tag") or callable(elem.tag): + continue + tag_str = str(elem.tag) + if tag_str.endswith("}t") or tag_str == "t": + continue + + elem.text = process_text_content(elem.text, "text content") + elem.tail = process_text_content(elem.tail, "tail content") + + return lxml.etree.ElementTree(xml_copy), warnings + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validation/docx.py b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validation/docx.py new file mode 100644 index 00000000..602c4708 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validation/docx.py @@ -0,0 +1,274 @@ +""" +Validator for Word document XML files against XSD schemas. +""" + +import re +import tempfile +import zipfile + +import lxml.etree + +from .base import BaseSchemaValidator + + +class DOCXSchemaValidator(BaseSchemaValidator): + """Validator for Word document XML files against XSD schemas.""" + + # Word-specific namespace + WORD_2006_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + + # Word-specific element to relationship type mappings + # Start with empty mapping - add specific cases as we discover them + ELEMENT_RELATIONSHIP_TYPES = {} + + def validate(self): + """Run all validation checks and return True if all pass.""" + # Test 0: XML well-formedness + if not self.validate_xml(): + return False + + # Test 1: Namespace declarations + all_valid = True + if not self.validate_namespaces(): + all_valid = False + + # Test 2: Unique IDs + if not self.validate_unique_ids(): + all_valid = False + + # Test 3: Relationship and file reference validation + if not self.validate_file_references(): + all_valid = False + + # Test 4: Content type declarations + if not self.validate_content_types(): + all_valid = False + + # Test 5: XSD schema validation + if not self.validate_against_xsd(): + all_valid = False + + # Test 6: Whitespace preservation + if not self.validate_whitespace_preservation(): + all_valid = False + + # Test 7: Deletion validation + if not self.validate_deletions(): + all_valid = False + + # Test 8: Insertion validation + if not self.validate_insertions(): + all_valid = False + + # Test 9: Relationship ID reference validation + if not self.validate_all_relationship_ids(): + all_valid = False + + # Count and compare paragraphs + self.compare_paragraph_counts() + + return all_valid + + def validate_whitespace_preservation(self): + """ + Validate that w:t elements with whitespace have xml:space='preserve'. + """ + errors = [] + + for xml_file in self.xml_files: + # Only check document.xml files + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + # Find all w:t elements + for elem in root.iter(f"{{{self.WORD_2006_NAMESPACE}}}t"): + if elem.text: + text = elem.text + # Check if text starts or ends with whitespace + if re.match(r"^\s.*", text) or re.match(r".*\s$", text): + # Check if xml:space="preserve" attribute exists + xml_space_attr = f"{{{self.XML_NAMESPACE}}}space" + if ( + xml_space_attr not in elem.attrib + or elem.attrib[xml_space_attr] != "preserve" + ): + # Show a preview of the text + text_preview = ( + repr(text)[:50] + "..." + if len(repr(text)) > 50 + else repr(text) + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: w:t element with whitespace missing xml:space='preserve': {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} whitespace preservation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All whitespace is properly preserved") + return True + + def validate_deletions(self): + """ + Validate that w:t elements are not within w:del elements. + For some reason, XSD validation does not catch this, so we do it manually. + """ + errors = [] + + for xml_file in self.xml_files: + # Only check document.xml files + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + # Find all w:t elements that are descendants of w:del elements + namespaces = {"w": self.WORD_2006_NAMESPACE} + xpath_expression = ".//w:del//w:t" + problematic_t_elements = root.xpath( + xpath_expression, namespaces=namespaces + ) + for t_elem in problematic_t_elements: + if t_elem.text: + # Show a preview of the text + text_preview = ( + repr(t_elem.text)[:50] + "..." + if len(repr(t_elem.text)) > 50 + else repr(t_elem.text) + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {t_elem.sourceline}: found within : {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} deletion validation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - No w:t elements found within w:del elements") + return True + + def count_paragraphs_in_unpacked(self): + """Count the number of paragraphs in the unpacked document.""" + count = 0 + + for xml_file in self.xml_files: + # Only check document.xml files + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + # Count all w:p elements + paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") + count = len(paragraphs) + except Exception as e: + print(f"Error counting paragraphs in unpacked document: {e}") + + return count + + def count_paragraphs_in_original(self): + """Count the number of paragraphs in the original docx file.""" + count = 0 + + try: + # Create temporary directory to unpack original + with tempfile.TemporaryDirectory() as temp_dir: + # Unpack original docx + with zipfile.ZipFile(self.original_file, "r") as zip_ref: + zip_ref.extractall(temp_dir) + + # Parse document.xml + doc_xml_path = temp_dir + "/word/document.xml" + root = lxml.etree.parse(doc_xml_path).getroot() + + # Count all w:p elements + paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") + count = len(paragraphs) + + except Exception as e: + print(f"Error counting paragraphs in original document: {e}") + + return count + + def validate_insertions(self): + """ + Validate that w:delText elements are not within w:ins elements. + w:delText is only allowed in w:ins if nested within a w:del. + """ + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + # Find w:delText in w:ins that are NOT within w:del + invalid_elements = root.xpath( + ".//w:ins//w:delText[not(ancestor::w:del)]", + namespaces=namespaces + ) + + for elem in invalid_elements: + text_preview = ( + repr(elem.text or "")[:50] + "..." + if len(repr(elem.text or "")) > 50 + else repr(elem.text or "") + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: within : {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} insertion validation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - No w:delText elements within w:ins elements") + return True + + def compare_paragraph_counts(self): + """Compare paragraph counts between original and new document.""" + original_count = self.count_paragraphs_in_original() + new_count = self.count_paragraphs_in_unpacked() + + diff = new_count - original_count + diff_str = f"+{diff}" if diff > 0 else str(diff) + print(f"\nParagraphs: {original_count} โ†’ {new_count} ({diff_str})") + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validation/pptx.py b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validation/pptx.py new file mode 100644 index 00000000..66d5b1e2 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validation/pptx.py @@ -0,0 +1,315 @@ +""" +Validator for PowerPoint presentation XML files against XSD schemas. +""" + +import re + +from .base import BaseSchemaValidator + + +class PPTXSchemaValidator(BaseSchemaValidator): + """Validator for PowerPoint presentation XML files against XSD schemas.""" + + # PowerPoint presentation namespace + PRESENTATIONML_NAMESPACE = ( + "http://schemas.openxmlformats.org/presentationml/2006/main" + ) + + # PowerPoint-specific element to relationship type mappings + ELEMENT_RELATIONSHIP_TYPES = { + "sldid": "slide", + "sldmasterid": "slidemaster", + "notesmasterid": "notesmaster", + "sldlayoutid": "slidelayout", + "themeid": "theme", + "tablestyleid": "tablestyles", + } + + def validate(self): + """Run all validation checks and return True if all pass.""" + # Test 0: XML well-formedness + if not self.validate_xml(): + return False + + # Test 1: Namespace declarations + all_valid = True + if not self.validate_namespaces(): + all_valid = False + + # Test 2: Unique IDs + if not self.validate_unique_ids(): + all_valid = False + + # Test 3: UUID ID validation + if not self.validate_uuid_ids(): + all_valid = False + + # Test 4: Relationship and file reference validation + if not self.validate_file_references(): + all_valid = False + + # Test 5: Slide layout ID validation + if not self.validate_slide_layout_ids(): + all_valid = False + + # Test 6: Content type declarations + if not self.validate_content_types(): + all_valid = False + + # Test 7: XSD schema validation + if not self.validate_against_xsd(): + all_valid = False + + # Test 8: Notes slide reference validation + if not self.validate_notes_slide_references(): + all_valid = False + + # Test 9: Relationship ID reference validation + if not self.validate_all_relationship_ids(): + all_valid = False + + # Test 10: Duplicate slide layout references validation + if not self.validate_no_duplicate_slide_layouts(): + all_valid = False + + return all_valid + + def validate_uuid_ids(self): + """Validate that ID attributes that look like UUIDs contain only hex values.""" + import lxml.etree + + errors = [] + # UUID pattern: 8-4-4-4-12 hex digits with optional braces/hyphens + uuid_pattern = re.compile( + r"^[\{\(]?[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}[\}\)]?$" + ) + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + # Check all elements for ID attributes + for elem in root.iter(): + for attr, value in elem.attrib.items(): + # Check if this is an ID attribute + attr_name = attr.split("}")[-1].lower() + if attr_name == "id" or attr_name.endswith("id"): + # Check if value looks like a UUID (has the right length and pattern structure) + if self._looks_like_uuid(value): + # Validate that it contains only hex characters in the right positions + if not uuid_pattern.match(value): + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: ID '{value}' appears to be a UUID but contains invalid hex characters" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} UUID ID validation errors:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All UUID-like IDs contain valid hex values") + return True + + def _looks_like_uuid(self, value): + """Check if a value has the general structure of a UUID.""" + # Remove common UUID delimiters + clean_value = value.strip("{}()").replace("-", "") + # Check if it's 32 hex-like characters (could include invalid hex chars) + return len(clean_value) == 32 and all(c.isalnum() for c in clean_value) + + def validate_slide_layout_ids(self): + """Validate that sldLayoutId elements in slide masters reference valid slide layouts.""" + import lxml.etree + + errors = [] + + # Find all slide master files + slide_masters = list(self.unpacked_dir.glob("ppt/slideMasters/*.xml")) + + if not slide_masters: + if self.verbose: + print("PASSED - No slide masters found") + return True + + for slide_master in slide_masters: + try: + # Parse the slide master file + root = lxml.etree.parse(str(slide_master)).getroot() + + # Find the corresponding _rels file for this slide master + rels_file = slide_master.parent / "_rels" / f"{slide_master.name}.rels" + + if not rels_file.exists(): + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: " + f"Missing relationships file: {rels_file.relative_to(self.unpacked_dir)}" + ) + continue + + # Parse the relationships file + rels_root = lxml.etree.parse(str(rels_file)).getroot() + + # Build a set of valid relationship IDs that point to slide layouts + valid_layout_rids = set() + for rel in rels_root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rel_type = rel.get("Type", "") + if "slideLayout" in rel_type: + valid_layout_rids.add(rel.get("Id")) + + # Find all sldLayoutId elements in the slide master + for sld_layout_id in root.findall( + f".//{{{self.PRESENTATIONML_NAMESPACE}}}sldLayoutId" + ): + r_id = sld_layout_id.get( + f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id" + ) + layout_id = sld_layout_id.get("id") + + if r_id and r_id not in valid_layout_rids: + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: " + f"Line {sld_layout_id.sourceline}: sldLayoutId with id='{layout_id}' " + f"references r:id='{r_id}' which is not found in slide layout relationships" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} slide layout ID validation errors:") + for error in errors: + print(error) + print( + "Remove invalid references or add missing slide layouts to the relationships file." + ) + return False + else: + if self.verbose: + print("PASSED - All slide layout IDs reference valid slide layouts") + return True + + def validate_no_duplicate_slide_layouts(self): + """Validate that each slide has exactly one slideLayout reference.""" + import lxml.etree + + errors = [] + slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) + + for rels_file in slide_rels_files: + try: + root = lxml.etree.parse(str(rels_file)).getroot() + + # Find all slideLayout relationships + layout_rels = [ + rel + for rel in root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ) + if "slideLayout" in rel.get("Type", "") + ] + + if len(layout_rels) > 1: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: has {len(layout_rels)} slideLayout references" + ) + + except Exception as e: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print("FAILED - Found slides with duplicate slideLayout references:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All slides have exactly one slideLayout reference") + return True + + def validate_notes_slide_references(self): + """Validate that each notesSlide file is referenced by only one slide.""" + import lxml.etree + + errors = [] + notes_slide_references = {} # Track which slides reference each notesSlide + + # Find all slide relationship files + slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) + + if not slide_rels_files: + if self.verbose: + print("PASSED - No slide relationship files found") + return True + + for rels_file in slide_rels_files: + try: + # Parse the relationships file + root = lxml.etree.parse(str(rels_file)).getroot() + + # Find all notesSlide relationships + for rel in root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rel_type = rel.get("Type", "") + if "notesSlide" in rel_type: + target = rel.get("Target", "") + if target: + # Normalize the target path to handle relative paths + normalized_target = target.replace("../", "") + + # Track which slide references this notesSlide + slide_name = rels_file.stem.replace( + ".xml", "" + ) # e.g., "slide1" + + if normalized_target not in notes_slide_references: + notes_slide_references[normalized_target] = [] + notes_slide_references[normalized_target].append( + (slide_name, rels_file) + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + # Check for duplicate references + for target, references in notes_slide_references.items(): + if len(references) > 1: + slide_names = [ref[0] for ref in references] + errors.append( + f" Notes slide '{target}' is referenced by multiple slides: {', '.join(slide_names)}" + ) + for slide_name, rels_file in references: + errors.append(f" - {rels_file.relative_to(self.unpacked_dir)}") + + if errors: + print( + f"FAILED - Found {len([e for e in errors if not e.startswith(' ')])} notes slide reference validation errors:" + ) + for error in errors: + print(error) + print("Each slide may optionally have its own slide file.") + return False + else: + if self.verbose: + print("PASSED - All notes slide references are unique") + return True + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validation/redlining.py b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validation/redlining.py new file mode 100644 index 00000000..7ed425ed --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/ooxml/scripts/validation/redlining.py @@ -0,0 +1,279 @@ +""" +Validator for tracked changes in Word documents. +""" + +import subprocess +import tempfile +import zipfile +from pathlib import Path + + +class RedliningValidator: + """Validator for tracked changes in Word documents.""" + + def __init__(self, unpacked_dir, original_docx, verbose=False): + self.unpacked_dir = Path(unpacked_dir) + self.original_docx = Path(original_docx) + self.verbose = verbose + self.namespaces = { + "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + } + + def validate(self): + """Main validation method that returns True if valid, False otherwise.""" + # Verify unpacked directory exists and has correct structure + modified_file = self.unpacked_dir / "word" / "document.xml" + if not modified_file.exists(): + print(f"FAILED - Modified document.xml not found at {modified_file}") + return False + + # First, check if there are any tracked changes by Claude to validate + try: + import xml.etree.ElementTree as ET + + tree = ET.parse(modified_file) + root = tree.getroot() + + # Check for w:del or w:ins tags authored by Claude + del_elements = root.findall(".//w:del", self.namespaces) + ins_elements = root.findall(".//w:ins", self.namespaces) + + # Filter to only include changes by Claude + claude_del_elements = [ + elem + for elem in del_elements + if elem.get(f"{{{self.namespaces['w']}}}author") == "Claude" + ] + claude_ins_elements = [ + elem + for elem in ins_elements + if elem.get(f"{{{self.namespaces['w']}}}author") == "Claude" + ] + + # Redlining validation is only needed if tracked changes by Claude have been used. + if not claude_del_elements and not claude_ins_elements: + if self.verbose: + print("PASSED - No tracked changes by Claude found.") + return True + + except Exception: + # If we can't parse the XML, continue with full validation + pass + + # Create temporary directory for unpacking original docx + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Unpack original docx + try: + with zipfile.ZipFile(self.original_docx, "r") as zip_ref: + zip_ref.extractall(temp_path) + except Exception as e: + print(f"FAILED - Error unpacking original docx: {e}") + return False + + original_file = temp_path / "word" / "document.xml" + if not original_file.exists(): + print( + f"FAILED - Original document.xml not found in {self.original_docx}" + ) + return False + + # Parse both XML files using xml.etree.ElementTree for redlining validation + try: + import xml.etree.ElementTree as ET + + modified_tree = ET.parse(modified_file) + modified_root = modified_tree.getroot() + original_tree = ET.parse(original_file) + original_root = original_tree.getroot() + except ET.ParseError as e: + print(f"FAILED - Error parsing XML files: {e}") + return False + + # Remove Claude's tracked changes from both documents + self._remove_claude_tracked_changes(original_root) + self._remove_claude_tracked_changes(modified_root) + + # Extract and compare text content + modified_text = self._extract_text_content(modified_root) + original_text = self._extract_text_content(original_root) + + if modified_text != original_text: + # Show detailed character-level differences for each paragraph + error_message = self._generate_detailed_diff( + original_text, modified_text + ) + print(error_message) + return False + + if self.verbose: + print("PASSED - All changes by Claude are properly tracked") + return True + + def _generate_detailed_diff(self, original_text, modified_text): + """Generate detailed word-level differences using git word diff.""" + error_parts = [ + "FAILED - Document text doesn't match after removing Claude's tracked changes", + "", + "Likely causes:", + " 1. Modified text inside another author's or tags", + " 2. Made edits without proper tracked changes", + " 3. Didn't nest inside when deleting another's insertion", + "", + "For pre-redlined documents, use correct patterns:", + " - To reject another's INSERTION: Nest inside their ", + " - To restore another's DELETION: Add new AFTER their ", + "", + ] + + # Show git word diff + git_diff = self._get_git_word_diff(original_text, modified_text) + if git_diff: + error_parts.extend(["Differences:", "============", git_diff]) + else: + error_parts.append("Unable to generate word diff (git not available)") + + return "\n".join(error_parts) + + def _get_git_word_diff(self, original_text, modified_text): + """Generate word diff using git with character-level precision.""" + try: + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create two files + original_file = temp_path / "original.txt" + modified_file = temp_path / "modified.txt" + + original_file.write_text(original_text, encoding="utf-8") + modified_file.write_text(modified_text, encoding="utf-8") + + # Try character-level diff first for precise differences + result = subprocess.run( + [ + "git", + "diff", + "--word-diff=plain", + "--word-diff-regex=.", # Character-by-character diff + "-U0", # Zero lines of context - show only changed lines + "--no-index", + str(original_file), + str(modified_file), + ], + capture_output=True, + text=True, + ) + + if result.stdout.strip(): + # Clean up the output - remove git diff header lines + lines = result.stdout.split("\n") + # Skip the header lines (diff --git, index, +++, ---, @@) + content_lines = [] + in_content = False + for line in lines: + if line.startswith("@@"): + in_content = True + continue + if in_content and line.strip(): + content_lines.append(line) + + if content_lines: + return "\n".join(content_lines) + + # Fallback to word-level diff if character-level is too verbose + result = subprocess.run( + [ + "git", + "diff", + "--word-diff=plain", + "-U0", # Zero lines of context + "--no-index", + str(original_file), + str(modified_file), + ], + capture_output=True, + text=True, + ) + + if result.stdout.strip(): + lines = result.stdout.split("\n") + content_lines = [] + in_content = False + for line in lines: + if line.startswith("@@"): + in_content = True + continue + if in_content and line.strip(): + content_lines.append(line) + return "\n".join(content_lines) + + except (subprocess.CalledProcessError, FileNotFoundError, Exception): + # Git not available or other error, return None to use fallback + pass + + return None + + def _remove_claude_tracked_changes(self, root): + """Remove tracked changes authored by Claude from the XML root.""" + ins_tag = f"{{{self.namespaces['w']}}}ins" + del_tag = f"{{{self.namespaces['w']}}}del" + author_attr = f"{{{self.namespaces['w']}}}author" + + # Remove w:ins elements + for parent in root.iter(): + to_remove = [] + for child in parent: + if child.tag == ins_tag and child.get(author_attr) == "Claude": + to_remove.append(child) + for elem in to_remove: + parent.remove(elem) + + # Unwrap content in w:del elements where author is "Claude" + deltext_tag = f"{{{self.namespaces['w']}}}delText" + t_tag = f"{{{self.namespaces['w']}}}t" + + for parent in root.iter(): + to_process = [] + for child in parent: + if child.tag == del_tag and child.get(author_attr) == "Claude": + to_process.append((child, list(parent).index(child))) + + # Process in reverse order to maintain indices + for del_elem, del_index in reversed(to_process): + # Convert w:delText to w:t before moving + for elem in del_elem.iter(): + if elem.tag == deltext_tag: + elem.tag = t_tag + + # Move all children of w:del to its parent before removing w:del + for child in reversed(list(del_elem)): + parent.insert(del_index, child) + parent.remove(del_elem) + + def _extract_text_content(self, root): + """Extract text content from Word XML, preserving paragraph structure. + + Empty paragraphs are skipped to avoid false positives when tracked + insertions add only structural elements without text content. + """ + p_tag = f"{{{self.namespaces['w']}}}p" + t_tag = f"{{{self.namespaces['w']}}}t" + + paragraphs = [] + for p_elem in root.findall(f".//{p_tag}"): + # Get all text elements within this paragraph + text_parts = [] + for t_elem in p_elem.findall(f".//{t_tag}"): + if t_elem.text: + text_parts.append(t_elem.text) + paragraph_text = "".join(text_parts) + # Skip empty paragraphs - they don't affect content validation + if paragraph_text: + paragraphs.append(paragraph_text) + + return "\n".join(paragraphs) + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/html2pptx.js b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/html2pptx.js new file mode 100755 index 00000000..437bf7c5 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/html2pptx.js @@ -0,0 +1,979 @@ +/** + * html2pptx - Convert HTML slide to pptxgenjs slide with positioned elements + * + * USAGE: + * const pptx = new pptxgen(); + * pptx.layout = 'LAYOUT_16x9'; // Must match HTML body dimensions + * + * const { slide, placeholders } = await html2pptx('slide.html', pptx); + * slide.addChart(pptx.charts.LINE, data, placeholders[0]); + * + * await pptx.writeFile('output.pptx'); + * + * FEATURES: + * - Converts HTML to PowerPoint with accurate positioning + * - Supports text, images, shapes, and bullet lists + * - Extracts placeholder elements (class="placeholder") with positions + * - Handles CSS gradients, borders, and margins + * + * VALIDATION: + * - Uses body width/height from HTML for viewport sizing + * - Throws error if HTML dimensions don't match presentation layout + * - Throws error if content overflows body (with overflow details) + * + * RETURNS: + * { slide, placeholders } where placeholders is an array of { id, x, y, w, h } + */ + +const { chromium } = require('playwright'); +const path = require('path'); +const sharp = require('sharp'); + +const PT_PER_PX = 0.75; +const PX_PER_IN = 96; +const EMU_PER_IN = 914400; + +// Helper: Get body dimensions and check for overflow +async function getBodyDimensions(page) { + const bodyDimensions = await page.evaluate(() => { + const body = document.body; + const style = window.getComputedStyle(body); + + return { + width: parseFloat(style.width), + height: parseFloat(style.height), + scrollWidth: body.scrollWidth, + scrollHeight: body.scrollHeight + }; + }); + + const errors = []; + const widthOverflowPx = Math.max(0, bodyDimensions.scrollWidth - bodyDimensions.width - 1); + const heightOverflowPx = Math.max(0, bodyDimensions.scrollHeight - bodyDimensions.height - 1); + + const widthOverflowPt = widthOverflowPx * PT_PER_PX; + const heightOverflowPt = heightOverflowPx * PT_PER_PX; + + if (widthOverflowPt > 0 || heightOverflowPt > 0) { + const directions = []; + if (widthOverflowPt > 0) directions.push(`${widthOverflowPt.toFixed(1)}pt horizontally`); + if (heightOverflowPt > 0) directions.push(`${heightOverflowPt.toFixed(1)}pt vertically`); + const reminder = heightOverflowPt > 0 ? ' (Remember: leave 0.5" margin at bottom of slide)' : ''; + errors.push(`HTML content overflows body by ${directions.join(' and ')}${reminder}`); + } + + return { ...bodyDimensions, errors }; +} + +// Helper: Validate dimensions match presentation layout +function validateDimensions(bodyDimensions, pres) { + const errors = []; + const widthInches = bodyDimensions.width / PX_PER_IN; + const heightInches = bodyDimensions.height / PX_PER_IN; + + if (pres.presLayout) { + const layoutWidth = pres.presLayout.width / EMU_PER_IN; + const layoutHeight = pres.presLayout.height / EMU_PER_IN; + + if (Math.abs(layoutWidth - widthInches) > 0.1 || Math.abs(layoutHeight - heightInches) > 0.1) { + errors.push( + `HTML dimensions (${widthInches.toFixed(1)}" ร— ${heightInches.toFixed(1)}") ` + + `don't match presentation layout (${layoutWidth.toFixed(1)}" ร— ${layoutHeight.toFixed(1)}")` + ); + } + } + return errors; +} + +function validateTextBoxPosition(slideData, bodyDimensions) { + const errors = []; + const slideHeightInches = bodyDimensions.height / PX_PER_IN; + const minBottomMargin = 0.5; // 0.5 inches from bottom + + for (const el of slideData.elements) { + // Check text elements (p, h1-h6, list) + if (['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'list'].includes(el.type)) { + const fontSize = el.style?.fontSize || 0; + const bottomEdge = el.position.y + el.position.h; + const distanceFromBottom = slideHeightInches - bottomEdge; + + if (fontSize > 12 && distanceFromBottom < minBottomMargin) { + const getText = () => { + if (typeof el.text === 'string') return el.text; + if (Array.isArray(el.text)) return el.text.find(t => t.text)?.text || ''; + if (Array.isArray(el.items)) return el.items.find(item => item.text)?.text || ''; + return ''; + }; + const textPrefix = getText().substring(0, 50) + (getText().length > 50 ? '...' : ''); + + errors.push( + `Text box "${textPrefix}" ends too close to bottom edge ` + + `(${distanceFromBottom.toFixed(2)}" from bottom, minimum ${minBottomMargin}" required)` + ); + } + } + } + + return errors; +} + +// Helper: Add background to slide +async function addBackground(slideData, targetSlide, tmpDir) { + if (slideData.background.type === 'image' && slideData.background.path) { + let imagePath = slideData.background.path.startsWith('file://') + ? slideData.background.path.replace('file://', '') + : slideData.background.path; + targetSlide.background = { path: imagePath }; + } else if (slideData.background.type === 'color' && slideData.background.value) { + targetSlide.background = { color: slideData.background.value }; + } +} + +// Helper: Add elements to slide +function addElements(slideData, targetSlide, pres) { + for (const el of slideData.elements) { + if (el.type === 'image') { + let imagePath = el.src.startsWith('file://') ? el.src.replace('file://', '') : el.src; + targetSlide.addImage({ + path: imagePath, + x: el.position.x, + y: el.position.y, + w: el.position.w, + h: el.position.h + }); + } else if (el.type === 'line') { + targetSlide.addShape(pres.ShapeType.line, { + x: el.x1, + y: el.y1, + w: el.x2 - el.x1, + h: el.y2 - el.y1, + line: { color: el.color, width: el.width } + }); + } else if (el.type === 'shape') { + const shapeOptions = { + x: el.position.x, + y: el.position.y, + w: el.position.w, + h: el.position.h, + shape: el.shape.rectRadius > 0 ? pres.ShapeType.roundRect : pres.ShapeType.rect + }; + + if (el.shape.fill) { + shapeOptions.fill = { color: el.shape.fill }; + if (el.shape.transparency != null) shapeOptions.fill.transparency = el.shape.transparency; + } + if (el.shape.line) shapeOptions.line = el.shape.line; + if (el.shape.rectRadius > 0) shapeOptions.rectRadius = el.shape.rectRadius; + if (el.shape.shadow) shapeOptions.shadow = el.shape.shadow; + + targetSlide.addText(el.text || '', shapeOptions); + } else if (el.type === 'list') { + const listOptions = { + x: el.position.x, + y: el.position.y, + w: el.position.w, + h: el.position.h, + fontSize: el.style.fontSize, + fontFace: el.style.fontFace, + color: el.style.color, + align: el.style.align, + valign: 'top', + lineSpacing: el.style.lineSpacing, + paraSpaceBefore: el.style.paraSpaceBefore, + paraSpaceAfter: el.style.paraSpaceAfter, + margin: el.style.margin + }; + if (el.style.margin) listOptions.margin = el.style.margin; + targetSlide.addText(el.items, listOptions); + } else { + // Check if text is single-line (height suggests one line) + const lineHeight = el.style.lineSpacing || el.style.fontSize * 1.2; + const isSingleLine = el.position.h <= lineHeight * 1.5; + + let adjustedX = el.position.x; + let adjustedW = el.position.w; + + // Make single-line text 2% wider to account for underestimate + if (isSingleLine) { + const widthIncrease = el.position.w * 0.02; + const align = el.style.align; + + if (align === 'center') { + // Center: expand both sides + adjustedX = el.position.x - (widthIncrease / 2); + adjustedW = el.position.w + widthIncrease; + } else if (align === 'right') { + // Right: expand to the left + adjustedX = el.position.x - widthIncrease; + adjustedW = el.position.w + widthIncrease; + } else { + // Left (default): expand to the right + adjustedW = el.position.w + widthIncrease; + } + } + + const textOptions = { + x: adjustedX, + y: el.position.y, + w: adjustedW, + h: el.position.h, + fontSize: el.style.fontSize, + fontFace: el.style.fontFace, + color: el.style.color, + bold: el.style.bold, + italic: el.style.italic, + underline: el.style.underline, + valign: 'top', + lineSpacing: el.style.lineSpacing, + paraSpaceBefore: el.style.paraSpaceBefore, + paraSpaceAfter: el.style.paraSpaceAfter, + inset: 0 // Remove default PowerPoint internal padding + }; + + if (el.style.align) textOptions.align = el.style.align; + if (el.style.margin) textOptions.margin = el.style.margin; + if (el.style.rotate !== undefined) textOptions.rotate = el.style.rotate; + if (el.style.transparency !== null && el.style.transparency !== undefined) textOptions.transparency = el.style.transparency; + + targetSlide.addText(el.text, textOptions); + } + } +} + +// Helper: Extract slide data from HTML page +async function extractSlideData(page) { + return await page.evaluate(() => { + const PT_PER_PX = 0.75; + const PX_PER_IN = 96; + + // Fonts that are single-weight and should not have bold applied + // (applying bold causes PowerPoint to use faux bold which makes text wider) + const SINGLE_WEIGHT_FONTS = ['impact']; + + // Helper: Check if a font should skip bold formatting + const shouldSkipBold = (fontFamily) => { + if (!fontFamily) return false; + const normalizedFont = fontFamily.toLowerCase().replace(/['"]/g, '').split(',')[0].trim(); + return SINGLE_WEIGHT_FONTS.includes(normalizedFont); + }; + + // Unit conversion helpers + const pxToInch = (px) => px / PX_PER_IN; + const pxToPoints = (pxStr) => parseFloat(pxStr) * PT_PER_PX; + const rgbToHex = (rgbStr) => { + // Handle transparent backgrounds by defaulting to white + if (rgbStr === 'rgba(0, 0, 0, 0)' || rgbStr === 'transparent') return 'FFFFFF'; + + const match = rgbStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (!match) return 'FFFFFF'; + return match.slice(1).map(n => parseInt(n).toString(16).padStart(2, '0')).join(''); + }; + + const extractAlpha = (rgbStr) => { + const match = rgbStr.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/); + if (!match || !match[4]) return null; + const alpha = parseFloat(match[4]); + return Math.round((1 - alpha) * 100); + }; + + const applyTextTransform = (text, textTransform) => { + if (textTransform === 'uppercase') return text.toUpperCase(); + if (textTransform === 'lowercase') return text.toLowerCase(); + if (textTransform === 'capitalize') { + return text.replace(/\b\w/g, c => c.toUpperCase()); + } + return text; + }; + + // Extract rotation angle from CSS transform and writing-mode + const getRotation = (transform, writingMode) => { + let angle = 0; + + // Handle writing-mode first + // PowerPoint: 90ยฐ = text rotated 90ยฐ clockwise (reads top to bottom, letters upright) + // PowerPoint: 270ยฐ = text rotated 270ยฐ clockwise (reads bottom to top, letters upright) + if (writingMode === 'vertical-rl') { + // vertical-rl alone = text reads top to bottom = 90ยฐ in PowerPoint + angle = 90; + } else if (writingMode === 'vertical-lr') { + // vertical-lr alone = text reads bottom to top = 270ยฐ in PowerPoint + angle = 270; + } + + // Then add any transform rotation + if (transform && transform !== 'none') { + // Try to match rotate() function + const rotateMatch = transform.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/); + if (rotateMatch) { + angle += parseFloat(rotateMatch[1]); + } else { + // Browser may compute as matrix - extract rotation from matrix + const matrixMatch = transform.match(/matrix\(([^)]+)\)/); + if (matrixMatch) { + const values = matrixMatch[1].split(',').map(parseFloat); + // matrix(a, b, c, d, e, f) where rotation = atan2(b, a) + const matrixAngle = Math.atan2(values[1], values[0]) * (180 / Math.PI); + angle += Math.round(matrixAngle); + } + } + } + + // Normalize to 0-359 range + angle = angle % 360; + if (angle < 0) angle += 360; + + return angle === 0 ? null : angle; + }; + + // Get position/dimensions accounting for rotation + const getPositionAndSize = (el, rect, rotation) => { + if (rotation === null) { + return { x: rect.left, y: rect.top, w: rect.width, h: rect.height }; + } + + // For 90ยฐ or 270ยฐ rotations, swap width and height + // because PowerPoint applies rotation to the original (unrotated) box + const isVertical = rotation === 90 || rotation === 270; + + if (isVertical) { + // The browser shows us the rotated dimensions (tall box for vertical text) + // But PowerPoint needs the pre-rotation dimensions (wide box that will be rotated) + // So we swap: browser's height becomes PPT's width, browser's width becomes PPT's height + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + return { + x: centerX - rect.height / 2, + y: centerY - rect.width / 2, + w: rect.height, + h: rect.width + }; + } + + // For other rotations, use element's offset dimensions + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + return { + x: centerX - el.offsetWidth / 2, + y: centerY - el.offsetHeight / 2, + w: el.offsetWidth, + h: el.offsetHeight + }; + }; + + // Parse CSS box-shadow into PptxGenJS shadow properties + const parseBoxShadow = (boxShadow) => { + if (!boxShadow || boxShadow === 'none') return null; + + // Browser computed style format: "rgba(0, 0, 0, 0.3) 2px 2px 8px 0px [inset]" + // CSS format: "[inset] 2px 2px 8px 0px rgba(0, 0, 0, 0.3)" + + const insetMatch = boxShadow.match(/inset/); + + // IMPORTANT: PptxGenJS/PowerPoint doesn't properly support inset shadows + // Only process outer shadows to avoid file corruption + if (insetMatch) return null; + + // Extract color first (rgba or rgb at start) + const colorMatch = boxShadow.match(/rgba?\([^)]+\)/); + + // Extract numeric values (handles both px and pt units) + const parts = boxShadow.match(/([-\d.]+)(px|pt)/g); + + if (!parts || parts.length < 2) return null; + + const offsetX = parseFloat(parts[0]); + const offsetY = parseFloat(parts[1]); + const blur = parts.length > 2 ? parseFloat(parts[2]) : 0; + + // Calculate angle from offsets (in degrees, 0 = right, 90 = down) + let angle = 0; + if (offsetX !== 0 || offsetY !== 0) { + angle = Math.atan2(offsetY, offsetX) * (180 / Math.PI); + if (angle < 0) angle += 360; + } + + // Calculate offset distance (hypotenuse) + const offset = Math.sqrt(offsetX * offsetX + offsetY * offsetY) * PT_PER_PX; + + // Extract opacity from rgba + let opacity = 0.5; + if (colorMatch) { + const opacityMatch = colorMatch[0].match(/[\d.]+\)$/); + if (opacityMatch) { + opacity = parseFloat(opacityMatch[0].replace(')', '')); + } + } + + return { + type: 'outer', + angle: Math.round(angle), + blur: blur * 0.75, // Convert to points + color: colorMatch ? rgbToHex(colorMatch[0]) : '000000', + offset: offset, + opacity + }; + }; + + // Parse inline formatting tags (, , , , , ) into text runs + const parseInlineFormatting = (element, baseOptions = {}, runs = [], baseTextTransform = (x) => x) => { + let prevNodeIsText = false; + + element.childNodes.forEach((node) => { + let textTransform = baseTextTransform; + + const isText = node.nodeType === Node.TEXT_NODE || node.tagName === 'BR'; + if (isText) { + const text = node.tagName === 'BR' ? '\n' : textTransform(node.textContent.replace(/\s+/g, ' ')); + const prevRun = runs[runs.length - 1]; + if (prevNodeIsText && prevRun) { + prevRun.text += text; + } else { + runs.push({ text, options: { ...baseOptions } }); + } + + } else if (node.nodeType === Node.ELEMENT_NODE && node.textContent.trim()) { + const options = { ...baseOptions }; + const computed = window.getComputedStyle(node); + + // Handle inline elements with computed styles + if (node.tagName === 'SPAN' || node.tagName === 'B' || node.tagName === 'STRONG' || node.tagName === 'I' || node.tagName === 'EM' || node.tagName === 'U') { + const isBold = computed.fontWeight === 'bold' || parseInt(computed.fontWeight) >= 600; + if (isBold && !shouldSkipBold(computed.fontFamily)) options.bold = true; + if (computed.fontStyle === 'italic') options.italic = true; + if (computed.textDecoration && computed.textDecoration.includes('underline')) options.underline = true; + if (computed.color && computed.color !== 'rgb(0, 0, 0)') { + options.color = rgbToHex(computed.color); + const transparency = extractAlpha(computed.color); + if (transparency !== null) options.transparency = transparency; + } + if (computed.fontSize) options.fontSize = pxToPoints(computed.fontSize); + + // Apply text-transform on the span element itself + if (computed.textTransform && computed.textTransform !== 'none') { + const transformStr = computed.textTransform; + textTransform = (text) => applyTextTransform(text, transformStr); + } + + // Validate: Check for margins on inline elements + if (computed.marginLeft && parseFloat(computed.marginLeft) > 0) { + errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-left which is not supported in PowerPoint. Remove margin from inline elements.`); + } + if (computed.marginRight && parseFloat(computed.marginRight) > 0) { + errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-right which is not supported in PowerPoint. Remove margin from inline elements.`); + } + if (computed.marginTop && parseFloat(computed.marginTop) > 0) { + errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-top which is not supported in PowerPoint. Remove margin from inline elements.`); + } + if (computed.marginBottom && parseFloat(computed.marginBottom) > 0) { + errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-bottom which is not supported in PowerPoint. Remove margin from inline elements.`); + } + + // Recursively process the child node. This will flatten nested spans into multiple runs. + parseInlineFormatting(node, options, runs, textTransform); + } + } + + prevNodeIsText = isText; + }); + + // Trim leading space from first run and trailing space from last run + if (runs.length > 0) { + runs[0].text = runs[0].text.replace(/^\s+/, ''); + runs[runs.length - 1].text = runs[runs.length - 1].text.replace(/\s+$/, ''); + } + + return runs.filter(r => r.text.length > 0); + }; + + // Extract background from body (image or color) + const body = document.body; + const bodyStyle = window.getComputedStyle(body); + const bgImage = bodyStyle.backgroundImage; + const bgColor = bodyStyle.backgroundColor; + + // Collect validation errors + const errors = []; + + // Validate: Check for CSS gradients + if (bgImage && (bgImage.includes('linear-gradient') || bgImage.includes('radial-gradient'))) { + errors.push( + 'CSS gradients are not supported. Use Sharp to rasterize gradients as PNG images first, ' + + 'then reference with background-image: url(\'gradient.png\')' + ); + } + + let background; + if (bgImage && bgImage !== 'none') { + // Extract URL from url("...") or url(...) + const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/); + if (urlMatch) { + background = { + type: 'image', + path: urlMatch[1] + }; + } else { + background = { + type: 'color', + value: rgbToHex(bgColor) + }; + } + } else { + background = { + type: 'color', + value: rgbToHex(bgColor) + }; + } + + // Process all elements + const elements = []; + const placeholders = []; + const textTags = ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI']; + const processed = new Set(); + + document.querySelectorAll('*').forEach((el) => { + if (processed.has(el)) return; + + // Validate text elements don't have backgrounds, borders, or shadows + if (textTags.includes(el.tagName)) { + const computed = window.getComputedStyle(el); + const hasBg = computed.backgroundColor && computed.backgroundColor !== 'rgba(0, 0, 0, 0)'; + const hasBorder = (computed.borderWidth && parseFloat(computed.borderWidth) > 0) || + (computed.borderTopWidth && parseFloat(computed.borderTopWidth) > 0) || + (computed.borderRightWidth && parseFloat(computed.borderRightWidth) > 0) || + (computed.borderBottomWidth && parseFloat(computed.borderBottomWidth) > 0) || + (computed.borderLeftWidth && parseFloat(computed.borderLeftWidth) > 0); + const hasShadow = computed.boxShadow && computed.boxShadow !== 'none'; + + if (hasBg || hasBorder || hasShadow) { + errors.push( + `Text element <${el.tagName.toLowerCase()}> has ${hasBg ? 'background' : hasBorder ? 'border' : 'shadow'}. ` + + 'Backgrounds, borders, and shadows are only supported on
                      elements, not text elements.' + ); + return; + } + } + + // Extract placeholder elements (for charts, etc.) + if (el.className && el.className.includes('placeholder')) { + const rect = el.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) { + errors.push( + `Placeholder "${el.id || 'unnamed'}" has ${rect.width === 0 ? 'width: 0' : 'height: 0'}. Check the layout CSS.` + ); + } else { + placeholders.push({ + id: el.id || `placeholder-${placeholders.length}`, + x: pxToInch(rect.left), + y: pxToInch(rect.top), + w: pxToInch(rect.width), + h: pxToInch(rect.height) + }); + } + processed.add(el); + return; + } + + // Extract images + if (el.tagName === 'IMG') { + const rect = el.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) { + elements.push({ + type: 'image', + src: el.src, + position: { + x: pxToInch(rect.left), + y: pxToInch(rect.top), + w: pxToInch(rect.width), + h: pxToInch(rect.height) + } + }); + processed.add(el); + return; + } + } + + // Extract DIVs with backgrounds/borders as shapes + const isContainer = el.tagName === 'DIV' && !textTags.includes(el.tagName); + if (isContainer) { + const computed = window.getComputedStyle(el); + const hasBg = computed.backgroundColor && computed.backgroundColor !== 'rgba(0, 0, 0, 0)'; + + // Validate: Check for unwrapped text content in DIV + for (const node of el.childNodes) { + if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent.trim(); + if (text) { + errors.push( + `DIV element contains unwrapped text "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}". ` + + 'All text must be wrapped in

                      ,

                      -

                      ,
                        , or
                          tags to appear in PowerPoint.' + ); + } + } + } + + // Check for background images on shapes + const bgImage = computed.backgroundImage; + if (bgImage && bgImage !== 'none') { + errors.push( + 'Background images on DIV elements are not supported. ' + + 'Use solid colors or borders for shapes, or use slide.addImage() in PptxGenJS to layer images.' + ); + return; + } + + // Check for borders - both uniform and partial + const borderTop = computed.borderTopWidth; + const borderRight = computed.borderRightWidth; + const borderBottom = computed.borderBottomWidth; + const borderLeft = computed.borderLeftWidth; + const borders = [borderTop, borderRight, borderBottom, borderLeft].map(b => parseFloat(b) || 0); + const hasBorder = borders.some(b => b > 0); + const hasUniformBorder = hasBorder && borders.every(b => b === borders[0]); + const borderLines = []; + + if (hasBorder && !hasUniformBorder) { + const rect = el.getBoundingClientRect(); + const x = pxToInch(rect.left); + const y = pxToInch(rect.top); + const w = pxToInch(rect.width); + const h = pxToInch(rect.height); + + // Collect lines to add after shape (inset by half the line width to center on edge) + if (parseFloat(borderTop) > 0) { + const widthPt = pxToPoints(borderTop); + const inset = (widthPt / 72) / 2; // Convert points to inches, then half + borderLines.push({ + type: 'line', + x1: x, y1: y + inset, x2: x + w, y2: y + inset, + width: widthPt, + color: rgbToHex(computed.borderTopColor) + }); + } + if (parseFloat(borderRight) > 0) { + const widthPt = pxToPoints(borderRight); + const inset = (widthPt / 72) / 2; + borderLines.push({ + type: 'line', + x1: x + w - inset, y1: y, x2: x + w - inset, y2: y + h, + width: widthPt, + color: rgbToHex(computed.borderRightColor) + }); + } + if (parseFloat(borderBottom) > 0) { + const widthPt = pxToPoints(borderBottom); + const inset = (widthPt / 72) / 2; + borderLines.push({ + type: 'line', + x1: x, y1: y + h - inset, x2: x + w, y2: y + h - inset, + width: widthPt, + color: rgbToHex(computed.borderBottomColor) + }); + } + if (parseFloat(borderLeft) > 0) { + const widthPt = pxToPoints(borderLeft); + const inset = (widthPt / 72) / 2; + borderLines.push({ + type: 'line', + x1: x + inset, y1: y, x2: x + inset, y2: y + h, + width: widthPt, + color: rgbToHex(computed.borderLeftColor) + }); + } + } + + if (hasBg || hasBorder) { + const rect = el.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) { + const shadow = parseBoxShadow(computed.boxShadow); + + // Only add shape if there's background or uniform border + if (hasBg || hasUniformBorder) { + elements.push({ + type: 'shape', + text: '', // Shape only - child text elements render on top + position: { + x: pxToInch(rect.left), + y: pxToInch(rect.top), + w: pxToInch(rect.width), + h: pxToInch(rect.height) + }, + shape: { + fill: hasBg ? rgbToHex(computed.backgroundColor) : null, + transparency: hasBg ? extractAlpha(computed.backgroundColor) : null, + line: hasUniformBorder ? { + color: rgbToHex(computed.borderColor), + width: pxToPoints(computed.borderWidth) + } : null, + // Convert border-radius to rectRadius (in inches) + // % values: 50%+ = circle (1), <50% = percentage of min dimension + // pt values: divide by 72 (72pt = 1 inch) + // px values: divide by 96 (96px = 1 inch) + rectRadius: (() => { + const radius = computed.borderRadius; + const radiusValue = parseFloat(radius); + if (radiusValue === 0) return 0; + + if (radius.includes('%')) { + if (radiusValue >= 50) return 1; + // Calculate percentage of smaller dimension + const minDim = Math.min(rect.width, rect.height); + return (radiusValue / 100) * pxToInch(minDim); + } + + if (radius.includes('pt')) return radiusValue / 72; + return radiusValue / PX_PER_IN; + })(), + shadow: shadow + } + }); + } + + // Add partial border lines + elements.push(...borderLines); + + processed.add(el); + return; + } + } + } + + // Extract bullet lists as single text block + if (el.tagName === 'UL' || el.tagName === 'OL') { + const rect = el.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) return; + + const liElements = Array.from(el.querySelectorAll('li')); + const items = []; + const ulComputed = window.getComputedStyle(el); + const ulPaddingLeftPt = pxToPoints(ulComputed.paddingLeft); + + // Split: margin-left for bullet position, indent for text position + // margin-left + indent = ul padding-left + const marginLeft = ulPaddingLeftPt * 0.5; + const textIndent = ulPaddingLeftPt * 0.5; + + liElements.forEach((li, idx) => { + const isLast = idx === liElements.length - 1; + const runs = parseInlineFormatting(li, { breakLine: false }); + // Clean manual bullets from first run + if (runs.length > 0) { + runs[0].text = runs[0].text.replace(/^[โ€ข\-\*โ–ชโ–ธ]\s*/, ''); + runs[0].options.bullet = { indent: textIndent }; + } + // Set breakLine on last run + if (runs.length > 0 && !isLast) { + runs[runs.length - 1].options.breakLine = true; + } + items.push(...runs); + }); + + const computed = window.getComputedStyle(liElements[0] || el); + + elements.push({ + type: 'list', + items: items, + position: { + x: pxToInch(rect.left), + y: pxToInch(rect.top), + w: pxToInch(rect.width), + h: pxToInch(rect.height) + }, + style: { + fontSize: pxToPoints(computed.fontSize), + fontFace: computed.fontFamily.split(',')[0].replace(/['"]/g, '').trim(), + color: rgbToHex(computed.color), + transparency: extractAlpha(computed.color), + align: computed.textAlign === 'start' ? 'left' : computed.textAlign, + lineSpacing: computed.lineHeight && computed.lineHeight !== 'normal' ? pxToPoints(computed.lineHeight) : null, + paraSpaceBefore: 0, + paraSpaceAfter: pxToPoints(computed.marginBottom), + // PptxGenJS margin array is [left, right, bottom, top] + margin: [marginLeft, 0, 0, 0] + } + }); + + liElements.forEach(li => processed.add(li)); + processed.add(el); + return; + } + + // Extract text elements (P, H1, H2, etc.) + if (!textTags.includes(el.tagName)) return; + + const rect = el.getBoundingClientRect(); + const text = el.textContent.trim(); + if (rect.width === 0 || rect.height === 0 || !text) return; + + // Validate: Check for manual bullet symbols in text elements (not in lists) + if (el.tagName !== 'LI' && /^[โ€ข\-\*โ–ชโ–ธโ—‹โ—โ—†โ—‡โ– โ–ก]\s/.test(text.trimStart())) { + errors.push( + `Text element <${el.tagName.toLowerCase()}> starts with bullet symbol "${text.substring(0, 20)}...". ` + + 'Use
                            or
                              lists instead of manual bullet symbols.' + ); + return; + } + + const computed = window.getComputedStyle(el); + const rotation = getRotation(computed.transform, computed.writingMode); + const { x, y, w, h } = getPositionAndSize(el, rect, rotation); + + const baseStyle = { + fontSize: pxToPoints(computed.fontSize), + fontFace: computed.fontFamily.split(',')[0].replace(/['"]/g, '').trim(), + color: rgbToHex(computed.color), + align: computed.textAlign === 'start' ? 'left' : computed.textAlign, + lineSpacing: pxToPoints(computed.lineHeight), + paraSpaceBefore: pxToPoints(computed.marginTop), + paraSpaceAfter: pxToPoints(computed.marginBottom), + // PptxGenJS margin array is [left, right, bottom, top] (not [top, right, bottom, left] as documented) + margin: [ + pxToPoints(computed.paddingLeft), + pxToPoints(computed.paddingRight), + pxToPoints(computed.paddingBottom), + pxToPoints(computed.paddingTop) + ] + }; + + const transparency = extractAlpha(computed.color); + if (transparency !== null) baseStyle.transparency = transparency; + + if (rotation !== null) baseStyle.rotate = rotation; + + const hasFormatting = el.querySelector('b, i, u, strong, em, span, br'); + + if (hasFormatting) { + // Text with inline formatting + const transformStr = computed.textTransform; + const runs = parseInlineFormatting(el, {}, [], (str) => applyTextTransform(str, transformStr)); + + // Adjust lineSpacing based on largest fontSize in runs + const adjustedStyle = { ...baseStyle }; + if (adjustedStyle.lineSpacing) { + const maxFontSize = Math.max( + adjustedStyle.fontSize, + ...runs.map(r => r.options?.fontSize || 0) + ); + if (maxFontSize > adjustedStyle.fontSize) { + const lineHeightMultiplier = adjustedStyle.lineSpacing / adjustedStyle.fontSize; + adjustedStyle.lineSpacing = maxFontSize * lineHeightMultiplier; + } + } + + elements.push({ + type: el.tagName.toLowerCase(), + text: runs, + position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) }, + style: adjustedStyle + }); + } else { + // Plain text - inherit CSS formatting + const textTransform = computed.textTransform; + const transformedText = applyTextTransform(text, textTransform); + + const isBold = computed.fontWeight === 'bold' || parseInt(computed.fontWeight) >= 600; + + elements.push({ + type: el.tagName.toLowerCase(), + text: transformedText, + position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) }, + style: { + ...baseStyle, + bold: isBold && !shouldSkipBold(computed.fontFamily), + italic: computed.fontStyle === 'italic', + underline: computed.textDecoration.includes('underline') + } + }); + } + + processed.add(el); + }); + + return { background, elements, placeholders, errors }; + }); +} + +async function html2pptx(htmlFile, pres, options = {}) { + const { + tmpDir = process.env.TMPDIR || '/tmp', + slide = null + } = options; + + try { + // Use Chrome on macOS, default Chromium on Unix + const launchOptions = { env: { TMPDIR: tmpDir } }; + if (process.platform === 'darwin') { + launchOptions.channel = 'chrome'; + } + + const browser = await chromium.launch(launchOptions); + + let bodyDimensions; + let slideData; + + const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile); + const validationErrors = []; + + try { + const page = await browser.newPage(); + page.on('console', (msg) => { + // Log the message text to your test runner's console + console.log(`Browser console: ${msg.text()}`); + }); + + await page.goto(`file://${filePath}`); + + bodyDimensions = await getBodyDimensions(page); + + await page.setViewportSize({ + width: Math.round(bodyDimensions.width), + height: Math.round(bodyDimensions.height) + }); + + slideData = await extractSlideData(page); + } finally { + await browser.close(); + } + + // Collect all validation errors + if (bodyDimensions.errors && bodyDimensions.errors.length > 0) { + validationErrors.push(...bodyDimensions.errors); + } + + const dimensionErrors = validateDimensions(bodyDimensions, pres); + if (dimensionErrors.length > 0) { + validationErrors.push(...dimensionErrors); + } + + const textBoxPositionErrors = validateTextBoxPosition(slideData, bodyDimensions); + if (textBoxPositionErrors.length > 0) { + validationErrors.push(...textBoxPositionErrors); + } + + if (slideData.errors && slideData.errors.length > 0) { + validationErrors.push(...slideData.errors); + } + + // Throw all errors at once if any exist + if (validationErrors.length > 0) { + const errorMessage = validationErrors.length === 1 + ? validationErrors[0] + : `Multiple validation errors found:\n${validationErrors.map((e, i) => ` ${i + 1}. ${e}`).join('\n')}`; + throw new Error(errorMessage); + } + + const targetSlide = slide || pres.addSlide(); + + await addBackground(slideData, targetSlide, tmpDir); + addElements(slideData, targetSlide, pres); + + return { slide: targetSlide, placeholders: slideData.placeholders }; + } catch (error) { + if (!error.message.startsWith(htmlFile)) { + throw new Error(`${htmlFile}: ${error.message}`); + } + throw error; + } +} + +module.exports = html2pptx; \ No newline at end of file diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/inventory.py b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/inventory.py new file mode 100755 index 00000000..edda390e --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/inventory.py @@ -0,0 +1,1020 @@ +#!/usr/bin/env python3 +""" +Extract structured text content from PowerPoint presentations. + +This module provides functionality to: +- Extract all text content from PowerPoint shapes +- Preserve paragraph formatting (alignment, bullets, fonts, spacing) +- Handle nested GroupShapes recursively with correct absolute positions +- Sort shapes by visual position on slides +- Filter out slide numbers and non-content placeholders +- Export to JSON with clean, structured data + +Classes: + ParagraphData: Represents a text paragraph with formatting + ShapeData: Represents a shape with position and text content + +Main Functions: + extract_text_inventory: Extract all text from a presentation + save_inventory: Save extracted data to JSON + +Usage: + python inventory.py input.pptx output.json +""" + +import argparse +import json +import platform +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple, Union + +from PIL import Image, ImageDraw, ImageFont +from pptx import Presentation +from pptx.enum.text import PP_ALIGN +from pptx.shapes.base import BaseShape + +# Type aliases for cleaner signatures +JsonValue = Union[str, int, float, bool, None] +ParagraphDict = Dict[str, JsonValue] +ShapeDict = Dict[ + str, Union[str, float, bool, List[ParagraphDict], List[str], Dict[str, Any], None] +] +InventoryData = Dict[ + str, Dict[str, "ShapeData"] +] # Dict of slide_id -> {shape_id -> ShapeData} +InventoryDict = Dict[str, Dict[str, ShapeDict]] # JSON-serializable inventory + + +def main(): + """Main entry point for command-line usage.""" + parser = argparse.ArgumentParser( + description="Extract text inventory from PowerPoint with proper GroupShape support.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python inventory.py presentation.pptx inventory.json + Extracts text inventory with correct absolute positions for grouped shapes + + python inventory.py presentation.pptx inventory.json --issues-only + Extracts only text shapes that have overflow or overlap issues + +The output JSON includes: + - All text content organized by slide and shape + - Correct absolute positions for shapes in groups + - Visual position and size in inches + - Paragraph properties and formatting + - Issue detection: text overflow and shape overlaps + """, + ) + + parser.add_argument("input", help="Input PowerPoint file (.pptx)") + parser.add_argument("output", help="Output JSON file for inventory") + parser.add_argument( + "--issues-only", + action="store_true", + help="Include only text shapes that have overflow or overlap issues", + ) + + args = parser.parse_args() + + input_path = Path(args.input) + if not input_path.exists(): + print(f"Error: Input file not found: {args.input}") + sys.exit(1) + + if not input_path.suffix.lower() == ".pptx": + print("Error: Input must be a PowerPoint file (.pptx)") + sys.exit(1) + + try: + print(f"Extracting text inventory from: {args.input}") + if args.issues_only: + print( + "Filtering to include only text shapes with issues (overflow/overlap)" + ) + inventory = extract_text_inventory(input_path, issues_only=args.issues_only) + + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + save_inventory(inventory, output_path) + + print(f"Output saved to: {args.output}") + + # Report statistics + total_slides = len(inventory) + total_shapes = sum(len(shapes) for shapes in inventory.values()) + if args.issues_only: + if total_shapes > 0: + print( + f"Found {total_shapes} text elements with issues in {total_slides} slides" + ) + else: + print("No issues discovered") + else: + print( + f"Found text in {total_slides} slides with {total_shapes} text elements" + ) + + except Exception as e: + print(f"Error processing presentation: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + + +@dataclass +class ShapeWithPosition: + """A shape with its absolute position on the slide.""" + + shape: BaseShape + absolute_left: int # in EMUs + absolute_top: int # in EMUs + + +class ParagraphData: + """Data structure for paragraph properties extracted from a PowerPoint paragraph.""" + + def __init__(self, paragraph: Any): + """Initialize from a PowerPoint paragraph object. + + Args: + paragraph: The PowerPoint paragraph object + """ + self.text: str = paragraph.text.strip() + self.bullet: bool = False + self.level: Optional[int] = None + self.alignment: Optional[str] = None + self.space_before: Optional[float] = None + self.space_after: Optional[float] = None + self.font_name: Optional[str] = None + self.font_size: Optional[float] = None + self.bold: Optional[bool] = None + self.italic: Optional[bool] = None + self.underline: Optional[bool] = None + self.color: Optional[str] = None + self.theme_color: Optional[str] = None + self.line_spacing: Optional[float] = None + + # Check for bullet formatting + if ( + hasattr(paragraph, "_p") + and paragraph._p is not None + and paragraph._p.pPr is not None + ): + pPr = paragraph._p.pPr + ns = "{http://schemas.openxmlformats.org/drawingml/2006/main}" + if ( + pPr.find(f"{ns}buChar") is not None + or pPr.find(f"{ns}buAutoNum") is not None + ): + self.bullet = True + if hasattr(paragraph, "level"): + self.level = paragraph.level + + # Add alignment if not LEFT (default) + if hasattr(paragraph, "alignment") and paragraph.alignment is not None: + alignment_map = { + PP_ALIGN.CENTER: "CENTER", + PP_ALIGN.RIGHT: "RIGHT", + PP_ALIGN.JUSTIFY: "JUSTIFY", + } + if paragraph.alignment in alignment_map: + self.alignment = alignment_map[paragraph.alignment] + + # Add spacing properties if set + if hasattr(paragraph, "space_before") and paragraph.space_before: + self.space_before = paragraph.space_before.pt + if hasattr(paragraph, "space_after") and paragraph.space_after: + self.space_after = paragraph.space_after.pt + + # Extract font properties from first run + if paragraph.runs: + first_run = paragraph.runs[0] + if hasattr(first_run, "font"): + font = first_run.font + if font.name: + self.font_name = font.name + if font.size: + self.font_size = font.size.pt + if font.bold is not None: + self.bold = font.bold + if font.italic is not None: + self.italic = font.italic + if font.underline is not None: + self.underline = font.underline + + # Handle color - both RGB and theme colors + try: + # Try RGB color first + if font.color.rgb: + self.color = str(font.color.rgb) + except (AttributeError, TypeError): + # Fall back to theme color + try: + if font.color.theme_color: + self.theme_color = font.color.theme_color.name + except (AttributeError, TypeError): + pass + + # Add line spacing if set + if hasattr(paragraph, "line_spacing") and paragraph.line_spacing is not None: + if hasattr(paragraph.line_spacing, "pt"): + self.line_spacing = round(paragraph.line_spacing.pt, 2) + else: + # Multiplier - convert to points + font_size = self.font_size if self.font_size else 12.0 + self.line_spacing = round(paragraph.line_spacing * font_size, 2) + + def to_dict(self) -> ParagraphDict: + """Convert to dictionary for JSON serialization, excluding None values.""" + result: ParagraphDict = {"text": self.text} + + # Add optional fields only if they have values + if self.bullet: + result["bullet"] = self.bullet + if self.level is not None: + result["level"] = self.level + if self.alignment: + result["alignment"] = self.alignment + if self.space_before is not None: + result["space_before"] = self.space_before + if self.space_after is not None: + result["space_after"] = self.space_after + if self.font_name: + result["font_name"] = self.font_name + if self.font_size is not None: + result["font_size"] = self.font_size + if self.bold is not None: + result["bold"] = self.bold + if self.italic is not None: + result["italic"] = self.italic + if self.underline is not None: + result["underline"] = self.underline + if self.color: + result["color"] = self.color + if self.theme_color: + result["theme_color"] = self.theme_color + if self.line_spacing is not None: + result["line_spacing"] = self.line_spacing + + return result + + +class ShapeData: + """Data structure for shape properties extracted from a PowerPoint shape.""" + + @staticmethod + def emu_to_inches(emu: int) -> float: + """Convert EMUs (English Metric Units) to inches.""" + return emu / 914400.0 + + @staticmethod + def inches_to_pixels(inches: float, dpi: int = 96) -> int: + """Convert inches to pixels at given DPI.""" + return int(inches * dpi) + + @staticmethod + def get_font_path(font_name: str) -> Optional[str]: + """Get the font file path for a given font name. + + Args: + font_name: Name of the font (e.g., 'Arial', 'Calibri') + + Returns: + Path to the font file, or None if not found + """ + system = platform.system() + + # Common font file variations to try + font_variations = [ + font_name, + font_name.lower(), + font_name.replace(" ", ""), + font_name.replace(" ", "-"), + ] + + # Define font directories and extensions by platform + if system == "Darwin": # macOS + font_dirs = [ + "/System/Library/Fonts/", + "/Library/Fonts/", + "~/Library/Fonts/", + ] + extensions = [".ttf", ".otf", ".ttc", ".dfont"] + else: # Linux + font_dirs = [ + "/usr/share/fonts/truetype/", + "/usr/local/share/fonts/", + "~/.fonts/", + ] + extensions = [".ttf", ".otf"] + + # Try to find the font file + from pathlib import Path + + for font_dir in font_dirs: + font_dir_path = Path(font_dir).expanduser() + if not font_dir_path.exists(): + continue + + # First try exact matches + for variant in font_variations: + for ext in extensions: + font_path = font_dir_path / f"{variant}{ext}" + if font_path.exists(): + return str(font_path) + + # Then try fuzzy matching - find files containing the font name + try: + for file_path in font_dir_path.iterdir(): + if file_path.is_file(): + file_name_lower = file_path.name.lower() + font_name_lower = font_name.lower().replace(" ", "") + if font_name_lower in file_name_lower and any( + file_name_lower.endswith(ext) for ext in extensions + ): + return str(file_path) + except (OSError, PermissionError): + continue + + return None + + @staticmethod + def get_slide_dimensions(slide: Any) -> tuple[Optional[int], Optional[int]]: + """Get slide dimensions from slide object. + + Args: + slide: Slide object + + Returns: + Tuple of (width_emu, height_emu) or (None, None) if not found + """ + try: + prs = slide.part.package.presentation_part.presentation + return prs.slide_width, prs.slide_height + except (AttributeError, TypeError): + return None, None + + @staticmethod + def get_default_font_size(shape: BaseShape, slide_layout: Any) -> Optional[float]: + """Extract default font size from slide layout for a placeholder shape. + + Args: + shape: Placeholder shape + slide_layout: Slide layout containing the placeholder definition + + Returns: + Default font size in points, or None if not found + """ + try: + if not hasattr(shape, "placeholder_format"): + return None + + shape_type = shape.placeholder_format.type # type: ignore + for layout_placeholder in slide_layout.placeholders: + if layout_placeholder.placeholder_format.type == shape_type: + # Find first defRPr element with sz (size) attribute + for elem in layout_placeholder.element.iter(): + if "defRPr" in elem.tag and (sz := elem.get("sz")): + return float(sz) / 100.0 # Convert EMUs to points + break + except Exception: + pass + return None + + def __init__( + self, + shape: BaseShape, + absolute_left: Optional[int] = None, + absolute_top: Optional[int] = None, + slide: Optional[Any] = None, + ): + """Initialize from a PowerPoint shape object. + + Args: + shape: The PowerPoint shape object (should be pre-validated) + absolute_left: Absolute left position in EMUs (for shapes in groups) + absolute_top: Absolute top position in EMUs (for shapes in groups) + slide: Optional slide object to get dimensions and layout information + """ + self.shape = shape # Store reference to original shape + self.shape_id: str = "" # Will be set after sorting + + # Get slide dimensions from slide object + self.slide_width_emu, self.slide_height_emu = ( + self.get_slide_dimensions(slide) if slide else (None, None) + ) + + # Get placeholder type if applicable + self.placeholder_type: Optional[str] = None + self.default_font_size: Optional[float] = None + if hasattr(shape, "is_placeholder") and shape.is_placeholder: # type: ignore + if shape.placeholder_format and shape.placeholder_format.type: # type: ignore + self.placeholder_type = ( + str(shape.placeholder_format.type).split(".")[-1].split(" ")[0] # type: ignore + ) + + # Get default font size from layout + if slide and hasattr(slide, "slide_layout"): + self.default_font_size = self.get_default_font_size( + shape, slide.slide_layout + ) + + # Get position information + # Use absolute positions if provided (for shapes in groups), otherwise use shape's position + left_emu = ( + absolute_left + if absolute_left is not None + else (shape.left if hasattr(shape, "left") else 0) + ) + top_emu = ( + absolute_top + if absolute_top is not None + else (shape.top if hasattr(shape, "top") else 0) + ) + + self.left: float = round(self.emu_to_inches(left_emu), 2) # type: ignore + self.top: float = round(self.emu_to_inches(top_emu), 2) # type: ignore + self.width: float = round( + self.emu_to_inches(shape.width if hasattr(shape, "width") else 0), + 2, # type: ignore + ) + self.height: float = round( + self.emu_to_inches(shape.height if hasattr(shape, "height") else 0), + 2, # type: ignore + ) + + # Store EMU positions for overflow calculations + self.left_emu = left_emu + self.top_emu = top_emu + self.width_emu = shape.width if hasattr(shape, "width") else 0 + self.height_emu = shape.height if hasattr(shape, "height") else 0 + + # Calculate overflow status + self.frame_overflow_bottom: Optional[float] = None + self.slide_overflow_right: Optional[float] = None + self.slide_overflow_bottom: Optional[float] = None + self.overlapping_shapes: Dict[ + str, float + ] = {} # Dict of shape_id -> overlap area in sq inches + self.warnings: List[str] = [] + self._estimate_frame_overflow() + self._calculate_slide_overflow() + self._detect_bullet_issues() + + @property + def paragraphs(self) -> List[ParagraphData]: + """Calculate paragraphs from the shape's text frame.""" + if not self.shape or not hasattr(self.shape, "text_frame"): + return [] + + paragraphs = [] + for paragraph in self.shape.text_frame.paragraphs: # type: ignore + if paragraph.text.strip(): + paragraphs.append(ParagraphData(paragraph)) + return paragraphs + + def _get_default_font_size(self) -> int: + """Get default font size from theme text styles or use conservative default.""" + try: + if not ( + hasattr(self.shape, "part") and hasattr(self.shape.part, "slide_layout") + ): + return 14 + + slide_master = self.shape.part.slide_layout.slide_master # type: ignore + if not hasattr(slide_master, "element"): + return 14 + + # Determine theme style based on placeholder type + style_name = "bodyStyle" # Default + if self.placeholder_type and "TITLE" in self.placeholder_type: + style_name = "titleStyle" + + # Find font size in theme styles + for child in slide_master.element.iter(): + tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag + if tag == style_name: + for elem in child.iter(): + if "sz" in elem.attrib: + return int(elem.attrib["sz"]) // 100 + except Exception: + pass + + return 14 # Conservative default for body text + + def _get_usable_dimensions(self, text_frame) -> Tuple[int, int]: + """Get usable width and height in pixels after accounting for margins.""" + # Default PowerPoint margins in inches + margins = {"top": 0.05, "bottom": 0.05, "left": 0.1, "right": 0.1} + + # Override with actual margins if set + if hasattr(text_frame, "margin_top") and text_frame.margin_top: + margins["top"] = self.emu_to_inches(text_frame.margin_top) + if hasattr(text_frame, "margin_bottom") and text_frame.margin_bottom: + margins["bottom"] = self.emu_to_inches(text_frame.margin_bottom) + if hasattr(text_frame, "margin_left") and text_frame.margin_left: + margins["left"] = self.emu_to_inches(text_frame.margin_left) + if hasattr(text_frame, "margin_right") and text_frame.margin_right: + margins["right"] = self.emu_to_inches(text_frame.margin_right) + + # Calculate usable area + usable_width = self.width - margins["left"] - margins["right"] + usable_height = self.height - margins["top"] - margins["bottom"] + + # Convert to pixels + return ( + self.inches_to_pixels(usable_width), + self.inches_to_pixels(usable_height), + ) + + def _wrap_text_line(self, line: str, max_width_px: int, draw, font) -> List[str]: + """Wrap a single line of text to fit within max_width_px.""" + if not line: + return [""] + + # Use textlength for efficient width calculation + if draw.textlength(line, font=font) <= max_width_px: + return [line] + + # Need to wrap - split into words + wrapped = [] + words = line.split(" ") + current_line = "" + + for word in words: + test_line = current_line + (" " if current_line else "") + word + if draw.textlength(test_line, font=font) <= max_width_px: + current_line = test_line + else: + if current_line: + wrapped.append(current_line) + current_line = word + + if current_line: + wrapped.append(current_line) + + return wrapped + + def _estimate_frame_overflow(self) -> None: + """Estimate if text overflows the shape bounds using PIL text measurement.""" + if not self.shape or not hasattr(self.shape, "text_frame"): + return + + text_frame = self.shape.text_frame # type: ignore + if not text_frame or not text_frame.paragraphs: + return + + # Get usable dimensions after accounting for margins + usable_width_px, usable_height_px = self._get_usable_dimensions(text_frame) + if usable_width_px <= 0 or usable_height_px <= 0: + return + + # Set up PIL for text measurement + dummy_img = Image.new("RGB", (1, 1)) + draw = ImageDraw.Draw(dummy_img) + + # Get default font size from placeholder or use conservative estimate + default_font_size = self._get_default_font_size() + + # Calculate total height of all paragraphs + total_height_px = 0 + + for para_idx, paragraph in enumerate(text_frame.paragraphs): + if not paragraph.text.strip(): + continue + + para_data = ParagraphData(paragraph) + + # Load font for this paragraph + font_name = para_data.font_name or "Arial" + font_size = int(para_data.font_size or default_font_size) + + font = None + font_path = self.get_font_path(font_name) + if font_path: + try: + font = ImageFont.truetype(font_path, size=font_size) + except Exception: + font = ImageFont.load_default() + else: + font = ImageFont.load_default() + + # Wrap all lines in this paragraph + all_wrapped_lines = [] + for line in paragraph.text.split("\n"): + wrapped = self._wrap_text_line(line, usable_width_px, draw, font) + all_wrapped_lines.extend(wrapped) + + if all_wrapped_lines: + # Calculate line height + if para_data.line_spacing: + # Custom line spacing explicitly set + line_height_px = para_data.line_spacing * 96 / 72 + else: + # PowerPoint default single spacing (1.0x font size) + line_height_px = font_size * 96 / 72 + + # Add space_before (except first paragraph) + if para_idx > 0 and para_data.space_before: + total_height_px += para_data.space_before * 96 / 72 + + # Add paragraph text height + total_height_px += len(all_wrapped_lines) * line_height_px + + # Add space_after + if para_data.space_after: + total_height_px += para_data.space_after * 96 / 72 + + # Check for overflow (ignore negligible overflows <= 0.05") + if total_height_px > usable_height_px: + overflow_px = total_height_px - usable_height_px + overflow_inches = round(overflow_px / 96.0, 2) + if overflow_inches > 0.05: # Only report significant overflows + self.frame_overflow_bottom = overflow_inches + + def _calculate_slide_overflow(self) -> None: + """Calculate if shape overflows the slide boundaries.""" + if self.slide_width_emu is None or self.slide_height_emu is None: + return + + # Check right overflow (ignore negligible overflows <= 0.01") + right_edge_emu = self.left_emu + self.width_emu + if right_edge_emu > self.slide_width_emu: + overflow_emu = right_edge_emu - self.slide_width_emu + overflow_inches = round(self.emu_to_inches(overflow_emu), 2) + if overflow_inches > 0.01: # Only report significant overflows + self.slide_overflow_right = overflow_inches + + # Check bottom overflow (ignore negligible overflows <= 0.01") + bottom_edge_emu = self.top_emu + self.height_emu + if bottom_edge_emu > self.slide_height_emu: + overflow_emu = bottom_edge_emu - self.slide_height_emu + overflow_inches = round(self.emu_to_inches(overflow_emu), 2) + if overflow_inches > 0.01: # Only report significant overflows + self.slide_overflow_bottom = overflow_inches + + def _detect_bullet_issues(self) -> None: + """Detect bullet point formatting issues in paragraphs.""" + if not self.shape or not hasattr(self.shape, "text_frame"): + return + + text_frame = self.shape.text_frame # type: ignore + if not text_frame or not text_frame.paragraphs: + return + + # Common bullet symbols that indicate manual bullets + bullet_symbols = ["โ€ข", "โ—", "โ—‹"] + + for paragraph in text_frame.paragraphs: + text = paragraph.text.strip() + # Check for manual bullet symbols + if text and any(text.startswith(symbol + " ") for symbol in bullet_symbols): + self.warnings.append( + "manual_bullet_symbol: use proper bullet formatting" + ) + break + + @property + def has_any_issues(self) -> bool: + """Check if shape has any issues (overflow, overlap, or warnings).""" + return ( + self.frame_overflow_bottom is not None + or self.slide_overflow_right is not None + or self.slide_overflow_bottom is not None + or len(self.overlapping_shapes) > 0 + or len(self.warnings) > 0 + ) + + def to_dict(self) -> ShapeDict: + """Convert to dictionary for JSON serialization.""" + result: ShapeDict = { + "left": self.left, + "top": self.top, + "width": self.width, + "height": self.height, + } + + # Add optional fields if present + if self.placeholder_type: + result["placeholder_type"] = self.placeholder_type + + if self.default_font_size: + result["default_font_size"] = self.default_font_size + + # Add overflow information only if there is overflow + overflow_data = {} + + # Add frame overflow if present + if self.frame_overflow_bottom is not None: + overflow_data["frame"] = {"overflow_bottom": self.frame_overflow_bottom} + + # Add slide overflow if present + slide_overflow = {} + if self.slide_overflow_right is not None: + slide_overflow["overflow_right"] = self.slide_overflow_right + if self.slide_overflow_bottom is not None: + slide_overflow["overflow_bottom"] = self.slide_overflow_bottom + if slide_overflow: + overflow_data["slide"] = slide_overflow + + # Only add overflow field if there is overflow + if overflow_data: + result["overflow"] = overflow_data + + # Add overlap field if there are overlapping shapes + if self.overlapping_shapes: + result["overlap"] = {"overlapping_shapes": self.overlapping_shapes} + + # Add warnings field if there are warnings + if self.warnings: + result["warnings"] = self.warnings + + # Add paragraphs after placeholder_type + result["paragraphs"] = [para.to_dict() for para in self.paragraphs] + + return result + + +def is_valid_shape(shape: BaseShape) -> bool: + """Check if a shape contains meaningful text content.""" + # Must have a text frame with content + if not hasattr(shape, "text_frame") or not shape.text_frame: # type: ignore + return False + + text = shape.text_frame.text.strip() # type: ignore + if not text: + return False + + # Skip slide numbers and numeric footers + if hasattr(shape, "is_placeholder") and shape.is_placeholder: # type: ignore + if shape.placeholder_format and shape.placeholder_format.type: # type: ignore + placeholder_type = ( + str(shape.placeholder_format.type).split(".")[-1].split(" ")[0] # type: ignore + ) + if placeholder_type == "SLIDE_NUMBER": + return False + if placeholder_type == "FOOTER" and text.isdigit(): + return False + + return True + + +def collect_shapes_with_absolute_positions( + shape: BaseShape, parent_left: int = 0, parent_top: int = 0 +) -> List[ShapeWithPosition]: + """Recursively collect all shapes with valid text, calculating absolute positions. + + For shapes within groups, their positions are relative to the group. + This function calculates the absolute position on the slide by accumulating + parent group offsets. + + Args: + shape: The shape to process + parent_left: Accumulated left offset from parent groups (in EMUs) + parent_top: Accumulated top offset from parent groups (in EMUs) + + Returns: + List of ShapeWithPosition objects with absolute positions + """ + if hasattr(shape, "shapes"): # GroupShape + result = [] + # Get this group's position + group_left = shape.left if hasattr(shape, "left") else 0 + group_top = shape.top if hasattr(shape, "top") else 0 + + # Calculate absolute position for this group + abs_group_left = parent_left + group_left + abs_group_top = parent_top + group_top + + # Process children with accumulated offsets + for child in shape.shapes: # type: ignore + result.extend( + collect_shapes_with_absolute_positions( + child, abs_group_left, abs_group_top + ) + ) + return result + + # Regular shape - check if it has valid text + if is_valid_shape(shape): + # Calculate absolute position + shape_left = shape.left if hasattr(shape, "left") else 0 + shape_top = shape.top if hasattr(shape, "top") else 0 + + return [ + ShapeWithPosition( + shape=shape, + absolute_left=parent_left + shape_left, + absolute_top=parent_top + shape_top, + ) + ] + + return [] + + +def sort_shapes_by_position(shapes: List[ShapeData]) -> List[ShapeData]: + """Sort shapes by visual position (top-to-bottom, left-to-right). + + Shapes within 0.5 inches vertically are considered on the same row. + """ + if not shapes: + return shapes + + # Sort by top position first + shapes = sorted(shapes, key=lambda s: (s.top, s.left)) + + # Group shapes by row (within 0.5 inches vertically) + result = [] + row = [shapes[0]] + row_top = shapes[0].top + + for shape in shapes[1:]: + if abs(shape.top - row_top) <= 0.5: + row.append(shape) + else: + # Sort current row by left position and add to result + result.extend(sorted(row, key=lambda s: s.left)) + row = [shape] + row_top = shape.top + + # Don't forget the last row + result.extend(sorted(row, key=lambda s: s.left)) + return result + + +def calculate_overlap( + rect1: Tuple[float, float, float, float], + rect2: Tuple[float, float, float, float], + tolerance: float = 0.05, +) -> Tuple[bool, float]: + """Calculate if and how much two rectangles overlap. + + Args: + rect1: (left, top, width, height) of first rectangle in inches + rect2: (left, top, width, height) of second rectangle in inches + tolerance: Minimum overlap in inches to consider as overlapping (default: 0.05") + + Returns: + Tuple of (overlaps, overlap_area) where: + - overlaps: True if rectangles overlap by more than tolerance + - overlap_area: Area of overlap in square inches + """ + left1, top1, w1, h1 = rect1 + left2, top2, w2, h2 = rect2 + + # Calculate overlap dimensions + overlap_width = min(left1 + w1, left2 + w2) - max(left1, left2) + overlap_height = min(top1 + h1, top2 + h2) - max(top1, top2) + + # Check if there's meaningful overlap (more than tolerance) + if overlap_width > tolerance and overlap_height > tolerance: + # Calculate overlap area in square inches + overlap_area = overlap_width * overlap_height + return True, round(overlap_area, 2) + + return False, 0 + + +def detect_overlaps(shapes: List[ShapeData]) -> None: + """Detect overlapping shapes and update their overlapping_shapes dictionaries. + + This function requires each ShapeData to have its shape_id already set. + It modifies the shapes in-place, adding shape IDs with overlap areas in square inches. + + Args: + shapes: List of ShapeData objects with shape_id attributes set + """ + n = len(shapes) + + # Compare each pair of shapes + for i in range(n): + for j in range(i + 1, n): + shape1 = shapes[i] + shape2 = shapes[j] + + # Ensure shape IDs are set + assert shape1.shape_id, f"Shape at index {i} has no shape_id" + assert shape2.shape_id, f"Shape at index {j} has no shape_id" + + rect1 = (shape1.left, shape1.top, shape1.width, shape1.height) + rect2 = (shape2.left, shape2.top, shape2.width, shape2.height) + + overlaps, overlap_area = calculate_overlap(rect1, rect2) + + if overlaps: + # Add shape IDs with overlap area in square inches + shape1.overlapping_shapes[shape2.shape_id] = overlap_area + shape2.overlapping_shapes[shape1.shape_id] = overlap_area + + +def extract_text_inventory( + pptx_path: Path, prs: Optional[Any] = None, issues_only: bool = False +) -> InventoryData: + """Extract text content from all slides in a PowerPoint presentation. + + Args: + pptx_path: Path to the PowerPoint file + prs: Optional Presentation object to use. If not provided, will load from pptx_path. + issues_only: If True, only include shapes that have overflow or overlap issues + + Returns a nested dictionary: {slide-N: {shape-N: ShapeData}} + Shapes are sorted by visual position (top-to-bottom, left-to-right). + The ShapeData objects contain the full shape information and can be + converted to dictionaries for JSON serialization using to_dict(). + """ + if prs is None: + prs = Presentation(str(pptx_path)) + inventory: InventoryData = {} + + for slide_idx, slide in enumerate(prs.slides): + # Collect all valid shapes from this slide with absolute positions + shapes_with_positions = [] + for shape in slide.shapes: # type: ignore + shapes_with_positions.extend(collect_shapes_with_absolute_positions(shape)) + + if not shapes_with_positions: + continue + + # Convert to ShapeData with absolute positions and slide reference + shape_data_list = [ + ShapeData( + swp.shape, + swp.absolute_left, + swp.absolute_top, + slide, + ) + for swp in shapes_with_positions + ] + + # Sort by visual position and assign stable IDs in one step + sorted_shapes = sort_shapes_by_position(shape_data_list) + for idx, shape_data in enumerate(sorted_shapes): + shape_data.shape_id = f"shape-{idx}" + + # Detect overlaps using the stable shape IDs + if len(sorted_shapes) > 1: + detect_overlaps(sorted_shapes) + + # Filter for issues only if requested (after overlap detection) + if issues_only: + sorted_shapes = [sd for sd in sorted_shapes if sd.has_any_issues] + + if not sorted_shapes: + continue + + # Create slide inventory using the stable shape IDs + inventory[f"slide-{slide_idx}"] = { + shape_data.shape_id: shape_data for shape_data in sorted_shapes + } + + return inventory + + +def get_inventory_as_dict(pptx_path: Path, issues_only: bool = False) -> InventoryDict: + """Extract text inventory and return as JSON-serializable dictionaries. + + This is a convenience wrapper around extract_text_inventory that returns + dictionaries instead of ShapeData objects, useful for testing and direct + JSON serialization. + + Args: + pptx_path: Path to the PowerPoint file + issues_only: If True, only include shapes that have overflow or overlap issues + + Returns: + Nested dictionary with all data serialized for JSON + """ + inventory = extract_text_inventory(pptx_path, issues_only=issues_only) + + # Convert ShapeData objects to dictionaries + dict_inventory: InventoryDict = {} + for slide_key, shapes in inventory.items(): + dict_inventory[slide_key] = { + shape_key: shape_data.to_dict() for shape_key, shape_data in shapes.items() + } + + return dict_inventory + + +def save_inventory(inventory: InventoryData, output_path: Path) -> None: + """Save inventory to JSON file with proper formatting. + + Converts ShapeData objects to dictionaries for JSON serialization. + """ + # Convert ShapeData objects to dictionaries + json_inventory: InventoryDict = {} + for slide_key, shapes in inventory.items(): + json_inventory[slide_key] = { + shape_key: shape_data.to_dict() for shape_key, shape_data in shapes.items() + } + + with open(output_path, "w", encoding="utf-8") as f: + json.dump(json_inventory, f, indent=2, ensure_ascii=False) + + +if __name__ == "__main__": + main() diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/rearrange.py b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/rearrange.py new file mode 100755 index 00000000..2519911f --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/rearrange.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +""" +Rearrange PowerPoint slides based on a sequence of indices. + +Usage: + python rearrange.py template.pptx output.pptx 0,34,34,50,52 + +This will create output.pptx using slides from template.pptx in the specified order. +Slides can be repeated (e.g., 34 appears twice). +""" + +import argparse +import shutil +import sys +from copy import deepcopy +from pathlib import Path + +import six +from pptx import Presentation + + +def main(): + parser = argparse.ArgumentParser( + description="Rearrange PowerPoint slides based on a sequence of indices.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python rearrange.py template.pptx output.pptx 0,34,34,50,52 + Creates output.pptx using slides 0, 34 (twice), 50, and 52 from template.pptx + + python rearrange.py template.pptx output.pptx 5,3,1,2,4 + Creates output.pptx with slides reordered as specified + +Note: Slide indices are 0-based (first slide is 0, second is 1, etc.) + """, + ) + + parser.add_argument("template", help="Path to template PPTX file") + parser.add_argument("output", help="Path for output PPTX file") + parser.add_argument( + "sequence", help="Comma-separated sequence of slide indices (0-based)" + ) + + args = parser.parse_args() + + # Parse the slide sequence + try: + slide_sequence = [int(x.strip()) for x in args.sequence.split(",")] + except ValueError: + print( + "Error: Invalid sequence format. Use comma-separated integers (e.g., 0,34,34,50,52)" + ) + sys.exit(1) + + # Check template exists + template_path = Path(args.template) + if not template_path.exists(): + print(f"Error: Template file not found: {args.template}") + sys.exit(1) + + # Create output directory if needed + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + + try: + rearrange_presentation(template_path, output_path, slide_sequence) + except ValueError as e: + print(f"Error: {e}") + sys.exit(1) + except Exception as e: + print(f"Error processing presentation: {e}") + sys.exit(1) + + +def duplicate_slide(pres, index): + """Duplicate a slide in the presentation.""" + source = pres.slides[index] + + # Use source's layout to preserve formatting + new_slide = pres.slides.add_slide(source.slide_layout) + + # Collect all image and media relationships from the source slide + image_rels = {} + for rel_id, rel in six.iteritems(source.part.rels): + if "image" in rel.reltype or "media" in rel.reltype: + image_rels[rel_id] = rel + + # CRITICAL: Clear placeholder shapes to avoid duplicates + for shape in new_slide.shapes: + sp = shape.element + sp.getparent().remove(sp) + + # Copy all shapes from source + for shape in source.shapes: + el = shape.element + new_el = deepcopy(el) + new_slide.shapes._spTree.insert_element_before(new_el, "p:extLst") + + # Handle picture shapes - need to update the blip reference + # Look for all blip elements (they can be in pic or other contexts) + # Using the element's own xpath method without namespaces argument + blips = new_el.xpath(".//a:blip[@r:embed]") + for blip in blips: + old_rId = blip.get( + "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed" + ) + if old_rId in image_rels: + # Create a new relationship in the destination slide for this image + old_rel = image_rels[old_rId] + # get_or_add returns the rId directly, or adds and returns new rId + new_rId = new_slide.part.rels.get_or_add( + old_rel.reltype, old_rel._target + ) + # Update the blip's embed reference to use the new relationship ID + blip.set( + "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed", + new_rId, + ) + + # Copy any additional image/media relationships that might be referenced elsewhere + for rel_id, rel in image_rels.items(): + try: + new_slide.part.rels.get_or_add(rel.reltype, rel._target) + except Exception: + pass # Relationship might already exist + + return new_slide + + +def delete_slide(pres, index): + """Delete a slide from the presentation.""" + rId = pres.slides._sldIdLst[index].rId + pres.part.drop_rel(rId) + del pres.slides._sldIdLst[index] + + +def reorder_slides(pres, slide_index, target_index): + """Move a slide from one position to another.""" + slides = pres.slides._sldIdLst + + # Remove slide element from current position + slide_element = slides[slide_index] + slides.remove(slide_element) + + # Insert at target position + slides.insert(target_index, slide_element) + + +def rearrange_presentation(template_path, output_path, slide_sequence): + """ + Create a new presentation with slides from template in specified order. + + Args: + template_path: Path to template PPTX file + output_path: Path for output PPTX file + slide_sequence: List of slide indices (0-based) to include + """ + # Copy template to preserve dimensions and theme + if template_path != output_path: + shutil.copy2(template_path, output_path) + prs = Presentation(output_path) + else: + prs = Presentation(template_path) + + total_slides = len(prs.slides) + + # Validate indices + for idx in slide_sequence: + if idx < 0 or idx >= total_slides: + raise ValueError(f"Slide index {idx} out of range (0-{total_slides - 1})") + + # Track original slides and their duplicates + slide_map = [] # List of actual slide indices for final presentation + duplicated = {} # Track duplicates: original_idx -> [duplicate_indices] + + # Step 1: DUPLICATE repeated slides + print(f"Processing {len(slide_sequence)} slides from template...") + for i, template_idx in enumerate(slide_sequence): + if template_idx in duplicated and duplicated[template_idx]: + # Already duplicated this slide, use the duplicate + slide_map.append(duplicated[template_idx].pop(0)) + print(f" [{i}] Using duplicate of slide {template_idx}") + elif slide_sequence.count(template_idx) > 1 and template_idx not in duplicated: + # First occurrence of a repeated slide - create duplicates + slide_map.append(template_idx) + duplicates = [] + count = slide_sequence.count(template_idx) - 1 + print( + f" [{i}] Using original slide {template_idx}, creating {count} duplicate(s)" + ) + for _ in range(count): + duplicate_slide(prs, template_idx) + duplicates.append(len(prs.slides) - 1) + duplicated[template_idx] = duplicates + else: + # Unique slide or first occurrence already handled, use original + slide_map.append(template_idx) + print(f" [{i}] Using original slide {template_idx}") + + # Step 2: DELETE unwanted slides (work backwards) + slides_to_keep = set(slide_map) + print(f"\nDeleting {len(prs.slides) - len(slides_to_keep)} unused slides...") + for i in range(len(prs.slides) - 1, -1, -1): + if i not in slides_to_keep: + delete_slide(prs, i) + # Update slide_map indices after deletion + slide_map = [idx - 1 if idx > i else idx for idx in slide_map] + + # Step 3: REORDER to final sequence + print(f"Reordering {len(slide_map)} slides to final sequence...") + for target_pos in range(len(slide_map)): + # Find which slide should be at target_pos + current_pos = slide_map[target_pos] + if current_pos != target_pos: + reorder_slides(prs, current_pos, target_pos) + # Update slide_map: the move shifts other slides + for i in range(len(slide_map)): + if slide_map[i] > current_pos and slide_map[i] <= target_pos: + slide_map[i] -= 1 + elif slide_map[i] < current_pos and slide_map[i] >= target_pos: + slide_map[i] += 1 + slide_map[target_pos] = target_pos + + # Save the presentation + prs.save(output_path) + print(f"\nSaved rearranged presentation to: {output_path}") + print(f"Final presentation has {len(prs.slides)} slides") + + +if __name__ == "__main__": + main() diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/replace.py b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/replace.py new file mode 100755 index 00000000..8f7a8b1b --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/replace.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python3 +"""Apply text replacements to PowerPoint presentation. + +Usage: + python replace.py + +The replacements JSON should have the structure output by inventory.py. +ALL text shapes identified by inventory.py will have their text cleared +unless "paragraphs" is specified in the replacements for that shape. +""" + +import json +import sys +from pathlib import Path +from typing import Any, Dict, List + +from inventory import InventoryData, extract_text_inventory +from pptx import Presentation +from pptx.dml.color import RGBColor +from pptx.enum.dml import MSO_THEME_COLOR +from pptx.enum.text import PP_ALIGN +from pptx.oxml.xmlchemy import OxmlElement +from pptx.util import Pt + + +def clear_paragraph_bullets(paragraph): + """Clear bullet formatting from a paragraph.""" + pPr = paragraph._element.get_or_add_pPr() + + # Remove existing bullet elements + for child in list(pPr): + if ( + child.tag.endswith("buChar") + or child.tag.endswith("buNone") + or child.tag.endswith("buAutoNum") + or child.tag.endswith("buFont") + ): + pPr.remove(child) + + return pPr + + +def apply_paragraph_properties(paragraph, para_data: Dict[str, Any]): + """Apply formatting properties to a paragraph.""" + # Get the text but don't set it on paragraph directly yet + text = para_data.get("text", "") + + # Get or create paragraph properties + pPr = clear_paragraph_bullets(paragraph) + + # Handle bullet formatting + if para_data.get("bullet", False): + level = para_data.get("level", 0) + paragraph.level = level + + # Calculate font-proportional indentation + font_size = para_data.get("font_size", 18.0) + level_indent_emu = int((font_size * (1.6 + level * 1.6)) * 12700) + hanging_indent_emu = int(-font_size * 0.8 * 12700) + + # Set indentation + pPr.attrib["marL"] = str(level_indent_emu) + pPr.attrib["indent"] = str(hanging_indent_emu) + + # Add bullet character + buChar = OxmlElement("a:buChar") + buChar.set("char", "โ€ข") + pPr.append(buChar) + + # Default to left alignment for bullets if not specified + if "alignment" not in para_data: + paragraph.alignment = PP_ALIGN.LEFT + else: + # Remove indentation for non-bullet text + pPr.attrib["marL"] = "0" + pPr.attrib["indent"] = "0" + + # Add buNone element + buNone = OxmlElement("a:buNone") + pPr.insert(0, buNone) + + # Apply alignment + if "alignment" in para_data: + alignment_map = { + "LEFT": PP_ALIGN.LEFT, + "CENTER": PP_ALIGN.CENTER, + "RIGHT": PP_ALIGN.RIGHT, + "JUSTIFY": PP_ALIGN.JUSTIFY, + } + if para_data["alignment"] in alignment_map: + paragraph.alignment = alignment_map[para_data["alignment"]] + + # Apply spacing + if "space_before" in para_data: + paragraph.space_before = Pt(para_data["space_before"]) + if "space_after" in para_data: + paragraph.space_after = Pt(para_data["space_after"]) + if "line_spacing" in para_data: + paragraph.line_spacing = Pt(para_data["line_spacing"]) + + # Apply run-level formatting + if not paragraph.runs: + run = paragraph.add_run() + run.text = text + else: + run = paragraph.runs[0] + run.text = text + + # Apply font properties + apply_font_properties(run, para_data) + + +def apply_font_properties(run, para_data: Dict[str, Any]): + """Apply font properties to a text run.""" + if "bold" in para_data: + run.font.bold = para_data["bold"] + if "italic" in para_data: + run.font.italic = para_data["italic"] + if "underline" in para_data: + run.font.underline = para_data["underline"] + if "font_size" in para_data: + run.font.size = Pt(para_data["font_size"]) + if "font_name" in para_data: + run.font.name = para_data["font_name"] + + # Apply color - prefer RGB, fall back to theme_color + if "color" in para_data: + color_hex = para_data["color"].lstrip("#") + if len(color_hex) == 6: + r = int(color_hex[0:2], 16) + g = int(color_hex[2:4], 16) + b = int(color_hex[4:6], 16) + run.font.color.rgb = RGBColor(r, g, b) + elif "theme_color" in para_data: + # Get theme color by name (e.g., "DARK_1", "ACCENT_1") + theme_name = para_data["theme_color"] + try: + run.font.color.theme_color = getattr(MSO_THEME_COLOR, theme_name) + except AttributeError: + print(f" WARNING: Unknown theme color name '{theme_name}'") + + +def detect_frame_overflow(inventory: InventoryData) -> Dict[str, Dict[str, float]]: + """Detect text overflow in shapes (text exceeding shape bounds). + + Returns dict of slide_key -> shape_key -> overflow_inches. + Only includes shapes that have text overflow. + """ + overflow_map = {} + + for slide_key, shapes_dict in inventory.items(): + for shape_key, shape_data in shapes_dict.items(): + # Check for frame overflow (text exceeding shape bounds) + if shape_data.frame_overflow_bottom is not None: + if slide_key not in overflow_map: + overflow_map[slide_key] = {} + overflow_map[slide_key][shape_key] = shape_data.frame_overflow_bottom + + return overflow_map + + +def validate_replacements(inventory: InventoryData, replacements: Dict) -> List[str]: + """Validate that all shapes in replacements exist in inventory. + + Returns list of error messages. + """ + errors = [] + + for slide_key, shapes_data in replacements.items(): + if not slide_key.startswith("slide-"): + continue + + # Check if slide exists + if slide_key not in inventory: + errors.append(f"Slide '{slide_key}' not found in inventory") + continue + + # Check each shape + for shape_key in shapes_data.keys(): + if shape_key not in inventory[slide_key]: + # Find shapes without replacements defined and show their content + unused_with_content = [] + for k in inventory[slide_key].keys(): + if k not in shapes_data: + shape_data = inventory[slide_key][k] + # Get text from paragraphs as preview + paragraphs = shape_data.paragraphs + if paragraphs and paragraphs[0].text: + first_text = paragraphs[0].text[:50] + if len(paragraphs[0].text) > 50: + first_text += "..." + unused_with_content.append(f"{k} ('{first_text}')") + else: + unused_with_content.append(k) + + errors.append( + f"Shape '{shape_key}' not found on '{slide_key}'. " + f"Shapes without replacements: {', '.join(sorted(unused_with_content)) if unused_with_content else 'none'}" + ) + + return errors + + +def check_duplicate_keys(pairs): + """Check for duplicate keys when loading JSON.""" + result = {} + for key, value in pairs: + if key in result: + raise ValueError(f"Duplicate key found in JSON: '{key}'") + result[key] = value + return result + + +def apply_replacements(pptx_file: str, json_file: str, output_file: str): + """Apply text replacements from JSON to PowerPoint presentation.""" + + # Load presentation + prs = Presentation(pptx_file) + + # Get inventory of all text shapes (returns ShapeData objects) + # Pass prs to use same Presentation instance + inventory = extract_text_inventory(Path(pptx_file), prs) + + # Detect text overflow in original presentation + original_overflow = detect_frame_overflow(inventory) + + # Load replacement data with duplicate key detection + with open(json_file, "r") as f: + replacements = json.load(f, object_pairs_hook=check_duplicate_keys) + + # Validate replacements + errors = validate_replacements(inventory, replacements) + if errors: + print("ERROR: Invalid shapes in replacement JSON:") + for error in errors: + print(f" - {error}") + print("\nPlease check the inventory and update your replacement JSON.") + print( + "You can regenerate the inventory with: python inventory.py " + ) + raise ValueError(f"Found {len(errors)} validation error(s)") + + # Track statistics + shapes_processed = 0 + shapes_cleared = 0 + shapes_replaced = 0 + + # Process each slide from inventory + for slide_key, shapes_dict in inventory.items(): + if not slide_key.startswith("slide-"): + continue + + slide_index = int(slide_key.split("-")[1]) + + if slide_index >= len(prs.slides): + print(f"Warning: Slide {slide_index} not found") + continue + + # Process each shape from inventory + for shape_key, shape_data in shapes_dict.items(): + shapes_processed += 1 + + # Get the shape directly from ShapeData + shape = shape_data.shape + if not shape: + print(f"Warning: {shape_key} has no shape reference") + continue + + # ShapeData already validates text_frame in __init__ + text_frame = shape.text_frame # type: ignore + + text_frame.clear() # type: ignore + shapes_cleared += 1 + + # Check for replacement paragraphs + replacement_shape_data = replacements.get(slide_key, {}).get(shape_key, {}) + if "paragraphs" not in replacement_shape_data: + continue + + shapes_replaced += 1 + + # Add replacement paragraphs + for i, para_data in enumerate(replacement_shape_data["paragraphs"]): + if i == 0: + p = text_frame.paragraphs[0] # type: ignore + else: + p = text_frame.add_paragraph() # type: ignore + + apply_paragraph_properties(p, para_data) + + # Check for issues after replacements + # Save to a temporary file and reload to avoid modifying the presentation during inventory + # (extract_text_inventory accesses font.color which adds empty elements) + import tempfile + + with tempfile.NamedTemporaryFile(suffix=".pptx", delete=False) as tmp: + tmp_path = Path(tmp.name) + prs.save(str(tmp_path)) + + try: + updated_inventory = extract_text_inventory(tmp_path) + updated_overflow = detect_frame_overflow(updated_inventory) + finally: + tmp_path.unlink() # Clean up temp file + + # Check if any text overflow got worse + overflow_errors = [] + for slide_key, shape_overflows in updated_overflow.items(): + for shape_key, new_overflow in shape_overflows.items(): + # Get original overflow (0 if there was no overflow before) + original = original_overflow.get(slide_key, {}).get(shape_key, 0.0) + + # Error if overflow increased + if new_overflow > original + 0.01: # Small tolerance for rounding + increase = new_overflow - original + overflow_errors.append( + f'{slide_key}/{shape_key}: overflow worsened by {increase:.2f}" ' + f'(was {original:.2f}", now {new_overflow:.2f}")' + ) + + # Collect warnings from updated shapes + warnings = [] + for slide_key, shapes_dict in updated_inventory.items(): + for shape_key, shape_data in shapes_dict.items(): + if shape_data.warnings: + for warning in shape_data.warnings: + warnings.append(f"{slide_key}/{shape_key}: {warning}") + + # Fail if there are any issues + if overflow_errors or warnings: + print("\nERROR: Issues detected in replacement output:") + if overflow_errors: + print("\nText overflow worsened:") + for error in overflow_errors: + print(f" - {error}") + if warnings: + print("\nFormatting warnings:") + for warning in warnings: + print(f" - {warning}") + print("\nPlease fix these issues before saving.") + raise ValueError( + f"Found {len(overflow_errors)} overflow error(s) and {len(warnings)} warning(s)" + ) + + # Save the presentation + prs.save(output_file) + + # Report results + print(f"Saved updated presentation to: {output_file}") + print(f"Processed {len(prs.slides)} slides") + print(f" - Shapes processed: {shapes_processed}") + print(f" - Shapes cleared: {shapes_cleared}") + print(f" - Shapes replaced: {shapes_replaced}") + + +def main(): + """Main entry point for command-line usage.""" + if len(sys.argv) != 4: + print(__doc__) + sys.exit(1) + + input_pptx = Path(sys.argv[1]) + replacements_json = Path(sys.argv[2]) + output_pptx = Path(sys.argv[3]) + + if not input_pptx.exists(): + print(f"Error: Input file '{input_pptx}' not found") + sys.exit(1) + + if not replacements_json.exists(): + print(f"Error: Replacements JSON file '{replacements_json}' not found") + sys.exit(1) + + try: + apply_replacements(str(input_pptx), str(replacements_json), str(output_pptx)) + except Exception as e: + print(f"Error applying replacements: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/thumbnail.py b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/thumbnail.py new file mode 100755 index 00000000..5c7fdf19 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/pptx-official/scripts/thumbnail.py @@ -0,0 +1,450 @@ +#!/usr/bin/env python3 +""" +Create thumbnail grids from PowerPoint presentation slides. + +Creates a grid layout of slide thumbnails with configurable columns (max 6). +Each grid contains up to colsร—(cols+1) images. For presentations with more +slides, multiple numbered grid files are created automatically. + +The program outputs the names of all files created. + +Output: +- Single grid: {prefix}.jpg (if slides fit in one grid) +- Multiple grids: {prefix}-1.jpg, {prefix}-2.jpg, etc. + +Grid limits by column count: +- 3 cols: max 12 slides per grid (3ร—4) +- 4 cols: max 20 slides per grid (4ร—5) +- 5 cols: max 30 slides per grid (5ร—6) [default] +- 6 cols: max 42 slides per grid (6ร—7) + +Usage: + python thumbnail.py input.pptx [output_prefix] [--cols N] [--outline-placeholders] + +Examples: + python thumbnail.py presentation.pptx + # Creates: thumbnails.jpg (using default prefix) + # Outputs: + # Created 1 grid(s): + # - thumbnails.jpg + + python thumbnail.py large-deck.pptx grid --cols 4 + # Creates: grid-1.jpg, grid-2.jpg, grid-3.jpg + # Outputs: + # Created 3 grid(s): + # - grid-1.jpg + # - grid-2.jpg + # - grid-3.jpg + + python thumbnail.py template.pptx analysis --outline-placeholders + # Creates thumbnail grids with red outlines around text placeholders +""" + +import argparse +import subprocess +import sys +import tempfile +from pathlib import Path + +from inventory import extract_text_inventory +from PIL import Image, ImageDraw, ImageFont +from pptx import Presentation + +# Constants +THUMBNAIL_WIDTH = 300 # Fixed thumbnail width in pixels +CONVERSION_DPI = 100 # DPI for PDF to image conversion +MAX_COLS = 6 # Maximum number of columns +DEFAULT_COLS = 5 # Default number of columns +JPEG_QUALITY = 95 # JPEG compression quality + +# Grid layout constants +GRID_PADDING = 20 # Padding between thumbnails +BORDER_WIDTH = 2 # Border width around thumbnails +FONT_SIZE_RATIO = 0.12 # Font size as fraction of thumbnail width +LABEL_PADDING_RATIO = 0.4 # Label padding as fraction of font size + + +def main(): + parser = argparse.ArgumentParser( + description="Create thumbnail grids from PowerPoint slides." + ) + parser.add_argument("input", help="Input PowerPoint file (.pptx)") + parser.add_argument( + "output_prefix", + nargs="?", + default="thumbnails", + help="Output prefix for image files (default: thumbnails, will create prefix.jpg or prefix-N.jpg)", + ) + parser.add_argument( + "--cols", + type=int, + default=DEFAULT_COLS, + help=f"Number of columns (default: {DEFAULT_COLS}, max: {MAX_COLS})", + ) + parser.add_argument( + "--outline-placeholders", + action="store_true", + help="Outline text placeholders with a colored border", + ) + + args = parser.parse_args() + + # Validate columns + cols = min(args.cols, MAX_COLS) + if args.cols > MAX_COLS: + print(f"Warning: Columns limited to {MAX_COLS} (requested {args.cols})") + + # Validate input + input_path = Path(args.input) + if not input_path.exists() or input_path.suffix.lower() != ".pptx": + print(f"Error: Invalid PowerPoint file: {args.input}") + sys.exit(1) + + # Construct output path (always JPG) + output_path = Path(f"{args.output_prefix}.jpg") + + print(f"Processing: {args.input}") + + try: + with tempfile.TemporaryDirectory() as temp_dir: + # Get placeholder regions if outlining is enabled + placeholder_regions = None + slide_dimensions = None + if args.outline_placeholders: + print("Extracting placeholder regions...") + placeholder_regions, slide_dimensions = get_placeholder_regions( + input_path + ) + if placeholder_regions: + print(f"Found placeholders on {len(placeholder_regions)} slides") + + # Convert slides to images + slide_images = convert_to_images(input_path, Path(temp_dir), CONVERSION_DPI) + if not slide_images: + print("Error: No slides found") + sys.exit(1) + + print(f"Found {len(slide_images)} slides") + + # Create grids (max colsร—(cols+1) images per grid) + grid_files = create_grids( + slide_images, + cols, + THUMBNAIL_WIDTH, + output_path, + placeholder_regions, + slide_dimensions, + ) + + # Print saved files + print(f"Created {len(grid_files)} grid(s):") + for grid_file in grid_files: + print(f" - {grid_file}") + + except Exception as e: + print(f"Error: {e}") + sys.exit(1) + + +def create_hidden_slide_placeholder(size): + """Create placeholder image for hidden slides.""" + img = Image.new("RGB", size, color="#F0F0F0") + draw = ImageDraw.Draw(img) + line_width = max(5, min(size) // 100) + draw.line([(0, 0), size], fill="#CCCCCC", width=line_width) + draw.line([(size[0], 0), (0, size[1])], fill="#CCCCCC", width=line_width) + return img + + +def get_placeholder_regions(pptx_path): + """Extract ALL text regions from the presentation. + + Returns a tuple of (placeholder_regions, slide_dimensions). + text_regions is a dict mapping slide indices to lists of text regions. + Each region is a dict with 'left', 'top', 'width', 'height' in inches. + slide_dimensions is a tuple of (width_inches, height_inches). + """ + prs = Presentation(str(pptx_path)) + inventory = extract_text_inventory(pptx_path, prs) + placeholder_regions = {} + + # Get actual slide dimensions in inches (EMU to inches conversion) + slide_width_inches = (prs.slide_width or 9144000) / 914400.0 + slide_height_inches = (prs.slide_height or 5143500) / 914400.0 + + for slide_key, shapes in inventory.items(): + # Extract slide index from "slide-N" format + slide_idx = int(slide_key.split("-")[1]) + regions = [] + + for shape_key, shape_data in shapes.items(): + # The inventory only contains shapes with text, so all shapes should be highlighted + regions.append( + { + "left": shape_data.left, + "top": shape_data.top, + "width": shape_data.width, + "height": shape_data.height, + } + ) + + if regions: + placeholder_regions[slide_idx] = regions + + return placeholder_regions, (slide_width_inches, slide_height_inches) + + +def convert_to_images(pptx_path, temp_dir, dpi): + """Convert PowerPoint to images via PDF, handling hidden slides.""" + # Detect hidden slides + print("Analyzing presentation...") + prs = Presentation(str(pptx_path)) + total_slides = len(prs.slides) + + # Find hidden slides (1-based indexing for display) + hidden_slides = { + idx + 1 + for idx, slide in enumerate(prs.slides) + if slide.element.get("show") == "0" + } + + print(f"Total slides: {total_slides}") + if hidden_slides: + print(f"Hidden slides: {sorted(hidden_slides)}") + + pdf_path = temp_dir / f"{pptx_path.stem}.pdf" + + # Convert to PDF + print("Converting to PDF...") + result = subprocess.run( + [ + "soffice", + "--headless", + "--convert-to", + "pdf", + "--outdir", + str(temp_dir), + str(pptx_path), + ], + capture_output=True, + text=True, + ) + if result.returncode != 0 or not pdf_path.exists(): + raise RuntimeError("PDF conversion failed") + + # Convert PDF to images + print(f"Converting to images at {dpi} DPI...") + result = subprocess.run( + ["pdftoppm", "-jpeg", "-r", str(dpi), str(pdf_path), str(temp_dir / "slide")], + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise RuntimeError("Image conversion failed") + + visible_images = sorted(temp_dir.glob("slide-*.jpg")) + + # Create full list with placeholders for hidden slides + all_images = [] + visible_idx = 0 + + # Get placeholder dimensions from first visible slide + if visible_images: + with Image.open(visible_images[0]) as img: + placeholder_size = img.size + else: + placeholder_size = (1920, 1080) + + for slide_num in range(1, total_slides + 1): + if slide_num in hidden_slides: + # Create placeholder image for hidden slide + placeholder_path = temp_dir / f"hidden-{slide_num:03d}.jpg" + placeholder_img = create_hidden_slide_placeholder(placeholder_size) + placeholder_img.save(placeholder_path, "JPEG") + all_images.append(placeholder_path) + else: + # Use the actual visible slide image + if visible_idx < len(visible_images): + all_images.append(visible_images[visible_idx]) + visible_idx += 1 + + return all_images + + +def create_grids( + image_paths, + cols, + width, + output_path, + placeholder_regions=None, + slide_dimensions=None, +): + """Create multiple thumbnail grids from slide images, max colsร—(cols+1) images per grid.""" + # Maximum images per grid is cols ร— (cols + 1) for better proportions + max_images_per_grid = cols * (cols + 1) + grid_files = [] + + print( + f"Creating grids with {cols} columns (max {max_images_per_grid} images per grid)" + ) + + # Split images into chunks + for chunk_idx, start_idx in enumerate( + range(0, len(image_paths), max_images_per_grid) + ): + end_idx = min(start_idx + max_images_per_grid, len(image_paths)) + chunk_images = image_paths[start_idx:end_idx] + + # Create grid for this chunk + grid = create_grid( + chunk_images, cols, width, start_idx, placeholder_regions, slide_dimensions + ) + + # Generate output filename + if len(image_paths) <= max_images_per_grid: + # Single grid - use base filename without suffix + grid_filename = output_path + else: + # Multiple grids - insert index before extension with dash + stem = output_path.stem + suffix = output_path.suffix + grid_filename = output_path.parent / f"{stem}-{chunk_idx + 1}{suffix}" + + # Save grid + grid_filename.parent.mkdir(parents=True, exist_ok=True) + grid.save(str(grid_filename), quality=JPEG_QUALITY) + grid_files.append(str(grid_filename)) + + return grid_files + + +def create_grid( + image_paths, + cols, + width, + start_slide_num=0, + placeholder_regions=None, + slide_dimensions=None, +): + """Create thumbnail grid from slide images with optional placeholder outlining.""" + font_size = int(width * FONT_SIZE_RATIO) + label_padding = int(font_size * LABEL_PADDING_RATIO) + + # Get dimensions + with Image.open(image_paths[0]) as img: + aspect = img.height / img.width + height = int(width * aspect) + + # Calculate grid size + rows = (len(image_paths) + cols - 1) // cols + grid_w = cols * width + (cols + 1) * GRID_PADDING + grid_h = rows * (height + font_size + label_padding * 2) + (rows + 1) * GRID_PADDING + + # Create grid + grid = Image.new("RGB", (grid_w, grid_h), "white") + draw = ImageDraw.Draw(grid) + + # Load font with size based on thumbnail width + try: + # Use Pillow's default font with size + font = ImageFont.load_default(size=font_size) + except Exception: + # Fall back to basic default font if size parameter not supported + font = ImageFont.load_default() + + # Place thumbnails + for i, img_path in enumerate(image_paths): + row, col = i // cols, i % cols + x = col * width + (col + 1) * GRID_PADDING + y_base = ( + row * (height + font_size + label_padding * 2) + (row + 1) * GRID_PADDING + ) + + # Add label with actual slide number + label = f"{start_slide_num + i}" + bbox = draw.textbbox((0, 0), label, font=font) + text_w = bbox[2] - bbox[0] + draw.text( + (x + (width - text_w) // 2, y_base + label_padding), + label, + fill="black", + font=font, + ) + + # Add thumbnail below label with proportional spacing + y_thumbnail = y_base + label_padding + font_size + label_padding + + with Image.open(img_path) as img: + # Get original dimensions before thumbnail + orig_w, orig_h = img.size + + # Apply placeholder outlines if enabled + if placeholder_regions and (start_slide_num + i) in placeholder_regions: + # Convert to RGBA for transparency support + if img.mode != "RGBA": + img = img.convert("RGBA") + + # Get the regions for this slide + regions = placeholder_regions[start_slide_num + i] + + # Calculate scale factors using actual slide dimensions + if slide_dimensions: + slide_width_inches, slide_height_inches = slide_dimensions + else: + # Fallback: estimate from image size at CONVERSION_DPI + slide_width_inches = orig_w / CONVERSION_DPI + slide_height_inches = orig_h / CONVERSION_DPI + + x_scale = orig_w / slide_width_inches + y_scale = orig_h / slide_height_inches + + # Create a highlight overlay + overlay = Image.new("RGBA", img.size, (255, 255, 255, 0)) + overlay_draw = ImageDraw.Draw(overlay) + + # Highlight each placeholder region + for region in regions: + # Convert from inches to pixels in the original image + px_left = int(region["left"] * x_scale) + px_top = int(region["top"] * y_scale) + px_width = int(region["width"] * x_scale) + px_height = int(region["height"] * y_scale) + + # Draw highlight outline with red color and thick stroke + # Using a bright red outline instead of fill + stroke_width = max( + 5, min(orig_w, orig_h) // 150 + ) # Thicker proportional stroke width + overlay_draw.rectangle( + [(px_left, px_top), (px_left + px_width, px_top + px_height)], + outline=(255, 0, 0, 255), # Bright red, fully opaque + width=stroke_width, + ) + + # Composite the overlay onto the image using alpha blending + img = Image.alpha_composite(img, overlay) + # Convert back to RGB for JPEG saving + img = img.convert("RGB") + + img.thumbnail((width, height), Image.Resampling.LANCZOS) + w, h = img.size + tx = x + (width - w) // 2 + ty = y_thumbnail + (height - h) // 2 + grid.paste(img, (tx, ty)) + + # Add border + if BORDER_WIDTH > 0: + draw.rectangle( + [ + (tx - BORDER_WIDTH, ty - BORDER_WIDTH), + (tx + w + BORDER_WIDTH - 1, ty + h + BORDER_WIDTH - 1), + ], + outline="gray", + width=BORDER_WIDTH, + ) + + return grid + + +if __name__ == "__main__": + main() diff --git a/plugins/antigravity-bundle-documents-presentations/skills/xlsx-official/LICENSE.txt b/plugins/antigravity-bundle-documents-presentations/skills/xlsx-official/LICENSE.txt new file mode 100644 index 00000000..c55ab422 --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/xlsx-official/LICENSE.txt @@ -0,0 +1,30 @@ +ยฉ 2025 Anthropic, PBC. All rights reserved. + +LICENSE: Use of these materials (including all code, prompts, assets, files, +and other components of this Skill) is governed by your agreement with +Anthropic regarding use of Anthropic's services. If no separate agreement +exists, use is governed by Anthropic's Consumer Terms of Service or +Commercial Terms of Service, as applicable: +https://www.anthropic.com/legal/consumer-terms +https://www.anthropic.com/legal/commercial-terms +Your applicable agreement is referred to as the "Agreement." "Services" are +as defined in the Agreement. + +ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the +contrary, users may not: + +- Extract these materials from the Services or retain copies of these + materials outside the Services +- Reproduce or copy these materials, except for temporary copies created + automatically during authorized use of the Services +- Create derivative works based on these materials +- Distribute, sublicense, or transfer these materials to any third party +- Make, offer to sell, sell, or import any inventions embodied in these + materials +- Reverse engineer, decompile, or disassemble these materials + +The receipt, viewing, or possession of these materials does not convey or +imply any license or right beyond those expressly granted above. + +Anthropic retains all right, title, and interest in these materials, +including all copyrights, patents, and other intellectual property rights. diff --git a/plugins/antigravity-bundle-documents-presentations/skills/xlsx-official/SKILL.md b/plugins/antigravity-bundle-documents-presentations/skills/xlsx-official/SKILL.md new file mode 100644 index 00000000..0f26f3ab --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/xlsx-official/SKILL.md @@ -0,0 +1,294 @@ +--- +name: xlsx-official +description: "Unless otherwise stated by the user or existing template" +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Requirements for Outputs + +## All Excel files + +### Zero Formula Errors +- Every Excel model MUST be delivered with ZERO formula errors (#REF!, #DIV/0!, #VALUE!, #N/A, #NAME?) + +### Preserve Existing Templates (when updating templates) +- Study and EXACTLY match existing format, style, and conventions when modifying files +- Never impose standardized formatting on files with established patterns +- Existing template conventions ALWAYS override these guidelines + +## Financial models + +### Color Coding Standards +Unless otherwise stated by the user or existing template + +#### Industry-Standard Color Conventions +- **Blue text (RGB: 0,0,255)**: Hardcoded inputs, and numbers users will change for scenarios +- **Black text (RGB: 0,0,0)**: ALL formulas and calculations +- **Green text (RGB: 0,128,0)**: Links pulling from other worksheets within same workbook +- **Red text (RGB: 255,0,0)**: External links to other files +- **Yellow background (RGB: 255,255,0)**: Key assumptions needing attention or cells that need to be updated + +### Number Formatting Standards + +#### Required Format Rules +- **Years**: Format as text strings (e.g., "2024" not "2,024") +- **Currency**: Use $#,##0 format; ALWAYS specify units in headers ("Revenue ($mm)") +- **Zeros**: Use number formatting to make all zeros "-", including percentages (e.g., "$#,##0;($#,##0);-") +- **Percentages**: Default to 0.0% format (one decimal) +- **Multiples**: Format as 0.0x for valuation multiples (EV/EBITDA, P/E) +- **Negative numbers**: Use parentheses (123) not minus -123 + +### Formula Construction Rules + +#### Assumptions Placement +- Place ALL assumptions (growth rates, margins, multiples, etc.) in separate assumption cells +- Use cell references instead of hardcoded values in formulas +- Example: Use =B5*(1+$B$6) instead of =B5*1.05 + +#### Formula Error Prevention +- Verify all cell references are correct +- Check for off-by-one errors in ranges +- Ensure consistent formulas across all projection periods +- Test with edge cases (zero values, negative numbers) +- Verify no unintended circular references + +#### Documentation Requirements for Hardcodes +- Comment or in cells beside (if end of table). Format: "Source: [System/Document], [Date], [Specific Reference], [URL if applicable]" +- Examples: + - "Source: Company 10-K, FY2024, Page 45, Revenue Note, [SEC EDGAR URL]" + - "Source: Company 10-Q, Q2 2025, Exhibit 99.1, [SEC EDGAR URL]" + - "Source: Bloomberg Terminal, 8/15/2025, AAPL US Equity" + - "Source: FactSet, 8/20/2025, Consensus Estimates Screen" + +# XLSX creation, editing, and analysis + +## Overview + +A user may ask you to create, edit, or analyze the contents of an .xlsx file. You have different tools and workflows available for different tasks. + +## Important Requirements + +**LibreOffice Required for Formula Recalculation**: You can assume LibreOffice is installed for recalculating formula values using the `recalc.py` script. The script automatically configures LibreOffice on first run + +## Reading and analyzing data + +### Data analysis with pandas +For data analysis, visualization, and basic operations, use **pandas** which provides powerful data manipulation capabilities: + +```python +import pandas as pd + +# Read Excel +df = pd.read_excel('file.xlsx') # Default: first sheet +all_sheets = pd.read_excel('file.xlsx', sheet_name=None) # All sheets as dict + +# Analyze +df.head() # Preview data +df.info() # Column info +df.describe() # Statistics + +# Write Excel +df.to_excel('output.xlsx', index=False) +``` + +## Excel File Workflows + +## CRITICAL: Use Formulas, Not Hardcoded Values + +**Always use Excel formulas instead of calculating values in Python and hardcoding them.** This ensures the spreadsheet remains dynamic and updateable. + +### โŒ WRONG - Hardcoding Calculated Values +```python +# Bad: Calculating in Python and hardcoding result +total = df['Sales'].sum() +sheet['B10'] = total # Hardcodes 5000 + +# Bad: Computing growth rate in Python +growth = (df.iloc[-1]['Revenue'] - df.iloc[0]['Revenue']) / df.iloc[0]['Revenue'] +sheet['C5'] = growth # Hardcodes 0.15 + +# Bad: Python calculation for average +avg = sum(values) / len(values) +sheet['D20'] = avg # Hardcodes 42.5 +``` + +### โœ… CORRECT - Using Excel Formulas +```python +# Good: Let Excel calculate the sum +sheet['B10'] = '=SUM(B2:B9)' + +# Good: Growth rate as Excel formula +sheet['C5'] = '=(C4-C2)/C2' + +# Good: Average using Excel function +sheet['D20'] = '=AVERAGE(D2:D19)' +``` + +This applies to ALL calculations - totals, percentages, ratios, differences, etc. The spreadsheet should be able to recalculate when source data changes. + +## Common Workflow +1. **Choose tool**: pandas for data, openpyxl for formulas/formatting +2. **Create/Load**: Create new workbook or load existing file +3. **Modify**: Add/edit data, formulas, and formatting +4. **Save**: Write to file +5. **Recalculate formulas (MANDATORY IF USING FORMULAS)**: Use the recalc.py script + ```bash + python recalc.py output.xlsx + ``` +6. **Verify and fix any errors**: + - The script returns JSON with error details + - If `status` is `errors_found`, check `error_summary` for specific error types and locations + - Fix the identified errors and recalculate again + - Common errors to fix: + - `#REF!`: Invalid cell references + - `#DIV/0!`: Division by zero + - `#VALUE!`: Wrong data type in formula + - `#NAME?`: Unrecognized formula name + +### Creating new Excel files + +```python +# Using openpyxl for formulas and formatting +from openpyxl import Workbook +from openpyxl.styles import Font, PatternFill, Alignment + +wb = Workbook() +sheet = wb.active + +# Add data +sheet['A1'] = 'Hello' +sheet['B1'] = 'World' +sheet.append(['Row', 'of', 'data']) + +# Add formula +sheet['B2'] = '=SUM(A1:A10)' + +# Formatting +sheet['A1'].font = Font(bold=True, color='FF0000') +sheet['A1'].fill = PatternFill('solid', start_color='FFFF00') +sheet['A1'].alignment = Alignment(horizontal='center') + +# Column width +sheet.column_dimensions['A'].width = 20 + +wb.save('output.xlsx') +``` + +### Editing existing Excel files + +```python +# Using openpyxl to preserve formulas and formatting +from openpyxl import load_workbook + +# Load existing file +wb = load_workbook('existing.xlsx') +sheet = wb.active # or wb['SheetName'] for specific sheet + +# Working with multiple sheets +for sheet_name in wb.sheetnames: + sheet = wb[sheet_name] + print(f"Sheet: {sheet_name}") + +# Modify cells +sheet['A1'] = 'New Value' +sheet.insert_rows(2) # Insert row at position 2 +sheet.delete_cols(3) # Delete column 3 + +# Add new sheet +new_sheet = wb.create_sheet('NewSheet') +new_sheet['A1'] = 'Data' + +wb.save('modified.xlsx') +``` + +## Recalculating formulas + +Excel files created or modified by openpyxl contain formulas as strings but not calculated values. Use the provided `recalc.py` script to recalculate formulas: + +```bash +python recalc.py [timeout_seconds] +``` + +Example: +```bash +python recalc.py output.xlsx 30 +``` + +The script: +- Automatically sets up LibreOffice macro on first run +- Recalculates all formulas in all sheets +- Scans ALL cells for Excel errors (#REF!, #DIV/0!, etc.) +- Returns JSON with detailed error locations and counts +- Works on both Linux and macOS + +## Formula Verification Checklist + +Quick checks to ensure formulas work correctly: + +### Essential Verification +- [ ] **Test 2-3 sample references**: Verify they pull correct values before building full model +- [ ] **Column mapping**: Confirm Excel columns match (e.g., column 64 = BL, not BK) +- [ ] **Row offset**: Remember Excel rows are 1-indexed (DataFrame row 5 = Excel row 6) + +### Common Pitfalls +- [ ] **NaN handling**: Check for null values with `pd.notna()` +- [ ] **Far-right columns**: FY data often in columns 50+ +- [ ] **Multiple matches**: Search all occurrences, not just first +- [ ] **Division by zero**: Check denominators before using `/` in formulas (#DIV/0!) +- [ ] **Wrong references**: Verify all cell references point to intended cells (#REF!) +- [ ] **Cross-sheet references**: Use correct format (Sheet1!A1) for linking sheets + +### Formula Testing Strategy +- [ ] **Start small**: Test formulas on 2-3 cells before applying broadly +- [ ] **Verify dependencies**: Check all cells referenced in formulas exist +- [ ] **Test edge cases**: Include zero, negative, and very large values + +### Interpreting recalc.py Output +The script returns JSON with error details: +```json +{ + "status": "success", // or "errors_found" + "total_errors": 0, // Total error count + "total_formulas": 42, // Number of formulas in file + "error_summary": { // Only present if errors found + "#REF!": { + "count": 2, + "locations": ["Sheet1!B5", "Sheet1!C10"] + } + } +} +``` + +## Best Practices + +### Library Selection +- **pandas**: Best for data analysis, bulk operations, and simple data export +- **openpyxl**: Best for complex formatting, formulas, and Excel-specific features + +### Working with openpyxl +- Cell indices are 1-based (row=1, column=1 refers to cell A1) +- Use `data_only=True` to read calculated values: `load_workbook('file.xlsx', data_only=True)` +- **Warning**: If opened with `data_only=True` and saved, formulas are replaced with values and permanently lost +- For large files: Use `read_only=True` for reading or `write_only=True` for writing +- Formulas are preserved but not evaluated - use recalc.py to update values + +### Working with pandas +- Specify data types to avoid inference issues: `pd.read_excel('file.xlsx', dtype={'id': str})` +- For large files, read specific columns: `pd.read_excel('file.xlsx', usecols=['A', 'C', 'E'])` +- Handle dates properly: `pd.read_excel('file.xlsx', parse_dates=['date_column'])` + +## Code Style Guidelines +**IMPORTANT**: When generating Python code for Excel operations: +- Write minimal, concise Python code without unnecessary comments +- Avoid verbose variable names and redundant operations +- Avoid unnecessary print statements + +**For Excel files themselves**: +- Add comments to cells with complex formulas or important assumptions +- Document data sources for hardcoded values +- Include notes for key calculations and model sections + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-documents-presentations/skills/xlsx-official/recalc.py b/plugins/antigravity-bundle-documents-presentations/skills/xlsx-official/recalc.py new file mode 100644 index 00000000..102e157b --- /dev/null +++ b/plugins/antigravity-bundle-documents-presentations/skills/xlsx-official/recalc.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +""" +Excel Formula Recalculation Script +Recalculates all formulas in an Excel file using LibreOffice +""" + +import json +import sys +import subprocess +import os +import platform +from pathlib import Path +from openpyxl import load_workbook + + +def setup_libreoffice_macro(): + """Setup LibreOffice macro for recalculation if not already configured""" + if platform.system() == 'Darwin': + macro_dir = os.path.expanduser('~/Library/Application Support/LibreOffice/4/user/basic/Standard') + else: + macro_dir = os.path.expanduser('~/.config/libreoffice/4/user/basic/Standard') + + macro_file = os.path.join(macro_dir, 'Module1.xba') + + if os.path.exists(macro_file): + with open(macro_file, 'r') as f: + if 'RecalculateAndSave' in f.read(): + return True + + if not os.path.exists(macro_dir): + subprocess.run(['soffice', '--headless', '--terminate_after_init'], + capture_output=True, timeout=10) + os.makedirs(macro_dir, exist_ok=True) + + macro_content = ''' + + + Sub RecalculateAndSave() + ThisComponent.calculateAll() + ThisComponent.store() + ThisComponent.close(True) + End Sub +''' + + try: + with open(macro_file, 'w') as f: + f.write(macro_content) + return True + except Exception: + return False + + +def recalc(filename, timeout=30): + """ + Recalculate formulas in Excel file and report any errors + + Args: + filename: Path to Excel file + timeout: Maximum time to wait for recalculation (seconds) + + Returns: + dict with error locations and counts + """ + if not Path(filename).exists(): + return {'error': f'File {filename} does not exist'} + + abs_path = str(Path(filename).absolute()) + + if not setup_libreoffice_macro(): + return {'error': 'Failed to setup LibreOffice macro'} + + cmd = [ + 'soffice', '--headless', '--norestore', + 'vnd.sun.star.script:Standard.Module1.RecalculateAndSave?language=Basic&location=application', + abs_path + ] + + # Handle timeout command differences between Linux and macOS + if platform.system() != 'Windows': + timeout_cmd = 'timeout' if platform.system() == 'Linux' else None + if platform.system() == 'Darwin': + # Check if gtimeout is available on macOS + try: + subprocess.run(['gtimeout', '--version'], capture_output=True, timeout=1, check=False) + timeout_cmd = 'gtimeout' + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + + if timeout_cmd: + cmd = [timeout_cmd, str(timeout)] + cmd + + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode != 0 and result.returncode != 124: # 124 is timeout exit code + error_msg = result.stderr or 'Unknown error during recalculation' + if 'Module1' in error_msg or 'RecalculateAndSave' not in error_msg: + return {'error': 'LibreOffice macro not configured properly'} + else: + return {'error': error_msg} + + # Check for Excel errors in the recalculated file - scan ALL cells + try: + wb = load_workbook(filename, data_only=True) + + excel_errors = ['#VALUE!', '#DIV/0!', '#REF!', '#NAME?', '#NULL!', '#NUM!', '#N/A'] + error_details = {err: [] for err in excel_errors} + total_errors = 0 + + for sheet_name in wb.sheetnames: + ws = wb[sheet_name] + # Check ALL rows and columns - no limits + for row in ws.iter_rows(): + for cell in row: + if cell.value is not None and isinstance(cell.value, str): + for err in excel_errors: + if err in cell.value: + location = f"{sheet_name}!{cell.coordinate}" + error_details[err].append(location) + total_errors += 1 + break + + wb.close() + + # Build result summary + result = { + 'status': 'success' if total_errors == 0 else 'errors_found', + 'total_errors': total_errors, + 'error_summary': {} + } + + # Add non-empty error categories + for err_type, locations in error_details.items(): + if locations: + result['error_summary'][err_type] = { + 'count': len(locations), + 'locations': locations[:20] # Show up to 20 locations + } + + # Add formula count for context - also check ALL cells + wb_formulas = load_workbook(filename, data_only=False) + formula_count = 0 + for sheet_name in wb_formulas.sheetnames: + ws = wb_formulas[sheet_name] + for row in ws.iter_rows(): + for cell in row: + if cell.value and isinstance(cell.value, str) and cell.value.startswith('='): + formula_count += 1 + wb_formulas.close() + + result['total_formulas'] = formula_count + + return result + + except Exception as e: + return {'error': str(e)} + + +def main(): + if len(sys.argv) < 2: + print("Usage: python recalc.py [timeout_seconds]") + print("\nRecalculates all formulas in an Excel file using LibreOffice") + print("\nReturns JSON with error details:") + print(" - status: 'success' or 'errors_found'") + print(" - total_errors: Total number of Excel errors found") + print(" - total_formulas: Number of formulas in the file") + print(" - error_summary: Breakdown by error type with locations") + print(" - #VALUE!, #DIV/0!, #REF!, #NAME?, #NULL!, #NUM!, #N/A") + sys.exit(1) + + filename = sys.argv[1] + timeout = int(sys.argv[2]) if len(sys.argv) > 2 else 30 + + result = recalc(filename, timeout) + print(json.dumps(result, indent=2)) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/plugins/antigravity-bundle-essentials/.codex-plugin/plugin.json b/plugins/antigravity-bundle-essentials/.codex-plugin/plugin.json new file mode 100644 index 00000000..a1fbe954 --- /dev/null +++ b/plugins/antigravity-bundle-essentials/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-essentials", + "version": "8.10.0", + "description": "Install the \"Essentials\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "essentials", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Essentials", + "shortDescription": "Essentials & Core ยท 5 curated skills", + "longDescription": "For everyone. Install these first. Covers Concise Planning, Lint And Validate, and 3 more skills.", + "developerName": "sickn33 and contributors", + "category": "Essentials & Core", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-essentials/skills/concise-planning/SKILL.md b/plugins/antigravity-bundle-essentials/skills/concise-planning/SKILL.md new file mode 100644 index 00000000..e822f00b --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/concise-planning/SKILL.md @@ -0,0 +1,68 @@ +--- +name: concise-planning +description: "Use when a user asks for a plan for a coding task, to generate a clear, actionable, and atomic checklist." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Concise Planning + +## Goal + +Turn a user request into a **single, actionable plan** with atomic steps. + +## Workflow + +### 1. Scan Context + +- Read `README.md`, docs, and relevant code files. +- Identify constraints (language, frameworks, tests). + +### 2. Minimal Interaction + +- Ask **at most 1โ€“2 questions** and only if truly blocking. +- Make reasonable assumptions for non-blocking unknowns. + +### 3. Generate Plan + +Use the following structure: + +- **Approach**: 1-3 sentences on what and why. +- **Scope**: Bullet points for "In" and "Out". +- **Action Items**: A list of 6-10 atomic, ordered tasks (Verb-first). +- **Validation**: At least one item for testing. + +## Plan Template + +```markdown +# Plan + + + +## Scope + +- In: +- Out: + +## Action Items + +[ ] +[ ] +[ ] +[ ] +[ ] + +## Open Questions + +- +``` + +## Checklist Guidelines + +- **Atomic**: Each step should be a single logical unit of work. +- **Verb-first**: "Add...", "Refactor...", "Verify...". +- **Concrete**: Name specific files or modules when possible. + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-essentials/skills/git-pushing/SKILL.md b/plugins/antigravity-bundle-essentials/skills/git-pushing/SKILL.md new file mode 100644 index 00000000..6c5af847 --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/git-pushing/SKILL.md @@ -0,0 +1,35 @@ +--- +name: git-pushing +description: "Stage all changes, create a conventional commit, and push to the remote branch. Use when explicitly asks to push changes (\"push this\", \"commit and push\"), mentions saving work to remote (\"save to github\", \"push to remote\"), or completes a feature and wants to share it." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Git Push Workflow + +Stage all changes, create a conventional commit, and push to the remote branch. + +## When to Use +Automatically activate when the user: + +- Explicitly asks to push changes ("push this", "commit and push") +- Mentions saving work to remote ("save to github", "push to remote") +- Completes a feature and wants to share it +- Says phrases like "let's push this up" or "commit these changes" + +## Workflow + +**ALWAYS use the script** - do NOT use manual git commands: + +```bash +bash skills/git-pushing/scripts/smart_commit.sh +``` + +With custom message: + +```bash +bash skills/git-pushing/scripts/smart_commit.sh "feat: add feature" +``` + +Script handles: staging, conventional commit message, Claude footer, push with -u flag. diff --git a/plugins/antigravity-bundle-essentials/skills/git-pushing/scripts/smart_commit.sh b/plugins/antigravity-bundle-essentials/skills/git-pushing/scripts/smart_commit.sh new file mode 100644 index 00000000..21299873 --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/git-pushing/scripts/smart_commit.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +# Default commit message if none provided +MESSAGE="${1:-chore: update code}" + +# Add all changes +git add . + +# Commit with the provided message +git commit -m "$MESSAGE" + +# Get current branch name +BRANCH=$(git rev-parse --abbrev-ref HEAD) + +# Push to remote, setting upstream if needed +git push -u origin "$BRANCH" + +echo "โœ… Successfully pushed to $BRANCH" diff --git a/plugins/antigravity-bundle-essentials/skills/kaizen/SKILL.md b/plugins/antigravity-bundle-essentials/skills/kaizen/SKILL.md new file mode 100644 index 00000000..89539694 --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/kaizen/SKILL.md @@ -0,0 +1,732 @@ +--- +name: kaizen +description: "Guide for continuous improvement, error proofing, and standardization. Use this skill when the user wants to improve code quality, refactor, or discuss process improvements." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Kaizen: Continuous Improvement + +## Overview + +Small improvements, continuously. Error-proof by design. Follow what works. Build only what's needed. + +**Core principle:** Many small improvements beat one big change. Prevent errors at design time, not with fixes. + +## When to Use +**Always applied for:** + +- Code implementation and refactoring +- Architecture and design decisions +- Process and workflow improvements +- Error handling and validation + +**Philosophy:** Quality through incremental progress and prevention, not perfection through massive effort. + +## The Four Pillars + +### 1. Continuous Improvement (Kaizen) + +Small, frequent improvements compound into major gains. + +#### Principles + +**Incremental over revolutionary:** + +- Make smallest viable change that improves quality +- One improvement at a time +- Verify each change before next +- Build momentum through small wins + +**Always leave code better:** + +- Fix small issues as you encounter them +- Refactor while you work (within scope) +- Update outdated comments +- Remove dead code when you see it + +**Iterative refinement:** + +- First version: make it work +- Second pass: make it clear +- Third pass: make it efficient +- Don't try all three at once + + +```typescript +// Iteration 1: Make it work +const calculateTotal = (items: Item[]) => { + let total = 0; + for (let i = 0; i < items.length; i++) { + total += items[i].price * items[i].quantity; + } + return total; +}; + +// Iteration 2: Make it clear (refactor) +const calculateTotal = (items: Item[]): number => { +return items.reduce((total, item) => { +return total + (item.price \* item.quantity); +}, 0); +}; + +// Iteration 3: Make it robust (add validation) +const calculateTotal = (items: Item[]): number => { +if (!items?.length) return 0; + +return items.reduce((total, item) => { +if (item.price < 0 || item.quantity < 0) { +throw new Error('Price and quantity must be non-negative'); +} +return total + (item.price \* item.quantity); +}, 0); +}; + +```` +Each step is complete, tested, and working + + + +```typescript +// Trying to do everything at once +const calculateTotal = (items: Item[]): number => { + // Validate, optimize, add features, handle edge cases all together + if (!items?.length) return 0; + const validItems = items.filter(item => { + if (item.price < 0) throw new Error('Negative price'); + if (item.quantity < 0) throw new Error('Negative quantity'); + return item.quantity > 0; // Also filtering zero quantities + }); + // Plus caching, plus logging, plus currency conversion... + return validItems.reduce(...); // Too many concerns at once +}; +```` + +Overwhelming, error-prone, hard to verify + + +#### In Practice + +**When implementing features:** + +1. Start with simplest version that works +2. Add one improvement (error handling, validation, etc.) +3. Test and verify +4. Repeat if time permits +5. Don't try to make it perfect immediately + +**When refactoring:** + +- Fix one smell at a time +- Commit after each improvement +- Keep tests passing throughout +- Stop when "good enough" (diminishing returns) + +**When reviewing code:** + +- Suggest incremental improvements (not rewrites) +- Prioritize: critical โ†’ important โ†’ nice-to-have +- Focus on highest-impact changes first +- Accept "better than before" even if not perfect + +### 2. Poka-Yoke (Error Proofing) + +Design systems that prevent errors at compile/design time, not runtime. + +#### Principles + +**Make errors impossible:** + +- Type system catches mistakes +- Compiler enforces contracts +- Invalid states unrepresentable +- Errors caught early (left of production) + +**Design for safety:** + +- Fail fast and loudly +- Provide helpful error messages +- Make correct path obvious +- Make incorrect path difficult + +**Defense in layers:** + +1. Type system (compile time) +2. Validation (runtime, early) +3. Guards (preconditions) +4. Error boundaries (graceful degradation) + +#### Type System Error Proofing + + +```typescript +// Error: string status can be any value +type OrderBad = { + status: string; // Can be "pending", "PENDING", "pnding", anything! + total: number; +}; + +// Good: Only valid states possible +type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered'; +type Order = { +status: OrderStatus; +total: number; +}; + +// Better: States with associated data +type Order = +| { status: 'pending'; createdAt: Date } +| { status: 'processing'; startedAt: Date; estimatedCompletion: Date } +| { status: 'shipped'; trackingNumber: string; shippedAt: Date } +| { status: 'delivered'; deliveredAt: Date; signature: string }; + +// Now impossible to have shipped without trackingNumber + +```` +Type system prevents entire classes of errors + + + +```typescript +// Make invalid states unrepresentable +type NonEmptyArray = [T, ...T[]]; + +const firstItem = (items: NonEmptyArray): T => { + return items[0]; // Always safe, never undefined! +}; + +// Caller must prove array is non-empty +const items: number[] = [1, 2, 3]; +if (items.length > 0) { + firstItem(items as NonEmptyArray); // Safe +} +```` + +Function signature guarantees safety + + +#### Validation Error Proofing + + +```typescript +// Error: Validation after use +const processPayment = (amount: number) => { + const fee = amount * 0.03; // Used before validation! + if (amount <= 0) throw new Error('Invalid amount'); + // ... +}; + +// Good: Validate immediately +const processPayment = (amount: number) => { +if (amount <= 0) { +throw new Error('Payment amount must be positive'); +} +if (amount > 10000) { +throw new Error('Payment exceeds maximum allowed'); +} + +const fee = amount \* 0.03; +// ... now safe to use +}; + +// Better: Validation at boundary with branded type +type PositiveNumber = number & { readonly \_\_brand: 'PositiveNumber' }; + +const validatePositive = (n: number): PositiveNumber => { +if (n <= 0) throw new Error('Must be positive'); +return n as PositiveNumber; +}; + +const processPayment = (amount: PositiveNumber) => { +// amount is guaranteed positive, no need to check +const fee = amount \* 0.03; +}; + +// Validate at system boundary +const handlePaymentRequest = (req: Request) => { +const amount = validatePositive(req.body.amount); // Validate once +processPayment(amount); // Use everywhere safely +}; + +```` +Validate once at boundary, safe everywhere else + + +#### Guards and Preconditions + + +```typescript +// Early returns prevent deeply nested code +const processUser = (user: User | null) => { + if (!user) { + logger.error('User not found'); + return; + } + + if (!user.email) { + logger.error('User email missing'); + return; + } + + if (!user.isActive) { + logger.info('User inactive, skipping'); + return; + } + + // Main logic here, guaranteed user is valid and active + sendEmail(user.email, 'Welcome!'); +}; +```` + +Guards make assumptions explicit and enforced + + +#### Configuration Error Proofing + + +```typescript +// Error: Optional config with unsafe defaults +type ConfigBad = { + apiKey?: string; + timeout?: number; +}; + +const client = new APIClient({ timeout: 5000 }); // apiKey missing! + +// Good: Required config, fails early +type Config = { +apiKey: string; +timeout: number; +}; + +const loadConfig = (): Config => { +const apiKey = process.env.API_KEY; +if (!apiKey) { +throw new Error('API_KEY environment variable required'); +} + +return { +apiKey, +timeout: 5000, +}; +}; + +// App fails at startup if config invalid, not during request +const config = loadConfig(); +const client = new APIClient(config); + +```` +Fail at startup, not in production + + +#### In Practice + +**When designing APIs:** +- Use types to constrain inputs +- Make invalid states unrepresentable +- Return Result instead of throwing +- Document preconditions in types + +**When handling errors:** +- Validate at system boundaries + +- Use guards for preconditions +- Fail fast with clear messages +- Log context for debugging + +**When configuring:** +- Required over optional with defaults +- Validate all config at startup +- Fail deployment if config invalid +- Don't allow partial configurations + +### 3. Standardized Work +Follow established patterns. Document what works. Make good practices easy to follow. + +#### Principles + +**Consistency over cleverness:** +- Follow existing codebase patterns +- Don't reinvent solved problems +- New pattern only if significantly better +- Team agreement on new patterns + +**Documentation lives with code:** +- README for setup and architecture +- CLAUDE.md for AI coding conventions +- Comments for "why", not "what" +- Examples for complex patterns + +**Automate standards:** +- Linters enforce style +- Type checks enforce contracts +- Tests verify behavior +- CI/CD enforces quality gates + +#### Following Patterns + + +```typescript +// Existing codebase pattern for API clients +class UserAPIClient { + async getUser(id: string): Promise { + return this.fetch(`/users/${id}`); + } +} + +// New code follows the same pattern +class OrderAPIClient { + async getOrder(id: string): Promise { + return this.fetch(`/orders/${id}`); + } +} +```` + +Consistency makes codebase predictable + + + +```typescript +// Existing pattern uses classes +class UserAPIClient { /* ... */ } + +// New code introduces different pattern without discussion +const getOrder = async (id: string): Promise => { +// Breaking consistency "because I prefer functions" +}; + +```` +Inconsistency creates confusion + + +#### Error Handling Patterns + + +```typescript +// Project standard: Result type for recoverable errors +type Result = { ok: true; value: T } | { ok: false; error: E }; + +// All services follow this pattern +const fetchUser = async (id: string): Promise> => { + try { + const user = await db.users.findById(id); + if (!user) { + return { ok: false, error: new Error('User not found') }; + } + return { ok: true, value: user }; + } catch (err) { + return { ok: false, error: err as Error }; + } +}; + +// Callers use consistent pattern +const result = await fetchUser('123'); +if (!result.ok) { + logger.error('Failed to fetch user', result.error); + return; +} +const user = result.value; // Type-safe! +```` + +Standard pattern across codebase + + +#### Documentation Standards + + +```typescript +/** + * Retries an async operation with exponential backoff. + * + * Why: Network requests fail temporarily; retrying improves reliability + * When to use: External API calls, database operations + * When not to use: User input validation, internal function calls + * + * @example + * const result = await retry( + * () => fetch('https://api.example.com/data'), + * { maxAttempts: 3, baseDelay: 1000 } + * ); + */ +const retry = async ( + operation: () => Promise, + options: RetryOptions +): Promise => { + // Implementation... +}; +``` +Documents why, when, and how + + +#### In Practice + +**Before adding new patterns:** + +- Search codebase for similar problems solved +- Check CLAUDE.md for project conventions +- Discuss with team if breaking from pattern +- Update docs when introducing new pattern + +**When writing code:** + +- Match existing file structure +- Use same naming conventions +- Follow same error handling approach +- Import from same locations + +**When reviewing:** + +- Check consistency with existing code +- Point to examples in codebase +- Suggest aligning with standards +- Update CLAUDE.md if new standard emerges + +### 4. Just-In-Time (JIT) + +Build what's needed now. No more, no less. Avoid premature optimization and over-engineering. + +#### Principles + +**YAGNI (You Aren't Gonna Need It):** + +- Implement only current requirements +- No "just in case" features +- No "we might need this later" code +- Delete speculation + +**Simplest thing that works:** + +- Start with straightforward solution +- Add complexity only when needed +- Refactor when requirements change +- Don't anticipate future needs + +**Optimize when measured:** + +- No premature optimization +- Profile before optimizing +- Measure impact of changes +- Accept "good enough" performance + +#### YAGNI in Action + + +```typescript +// Current requirement: Log errors to console +const logError = (error: Error) => { + console.error(error.message); +}; +``` +Simple, meets current need + + + +```typescript +// Over-engineered for "future needs" +interface LogTransport { + write(level: LogLevel, message: string, meta?: LogMetadata): Promise; +} + +class ConsoleTransport implements LogTransport { /_... _/ } +class FileTransport implements LogTransport { /_ ... _/ } +class RemoteTransport implements LogTransport { /_ ..._/ } + +class Logger { +private transports: LogTransport[] = []; +private queue: LogEntry[] = []; +private rateLimiter: RateLimiter; +private formatter: LogFormatter; + +// 200 lines of code for "maybe we'll need it" +} + +const logError = (error: Error) => { +Logger.getInstance().log('error', error.message); +}; + +```` +Building for imaginary future requirements + + +**When to add complexity:** +- Current requirement demands it +- Pain points identified through use +- Measured performance issues +- Multiple use cases emerged + + +```typescript +// Start simple +const formatCurrency = (amount: number): string => { + return `$${amount.toFixed(2)}`; +}; + +// Requirement evolves: support multiple currencies +const formatCurrency = (amount: number, currency: string): string => { + const symbols = { USD: '$', EUR: 'โ‚ฌ', GBP: 'ยฃ' }; + return `${symbols[currency]}${amount.toFixed(2)}`; +}; + +// Requirement evolves: support localization +const formatCurrency = (amount: number, locale: string): string => { + return new Intl.NumberFormat(locale, {\n style: 'currency', + currency: locale === 'en-US' ? 'USD' : 'EUR', + }).format(amount); +}; +```` + +Complexity added only when needed + + +#### Premature Abstraction + + +```typescript +// One use case, but building generic framework +abstract class BaseCRUDService { + abstract getAll(): Promise; + abstract getById(id: string): Promise; + abstract create(data: Partial): Promise; + abstract update(id: string, data: Partial): Promise; + abstract delete(id: string): Promise; +} + +class GenericRepository { /_300 lines _/ } +class QueryBuilder { /_ 200 lines_/ } +// ... building entire ORM for single table + +```` +Massive abstraction for uncertain future + + + +```typescript +// Simple functions for current needs +const getUsers = async (): Promise => { + return db.query('SELECT * FROM users'); +}; + +const getUserById = async (id: string): Promise => { + return db.query('SELECT * FROM users WHERE id = $1', [id]); +}; + +// When pattern emerges across multiple entities, then abstract +```` + +Abstract only when pattern proven across 3+ cases + + +#### Performance Optimization + + +```typescript +// Current: Simple approach +const filterActiveUsers = (users: User[]): User[] => { + return users.filter(user => user.isActive); +}; + +// Benchmark shows: 50ms for 1000 users (acceptable) +// โœ“ Ship it, no optimization needed + +// Later: After profiling shows this is bottleneck +// Then optimize with indexed lookup or caching + +```` +Optimize based on measurement, not assumptions + + + +```typescript +// Premature optimization +const filterActiveUsers = (users: User[]): User[] => { + // "This might be slow, so let's cache and index" + const cache = new WeakMap(); + const indexed = buildBTreeIndex(users, 'isActive'); + // 100 lines of optimization code + // Adds complexity, harder to maintain + // No evidence it was needed +};\ +```` + +Complex solution for unmeasured problem + + +#### In Practice + +**When implementing:** + +- Solve the immediate problem +- Use straightforward approach +- Resist "what if" thinking +- Delete speculative code + +**When optimizing:** + +- Profile first, optimize second +- Measure before and after +- Document why optimization needed +- Keep simple version in tests + +**When abstracting:** + +- Wait for 3+ similar cases (Rule of Three) +- Make abstraction as simple as possible +- Prefer duplication over wrong abstraction +- Refactor when pattern clear + +## Integration with Commands + +The Kaizen skill guides how you work. The commands provide structured analysis: + +- **`/why`**: Root cause analysis (5 Whys) +- **`/cause-and-effect`**: Multi-factor analysis (Fishbone) +- **`/plan-do-check-act`**: Iterative improvement cycles +- **`/analyse-problem`**: Comprehensive documentation (A3) +- **`/analyse`**: Smart method selection (Gemba/VSM/Muda) + +Use commands for structured problem-solving. Apply skill for day-to-day development. + +## Red Flags + +**Violating Continuous Improvement:** + +- "I'll refactor it later" (never happens) +- Leaving code worse than you found it +- Big bang rewrites instead of incremental + +**Violating Poka-Yoke:** + +- "Users should just be careful" +- Validation after use instead of before +- Optional config with no validation + +**Violating Standardized Work:** + +- "I prefer to do it my way" +- Not checking existing patterns +- Ignoring project conventions + +**Violating Just-In-Time:** + +- "We might need this someday" +- Building frameworks before using them +- Optimizing without measuring + +## Remember + +**Kaizen is about:** + +- Small improvements continuously +- Preventing errors by design +- Following proven patterns +- Building only what's needed + +**Not about:** + +- Perfection on first try +- Massive refactoring projects +- Clever abstractions +- Premature optimization + +**Mindset:** Good enough today, better tomorrow. Repeat. diff --git a/plugins/antigravity-bundle-essentials/skills/lint-and-validate/SKILL.md b/plugins/antigravity-bundle-essentials/skills/lint-and-validate/SKILL.md new file mode 100644 index 00000000..328f3a38 --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/lint-and-validate/SKILL.md @@ -0,0 +1,49 @@ +--- +name: lint-and-validate +description: "MANDATORY: Run appropriate validation tools after EVERY code change. Do not finish a task until the code is error-free." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Lint and Validate Skill + +> **MANDATORY:** Run appropriate validation tools after EVERY code change. Do not finish a task until the code is error-free. + +### Procedures by Ecosystem + +#### Node.js / TypeScript +1. **Lint/Fix:** `npm run lint` or `npx eslint "path" --fix` +2. **Types:** `npx tsc --noEmit` +3. **Security:** `npm audit --audit-level=high` + +#### Python +1. **Linter (Ruff):** `ruff check "path" --fix` (Fast & Modern) +2. **Security (Bandit):** `bandit -r "path" -ll` +3. **Types (MyPy):** `mypy "path"` + +## The Quality Loop +1. **Write/Edit Code** +2. **Run Audit:** `npm run lint && npx tsc --noEmit` +3. **Analyze Report:** Check the "FINAL AUDIT REPORT" section. +4. **Fix & Repeat:** Submitting code with "FINAL AUDIT" failures is NOT allowed. + +## Error Handling +- If `lint` fails: Fix the style or syntax issues immediately. +- If `tsc` fails: Correct type mismatches before proceeding. +- If no tool is configured: Check the project root for `.eslintrc`, `tsconfig.json`, `pyproject.toml` and suggest creating one. + +--- +**Strict Rule:** No code should be committed or reported as "done" without passing these checks. + +--- + +## Scripts + +| Script | Purpose | Command | +|--------|---------|---------| +| `scripts/lint_runner.py` | Unified lint check | `python scripts/lint_runner.py ` | +| `scripts/type_coverage.py` | Type coverage analysis | `python scripts/type_coverage.py ` | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-essentials/skills/lint-and-validate/scripts/lint_runner.py b/plugins/antigravity-bundle-essentials/skills/lint-and-validate/scripts/lint_runner.py new file mode 100644 index 00000000..6308f0a5 --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/lint-and-validate/scripts/lint_runner.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +""" +Lint Runner - Unified linting and type checking +Runs appropriate linters based on project type. + +Usage: + python lint_runner.py + +Supports: + - Node.js: npm run lint, npx tsc --noEmit + - Python: ruff check, mypy +""" + +import subprocess +import sys +import json +from pathlib import Path +from datetime import datetime + +# Fix Windows console encoding +try: + sys.stdout.reconfigure(encoding='utf-8', errors='replace') +except: + pass + + +def detect_project_type(project_path: Path) -> dict: + """Detect project type and available linters.""" + result = { + "type": "unknown", + "linters": [] + } + + # Node.js project + package_json = project_path / "package.json" + if package_json.exists(): + result["type"] = "node" + try: + pkg = json.loads(package_json.read_text(encoding='utf-8')) + scripts = pkg.get("scripts", {}) + deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})} + + # Check for lint script + if "lint" in scripts: + result["linters"].append({"name": "npm lint", "cmd": ["npm", "run", "lint"]}) + elif "eslint" in deps: + result["linters"].append({"name": "eslint", "cmd": ["npx", "eslint", "."]}) + + # Check for TypeScript + if "typescript" in deps or (project_path / "tsconfig.json").exists(): + result["linters"].append({"name": "tsc", "cmd": ["npx", "tsc", "--noEmit"]}) + + except: + pass + + # Python project + if (project_path / "pyproject.toml").exists() or (project_path / "requirements.txt").exists(): + result["type"] = "python" + + # Check for ruff + result["linters"].append({"name": "ruff", "cmd": ["ruff", "check", "."]}) + + # Check for mypy + if (project_path / "mypy.ini").exists() or (project_path / "pyproject.toml").exists(): + result["linters"].append({"name": "mypy", "cmd": ["mypy", "."]}) + + return result + + +def run_linter(linter: dict, cwd: Path) -> dict: + """Run a single linter and return results.""" + result = { + "name": linter["name"], + "passed": False, + "output": "", + "error": "" + } + + try: + proc = subprocess.run( + linter["cmd"], + cwd=str(cwd), + capture_output=True, + text=True, + encoding='utf-8', + errors='replace', + timeout=120 + ) + + result["output"] = proc.stdout[:2000] if proc.stdout else "" + result["error"] = proc.stderr[:500] if proc.stderr else "" + result["passed"] = proc.returncode == 0 + + except FileNotFoundError: + result["error"] = f"Command not found: {linter['cmd'][0]}" + except subprocess.TimeoutExpired: + result["error"] = "Timeout after 120s" + except Exception as e: + result["error"] = str(e) + + return result + + +def main(): + project_path = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve() + + print(f"\n{'='*60}") + print(f"[LINT RUNNER] Unified Linting") + print(f"{'='*60}") + print(f"Project: {project_path}") + print(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + # Detect project type + project_info = detect_project_type(project_path) + print(f"Type: {project_info['type']}") + print(f"Linters: {len(project_info['linters'])}") + print("-"*60) + + if not project_info["linters"]: + print("No linters found for this project type.") + output = { + "script": "lint_runner", + "project": str(project_path), + "type": project_info["type"], + "checks": [], + "passed": True, + "message": "No linters configured" + } + print(json.dumps(output, indent=2)) + sys.exit(0) + + # Run each linter + results = [] + all_passed = True + + for linter in project_info["linters"]: + print(f"\nRunning: {linter['name']}...") + result = run_linter(linter, project_path) + results.append(result) + + if result["passed"]: + print(f" [PASS] {linter['name']}") + else: + print(f" [FAIL] {linter['name']}") + if result["error"]: + print(f" Error: {result['error'][:200]}") + all_passed = False + + # Summary + print("\n" + "="*60) + print("SUMMARY") + print("="*60) + + for r in results: + icon = "[PASS]" if r["passed"] else "[FAIL]" + print(f"{icon} {r['name']}") + + output = { + "script": "lint_runner", + "project": str(project_path), + "type": project_info["type"], + "checks": results, + "passed": all_passed + } + + print("\n" + json.dumps(output, indent=2)) + + sys.exit(0 if all_passed else 1) + + +if __name__ == "__main__": + main() diff --git a/plugins/antigravity-bundle-essentials/skills/lint-and-validate/scripts/type_coverage.py b/plugins/antigravity-bundle-essentials/skills/lint-and-validate/scripts/type_coverage.py new file mode 100644 index 00000000..0a846277 --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/lint-and-validate/scripts/type_coverage.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +""" +Type Coverage Checker - Measures TypeScript/Python type coverage. +Identifies untyped functions, any usage, and type safety issues. +""" +import sys +import re +import subprocess +from pathlib import Path + +# Fix Windows console encoding for Unicode output +try: + sys.stdout.reconfigure(encoding='utf-8', errors='replace') + sys.stderr.reconfigure(encoding='utf-8', errors='replace') +except AttributeError: + pass # Python < 3.7 + +def check_typescript_coverage(project_path: Path) -> dict: + """Check TypeScript type coverage.""" + issues = [] + passed = [] + stats = {'any_count': 0, 'untyped_functions': 0, 'total_functions': 0} + + ts_files = list(project_path.rglob("*.ts")) + list(project_path.rglob("*.tsx")) + ts_files = [f for f in ts_files if 'node_modules' not in str(f) and '.d.ts' not in str(f)] + + if not ts_files: + return {'type': 'typescript', 'files': 0, 'passed': [], 'issues': ["[!] No TypeScript files found"], 'stats': stats} + + for file_path in ts_files[:30]: # Limit + try: + content = file_path.read_text(encoding='utf-8', errors='ignore') + + # Count 'any' usage + any_matches = re.findall(r':\s*any\b', content) + stats['any_count'] += len(any_matches) + + # Find functions without return types + # function name(params) { - no return type + untyped = re.findall(r'function\s+\w+\s*\([^)]*\)\s*{', content) + # Arrow functions without types: const fn = (x) => or (x) => + untyped += re.findall(r'=\s*\([^:)]*\)\s*=>', content) + stats['untyped_functions'] += len(untyped) + + # Count typed functions + typed = re.findall(r'function\s+\w+\s*\([^)]*\)\s*:\s*\w+', content) + typed += re.findall(r':\s*\([^)]*\)\s*=>\s*\w+', content) + stats['total_functions'] += len(typed) + len(untyped) + + except Exception: + continue + + # Analyze results + if stats['any_count'] == 0: + passed.append("[OK] No 'any' types found") + elif stats['any_count'] <= 5: + issues.append(f"[!] {stats['any_count']} 'any' types found (acceptable)") + else: + issues.append(f"[X] {stats['any_count']} 'any' types found (too many)") + + if stats['total_functions'] > 0: + typed_ratio = (stats['total_functions'] - stats['untyped_functions']) / stats['total_functions'] * 100 + if typed_ratio >= 80: + passed.append(f"[OK] Type coverage: {typed_ratio:.0f}%") + elif typed_ratio >= 50: + issues.append(f"[!] Type coverage: {typed_ratio:.0f}% (improve)") + else: + issues.append(f"[X] Type coverage: {typed_ratio:.0f}% (too low)") + + passed.append(f"[OK] Analyzed {len(ts_files)} TypeScript files") + + return {'type': 'typescript', 'files': len(ts_files), 'passed': passed, 'issues': issues, 'stats': stats} + +def check_python_coverage(project_path: Path) -> dict: + """Check Python type hints coverage.""" + issues = [] + passed = [] + stats = {'untyped_functions': 0, 'typed_functions': 0, 'any_count': 0} + + py_files = list(project_path.rglob("*.py")) + py_files = [f for f in py_files if not any(x in str(f) for x in ['venv', '__pycache__', '.git', 'node_modules'])] + + if not py_files: + return {'type': 'python', 'files': 0, 'passed': [], 'issues': ["[!] No Python files found"], 'stats': stats} + + for file_path in py_files[:30]: # Limit + try: + content = file_path.read_text(encoding='utf-8', errors='ignore') + + # Count Any usage + any_matches = re.findall(r':\s*Any\b', content) + stats['any_count'] += len(any_matches) + + # Find functions with type hints + typed_funcs = re.findall(r'def\s+\w+\s*\([^)]*:[^)]+\)', content) + typed_funcs += re.findall(r'def\s+\w+\s*\([^)]*\)\s*->', content) + stats['typed_functions'] += len(typed_funcs) + + # Find functions without type hints + all_funcs = re.findall(r'def\s+\w+\s*\(', content) + stats['untyped_functions'] += len(all_funcs) - len(typed_funcs) + + except Exception: + continue + + total = stats['typed_functions'] + stats['untyped_functions'] + + if total > 0: + typed_ratio = stats['typed_functions'] / total * 100 + if typed_ratio >= 70: + passed.append(f"[OK] Type hints coverage: {typed_ratio:.0f}%") + elif typed_ratio >= 40: + issues.append(f"[!] Type hints coverage: {typed_ratio:.0f}%") + else: + issues.append(f"[X] Type hints coverage: {typed_ratio:.0f}% (add type hints)") + + if stats['any_count'] == 0: + passed.append("[OK] No 'Any' types found") + elif stats['any_count'] <= 3: + issues.append(f"[!] {stats['any_count']} 'Any' types found") + else: + issues.append(f"[X] {stats['any_count']} 'Any' types found") + + passed.append(f"[OK] Analyzed {len(py_files)} Python files") + + return {'type': 'python', 'files': len(py_files), 'passed': passed, 'issues': issues, 'stats': stats} + +def main(): + target = sys.argv[1] if len(sys.argv) > 1 else "." + project_path = Path(target) + + print("\n" + "=" * 60) + print(" TYPE COVERAGE CHECKER") + print("=" * 60 + "\n") + + results = [] + + # Check TypeScript + ts_result = check_typescript_coverage(project_path) + if ts_result['files'] > 0: + results.append(ts_result) + + # Check Python + py_result = check_python_coverage(project_path) + if py_result['files'] > 0: + results.append(py_result) + + if not results: + print("[!] No TypeScript or Python files found.") + sys.exit(0) + + # Print results + critical_issues = 0 + for result in results: + print(f"\n[{result['type'].upper()}]") + print("-" * 40) + for item in result['passed']: + print(f" {item}") + for item in result['issues']: + print(f" {item}") + if item.startswith("[X]"): + critical_issues += 1 + + print("\n" + "=" * 60) + if critical_issues == 0: + print("[OK] TYPE COVERAGE: ACCEPTABLE") + sys.exit(0) + else: + print(f"[X] TYPE COVERAGE: {critical_issues} critical issues") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/plugins/antigravity-bundle-essentials/skills/systematic-debugging/CREATION-LOG.md b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/CREATION-LOG.md new file mode 100644 index 00000000..024d00a5 --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/CREATION-LOG.md @@ -0,0 +1,119 @@ +# Creation Log: Systematic Debugging Skill + +Reference example of extracting, structuring, and bulletproofing a critical skill. + +## Source Material + +Extracted debugging framework from `/Users/jesse/.claude/CLAUDE.md`: +- 4-phase systematic process (Investigation โ†’ Pattern Analysis โ†’ Hypothesis โ†’ Implementation) +- Core mandate: ALWAYS find root cause, NEVER fix symptoms +- Rules designed to resist time pressure and rationalization + +## Extraction Decisions + +**What to include:** +- Complete 4-phase framework with all rules +- Anti-shortcuts ("NEVER fix symptom", "STOP and re-analyze") +- Pressure-resistant language ("even if faster", "even if I seem in a hurry") +- Concrete steps for each phase + +**What to leave out:** +- Project-specific context +- Repetitive variations of same rule +- Narrative explanations (condensed to principles) + +## Structure Following skill-creation/SKILL.md + +1. **Rich when_to_use** - Included symptoms and anti-patterns +2. **Type: technique** - Concrete process with steps +3. **Keywords** - "root cause", "symptom", "workaround", "debugging", "investigation" +4. **Flowchart** - Decision point for "fix failed" โ†’ re-analyze vs add more fixes +5. **Phase-by-phase breakdown** - Scannable checklist format +6. **Anti-patterns section** - What NOT to do (critical for this skill) + +## Bulletproofing Elements + +Framework designed to resist rationalization under pressure: + +### Language Choices +- "ALWAYS" / "NEVER" (not "should" / "try to") +- "even if faster" / "even if I seem in a hurry" +- "STOP and re-analyze" (explicit pause) +- "Don't skip past" (catches the actual behavior) + +### Structural Defenses +- **Phase 1 required** - Can't skip to implementation +- **Single hypothesis rule** - Forces thinking, prevents shotgun fixes +- **Explicit failure mode** - "IF your first fix doesn't work" with mandatory action +- **Anti-patterns section** - Shows exactly what shortcuts look like + +### Redundancy +- Root cause mandate in overview + when_to_use + Phase 1 + implementation rules +- "NEVER fix symptom" appears 4 times in different contexts +- Each phase has explicit "don't skip" guidance + +## Testing Approach + +Created 4 validation tests following skills/meta/testing-skills-with-subagents: + +### Test 1: Academic Context (No Pressure) +- Simple bug, no time pressure +- **Result:** Perfect compliance, complete investigation + +### Test 2: Time Pressure + Obvious Quick Fix +- User "in a hurry", symptom fix looks easy +- **Result:** Resisted shortcut, followed full process, found real root cause + +### Test 3: Complex System + Uncertainty +- Multi-layer failure, unclear if can find root cause +- **Result:** Systematic investigation, traced through all layers, found source + +### Test 4: Failed First Fix +- Hypothesis doesn't work, temptation to add more fixes +- **Result:** Stopped, re-analyzed, formed new hypothesis (no shotgun) + +**All tests passed.** No rationalizations found. + +## Iterations + +### Initial Version +- Complete 4-phase framework +- Anti-patterns section +- Flowchart for "fix failed" decision + +### Enhancement 1: TDD Reference +- Added link to skills/testing/test-driven-development +- Note explaining TDD's "simplest code" โ‰  debugging's "root cause" +- Prevents confusion between methodologies + +## Final Outcome + +Bulletproof skill that: +- โœ… Clearly mandates root cause investigation +- โœ… Resists time pressure rationalization +- โœ… Provides concrete steps for each phase +- โœ… Shows anti-patterns explicitly +- โœ… Tested under multiple pressure scenarios +- โœ… Clarifies relationship to TDD +- โœ… Ready for use + +## Key Insight + +**Most important bulletproofing:** Anti-patterns section showing exact shortcuts that feel justified in the moment. When Claude thinks "I'll just add this one quick fix", seeing that exact pattern listed as wrong creates cognitive friction. + +## Usage Example + +When encountering a bug: +1. Load skill: skills/debugging/systematic-debugging +2. Read overview (10 sec) - reminded of mandate +3. Follow Phase 1 checklist - forced investigation +4. If tempted to skip - see anti-pattern, stop +5. Complete all phases - root cause found + +**Time investment:** 5-10 minutes +**Time saved:** Hours of symptom-whack-a-mole + +--- + +*Created: 2025-10-03* +*Purpose: Reference example for skill extraction and bulletproofing* diff --git a/plugins/antigravity-bundle-essentials/skills/systematic-debugging/SKILL.md b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/SKILL.md new file mode 100644 index 00000000..4ae6dbac --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/SKILL.md @@ -0,0 +1,298 @@ +--- +name: systematic-debugging +description: "Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes" +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Systematic Debugging + +## Overview + +Random fixes waste time and create new bugs. Quick patches mask underlying issues. + +**Core principle:** ALWAYS find root cause before attempting fixes. Symptom fixes are failure. + +**Violating the letter of this process is violating the spirit of debugging.** + +## The Iron Law + +``` +NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST +``` + +If you haven't completed Phase 1, you cannot propose fixes. + +## When to Use +Use for ANY technical issue: +- Test failures +- Bugs in production +- Unexpected behavior +- Performance problems +- Build failures +- Integration issues + +**Use this ESPECIALLY when:** +- Under time pressure (emergencies make guessing tempting) +- "Just one quick fix" seems obvious +- You've already tried multiple fixes +- Previous fix didn't work +- You don't fully understand the issue + +**Don't skip when:** +- Issue seems simple (simple bugs have root causes too) +- You're in a hurry (rushing guarantees rework) +- Manager wants it fixed NOW (systematic is faster than thrashing) + +## The Four Phases + +You MUST complete each phase before proceeding to the next. + +### Phase 1: Root Cause Investigation + +**BEFORE attempting ANY fix:** + +1. **Read Error Messages Carefully** + - Don't skip past errors or warnings + - They often contain the exact solution + - Read stack traces completely + - Note line numbers, file paths, error codes + +2. **Reproduce Consistently** + - Can you trigger it reliably? + - What are the exact steps? + - Does it happen every time? + - If not reproducible โ†’ gather more data, don't guess + +3. **Check Recent Changes** + - What changed that could cause this? + - Git diff, recent commits + - New dependencies, config changes + - Environmental differences + +4. **Gather Evidence in Multi-Component Systems** + + **WHEN system has multiple components (CI โ†’ build โ†’ signing, API โ†’ service โ†’ database):** + + **BEFORE proposing fixes, add diagnostic instrumentation:** + ``` + For EACH component boundary: + - Log what data enters component + - Log what data exits component + - Verify environment/config propagation + - Check state at each layer + + Run once to gather evidence showing WHERE it breaks + THEN analyze evidence to identify failing component + THEN investigate that specific component + ``` + + **Example (multi-layer system):** + ```bash + # Layer 1: Workflow + echo "=== Secrets available in workflow: ===" + echo "IDENTITY: ${IDENTITY:+SET}${IDENTITY:-UNSET}" + + # Layer 2: Build script + echo "=== Env vars in build script: ===" + env | grep IDENTITY || echo "IDENTITY not in environment" + + # Layer 3: Signing script + echo "=== Keychain state: ===" + security list-keychains + security find-identity -v + + # Layer 4: Actual signing + codesign --sign "$IDENTITY" --verbose=4 "$APP" + ``` + + **This reveals:** Which layer fails (secrets โ†’ workflow โœ“, workflow โ†’ build โœ—) + +5. **Trace Data Flow** + + **WHEN error is deep in call stack:** + + See `root-cause-tracing.md` in this directory for the complete backward tracing technique. + + **Quick version:** + - Where does bad value originate? + - What called this with bad value? + - Keep tracing up until you find the source + - Fix at source, not at symptom + +### Phase 2: Pattern Analysis + +**Find the pattern before fixing:** + +1. **Find Working Examples** + - Locate similar working code in same codebase + - What works that's similar to what's broken? + +2. **Compare Against References** + - If implementing pattern, read reference implementation COMPLETELY + - Don't skim - read every line + - Understand the pattern fully before applying + +3. **Identify Differences** + - What's different between working and broken? + - List every difference, however small + - Don't assume "that can't matter" + +4. **Understand Dependencies** + - What other components does this need? + - What settings, config, environment? + - What assumptions does it make? + +### Phase 3: Hypothesis and Testing + +**Scientific method:** + +1. **Form Single Hypothesis** + - State clearly: "I think X is the root cause because Y" + - Write it down + - Be specific, not vague + +2. **Test Minimally** + - Make the SMALLEST possible change to test hypothesis + - One variable at a time + - Don't fix multiple things at once + +3. **Verify Before Continuing** + - Did it work? Yes โ†’ Phase 4 + - Didn't work? Form NEW hypothesis + - DON'T add more fixes on top + +4. **When You Don't Know** + - Say "I don't understand X" + - Don't pretend to know + - Ask for help + - Research more + +### Phase 4: Implementation + +**Fix the root cause, not the symptom:** + +1. **Create Failing Test Case** + - Simplest possible reproduction + - Automated test if possible + - One-off test script if no framework + - MUST have before fixing + - Use the `superpowers:test-driven-development` skill for writing proper failing tests + +2. **Implement Single Fix** + - Address the root cause identified + - ONE change at a time + - No "while I'm here" improvements + - No bundled refactoring + +3. **Verify Fix** + - Test passes now? + - No other tests broken? + - Issue actually resolved? + +4. **If Fix Doesn't Work** + - STOP + - Count: How many fixes have you tried? + - If < 3: Return to Phase 1, re-analyze with new information + - **If โ‰ฅ 3: STOP and question the architecture (step 5 below)** + - DON'T attempt Fix #4 without architectural discussion + +5. **If 3+ Fixes Failed: Question Architecture** + + **Pattern indicating architectural problem:** + - Each fix reveals new shared state/coupling/problem in different place + - Fixes require "massive refactoring" to implement + - Each fix creates new symptoms elsewhere + + **STOP and question fundamentals:** + - Is this pattern fundamentally sound? + - Are we "sticking with it through sheer inertia"? + - Should we refactor architecture vs. continue fixing symptoms? + + **Discuss with your human partner before attempting more fixes** + + This is NOT a failed hypothesis - this is a wrong architecture. + +## Red Flags - STOP and Follow Process + +If you catch yourself thinking: +- "Quick fix for now, investigate later" +- "Just try changing X and see if it works" +- "Add multiple changes, run tests" +- "Skip the test, I'll manually verify" +- "It's probably X, let me fix that" +- "I don't fully understand but this might work" +- "Pattern says X but I'll adapt it differently" +- "Here are the main problems: [lists fixes without investigation]" +- Proposing solutions before tracing data flow +- **"One more fix attempt" (when already tried 2+)** +- **Each fix reveals new problem in different place** + +**ALL of these mean: STOP. Return to Phase 1.** + +**If 3+ fixes failed:** Question the architecture (see Phase 4.5) + +## your human partner's Signals You're Doing It Wrong + +**Watch for these redirections:** +- "Is that not happening?" - You assumed without verifying +- "Will it show us...?" - You should have added evidence gathering +- "Stop guessing" - You're proposing fixes without understanding +- "Ultrathink this" - Question fundamentals, not just symptoms +- "We're stuck?" (frustrated) - Your approach isn't working + +**When you see these:** STOP. Return to Phase 1. + +## Common Rationalizations + +| Excuse | Reality | +|--------|---------| +| "Issue is simple, don't need process" | Simple issues have root causes too. Process is fast for simple bugs. | +| "Emergency, no time for process" | Systematic debugging is FASTER than guess-and-check thrashing. | +| "Just try this first, then investigate" | First fix sets the pattern. Do it right from the start. | +| "I'll write test after confirming fix works" | Untested fixes don't stick. Test first proves it. | +| "Multiple fixes at once saves time" | Can't isolate what worked. Causes new bugs. | +| "Reference too long, I'll adapt the pattern" | Partial understanding guarantees bugs. Read it completely. | +| "I see the problem, let me fix it" | Seeing symptoms โ‰  understanding root cause. | +| "One more fix attempt" (after 2+ failures) | 3+ failures = architectural problem. Question pattern, don't fix again. | + +## Quick Reference + +| Phase | Key Activities | Success Criteria | +|-------|---------------|------------------| +| **1. Root Cause** | Read errors, reproduce, check changes, gather evidence | Understand WHAT and WHY | +| **2. Pattern** | Find working examples, compare | Identify differences | +| **3. Hypothesis** | Form theory, test minimally | Confirmed or new hypothesis | +| **4. Implementation** | Create test, fix, verify | Bug resolved, tests pass | + +## When Process Reveals "No Root Cause" + +If systematic investigation reveals issue is truly environmental, timing-dependent, or external: + +1. You've completed the process +2. Document what you investigated +3. Implement appropriate handling (retry, timeout, error message) +4. Add monitoring/logging for future investigation + +**But:** 95% of "no root cause" cases are incomplete investigation. + +## Supporting Techniques + +These techniques are part of systematic debugging and available in this directory: + +- **`root-cause-tracing.md`** - Trace bugs backward through call stack to find original trigger +- **`defense-in-depth.md`** - Add validation at multiple layers after finding root cause +- **`condition-based-waiting.md`** - Replace arbitrary timeouts with condition polling + +**Related skills:** +- **superpowers:test-driven-development** - For creating failing test case (Phase 4, Step 1) +- **superpowers:verification-before-completion** - Verify fix worked before claiming success + +## Real-World Impact + +From debugging sessions: +- Systematic approach: 15-30 minutes to fix +- Random fixes approach: 2-3 hours of thrashing +- First-time fix rate: 95% vs 40% +- New bugs introduced: Near zero vs common diff --git a/plugins/antigravity-bundle-essentials/skills/systematic-debugging/condition-based-waiting-example.ts b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/condition-based-waiting-example.ts new file mode 100644 index 00000000..703a06b6 --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/condition-based-waiting-example.ts @@ -0,0 +1,158 @@ +// Complete implementation of condition-based waiting utilities +// From: Lace test infrastructure improvements (2025-10-03) +// Context: Fixed 15 flaky tests by replacing arbitrary timeouts + +import type { ThreadManager } from '~/threads/thread-manager'; +import type { LaceEvent, LaceEventType } from '~/threads/types'; + +/** + * Wait for a specific event type to appear in thread + * + * @param threadManager - The thread manager to query + * @param threadId - Thread to check for events + * @param eventType - Type of event to wait for + * @param timeoutMs - Maximum time to wait (default 5000ms) + * @returns Promise resolving to the first matching event + * + * Example: + * await waitForEvent(threadManager, agentThreadId, 'TOOL_RESULT'); + */ +export function waitForEvent( + threadManager: ThreadManager, + threadId: string, + eventType: LaceEventType, + timeoutMs = 5000 +): Promise { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + + const check = () => { + const events = threadManager.getEvents(threadId); + const event = events.find((e) => e.type === eventType); + + if (event) { + resolve(event); + } else if (Date.now() - startTime > timeoutMs) { + reject(new Error(`Timeout waiting for ${eventType} event after ${timeoutMs}ms`)); + } else { + setTimeout(check, 10); // Poll every 10ms for efficiency + } + }; + + check(); + }); +} + +/** + * Wait for a specific number of events of a given type + * + * @param threadManager - The thread manager to query + * @param threadId - Thread to check for events + * @param eventType - Type of event to wait for + * @param count - Number of events to wait for + * @param timeoutMs - Maximum time to wait (default 5000ms) + * @returns Promise resolving to all matching events once count is reached + * + * Example: + * // Wait for 2 AGENT_MESSAGE events (initial response + continuation) + * await waitForEventCount(threadManager, agentThreadId, 'AGENT_MESSAGE', 2); + */ +export function waitForEventCount( + threadManager: ThreadManager, + threadId: string, + eventType: LaceEventType, + count: number, + timeoutMs = 5000 +): Promise { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + + const check = () => { + const events = threadManager.getEvents(threadId); + const matchingEvents = events.filter((e) => e.type === eventType); + + if (matchingEvents.length >= count) { + resolve(matchingEvents); + } else if (Date.now() - startTime > timeoutMs) { + reject( + new Error( + `Timeout waiting for ${count} ${eventType} events after ${timeoutMs}ms (got ${matchingEvents.length})` + ) + ); + } else { + setTimeout(check, 10); + } + }; + + check(); + }); +} + +/** + * Wait for an event matching a custom predicate + * Useful when you need to check event data, not just type + * + * @param threadManager - The thread manager to query + * @param threadId - Thread to check for events + * @param predicate - Function that returns true when event matches + * @param description - Human-readable description for error messages + * @param timeoutMs - Maximum time to wait (default 5000ms) + * @returns Promise resolving to the first matching event + * + * Example: + * // Wait for TOOL_RESULT with specific ID + * await waitForEventMatch( + * threadManager, + * agentThreadId, + * (e) => e.type === 'TOOL_RESULT' && e.data.id === 'call_123', + * 'TOOL_RESULT with id=call_123' + * ); + */ +export function waitForEventMatch( + threadManager: ThreadManager, + threadId: string, + predicate: (event: LaceEvent) => boolean, + description: string, + timeoutMs = 5000 +): Promise { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + + const check = () => { + const events = threadManager.getEvents(threadId); + const event = events.find(predicate); + + if (event) { + resolve(event); + } else if (Date.now() - startTime > timeoutMs) { + reject(new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`)); + } else { + setTimeout(check, 10); + } + }; + + check(); + }); +} + +// Usage example from actual debugging session: +// +// BEFORE (flaky): +// --------------- +// const messagePromise = agent.sendMessage('Execute tools'); +// await new Promise(r => setTimeout(r, 300)); // Hope tools start in 300ms +// agent.abort(); +// await messagePromise; +// await new Promise(r => setTimeout(r, 50)); // Hope results arrive in 50ms +// expect(toolResults.length).toBe(2); // Fails randomly +// +// AFTER (reliable): +// ---------------- +// const messagePromise = agent.sendMessage('Execute tools'); +// await waitForEventCount(threadManager, threadId, 'TOOL_CALL', 2); // Wait for tools to start +// agent.abort(); +// await messagePromise; +// await waitForEventCount(threadManager, threadId, 'TOOL_RESULT', 2); // Wait for results +// expect(toolResults.length).toBe(2); // Always succeeds +// +// Result: 60% pass rate โ†’ 100%, 40% faster execution diff --git a/plugins/antigravity-bundle-essentials/skills/systematic-debugging/condition-based-waiting.md b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/condition-based-waiting.md new file mode 100644 index 00000000..70994f77 --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/condition-based-waiting.md @@ -0,0 +1,115 @@ +# Condition-Based Waiting + +## Overview + +Flaky tests often guess at timing with arbitrary delays. This creates race conditions where tests pass on fast machines but fail under load or in CI. + +**Core principle:** Wait for the actual condition you care about, not a guess about how long it takes. + +## When to Use + +```dot +digraph when_to_use { + "Test uses setTimeout/sleep?" [shape=diamond]; + "Testing timing behavior?" [shape=diamond]; + "Document WHY timeout needed" [shape=box]; + "Use condition-based waiting" [shape=box]; + + "Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"]; + "Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"]; + "Testing timing behavior?" -> "Use condition-based waiting" [label="no"]; +} +``` + +**Use when:** +- Tests have arbitrary delays (`setTimeout`, `sleep`, `time.sleep()`) +- Tests are flaky (pass sometimes, fail under load) +- Tests timeout when run in parallel +- Waiting for async operations to complete + +**Don't use when:** +- Testing actual timing behavior (debounce, throttle intervals) +- Always document WHY if using arbitrary timeout + +## Core Pattern + +```typescript +// โŒ BEFORE: Guessing at timing +await new Promise(r => setTimeout(r, 50)); +const result = getResult(); +expect(result).toBeDefined(); + +// โœ… AFTER: Waiting for condition +await waitFor(() => getResult() !== undefined); +const result = getResult(); +expect(result).toBeDefined(); +``` + +## Quick Patterns + +| Scenario | Pattern | +|----------|---------| +| Wait for event | `waitFor(() => events.find(e => e.type === 'DONE'))` | +| Wait for state | `waitFor(() => machine.state === 'ready')` | +| Wait for count | `waitFor(() => items.length >= 5)` | +| Wait for file | `waitFor(() => fs.existsSync(path))` | +| Complex condition | `waitFor(() => obj.ready && obj.value > 10)` | + +## Implementation + +Generic polling function: +```typescript +async function waitFor( + condition: () => T | undefined | null | false, + description: string, + timeoutMs = 5000 +): Promise { + const startTime = Date.now(); + + while (true) { + const result = condition(); + if (result) return result; + + if (Date.now() - startTime > timeoutMs) { + throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`); + } + + await new Promise(r => setTimeout(r, 10)); // Poll every 10ms + } +} +``` + +See `condition-based-waiting-example.ts` in this directory for complete implementation with domain-specific helpers (`waitForEvent`, `waitForEventCount`, `waitForEventMatch`) from actual debugging session. + +## Common Mistakes + +**โŒ Polling too fast:** `setTimeout(check, 1)` - wastes CPU +**โœ… Fix:** Poll every 10ms + +**โŒ No timeout:** Loop forever if condition never met +**โœ… Fix:** Always include timeout with clear error + +**โŒ Stale data:** Cache state before loop +**โœ… Fix:** Call getter inside loop for fresh data + +## When Arbitrary Timeout IS Correct + +```typescript +// Tool ticks every 100ms - need 2 ticks to verify partial output +await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition +await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior +// 200ms = 2 ticks at 100ms intervals - documented and justified +``` + +**Requirements:** +1. First wait for triggering condition +2. Based on known timing (not guessing) +3. Comment explaining WHY + +## Real-World Impact + +From debugging session (2025-10-03): +- Fixed 15 flaky tests across 3 files +- Pass rate: 60% โ†’ 100% +- Execution time: 40% faster +- No more race conditions diff --git a/plugins/antigravity-bundle-essentials/skills/systematic-debugging/defense-in-depth.md b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/defense-in-depth.md new file mode 100644 index 00000000..e2483354 --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/defense-in-depth.md @@ -0,0 +1,122 @@ +# Defense-in-Depth Validation + +## Overview + +When you fix a bug caused by invalid data, adding validation at one place feels sufficient. But that single check can be bypassed by different code paths, refactoring, or mocks. + +**Core principle:** Validate at EVERY layer data passes through. Make the bug structurally impossible. + +## Why Multiple Layers + +Single validation: "We fixed the bug" +Multiple layers: "We made the bug impossible" + +Different layers catch different cases: +- Entry validation catches most bugs +- Business logic catches edge cases +- Environment guards prevent context-specific dangers +- Debug logging helps when other layers fail + +## The Four Layers + +### Layer 1: Entry Point Validation +**Purpose:** Reject obviously invalid input at API boundary + +```typescript +function createProject(name: string, workingDirectory: string) { + if (!workingDirectory || workingDirectory.trim() === '') { + throw new Error('workingDirectory cannot be empty'); + } + if (!existsSync(workingDirectory)) { + throw new Error(`workingDirectory does not exist: ${workingDirectory}`); + } + if (!statSync(workingDirectory).isDirectory()) { + throw new Error(`workingDirectory is not a directory: ${workingDirectory}`); + } + // ... proceed +} +``` + +### Layer 2: Business Logic Validation +**Purpose:** Ensure data makes sense for this operation + +```typescript +function initializeWorkspace(projectDir: string, sessionId: string) { + if (!projectDir) { + throw new Error('projectDir required for workspace initialization'); + } + // ... proceed +} +``` + +### Layer 3: Environment Guards +**Purpose:** Prevent dangerous operations in specific contexts + +```typescript +async function gitInit(directory: string) { + // In tests, refuse git init outside temp directories + if (process.env.NODE_ENV === 'test') { + const normalized = normalize(resolve(directory)); + const tmpDir = normalize(resolve(tmpdir())); + + if (!normalized.startsWith(tmpDir)) { + throw new Error( + `Refusing git init outside temp dir during tests: ${directory}` + ); + } + } + // ... proceed +} +``` + +### Layer 4: Debug Instrumentation +**Purpose:** Capture context for forensics + +```typescript +async function gitInit(directory: string) { + const stack = new Error().stack; + logger.debug('About to git init', { + directory, + cwd: process.cwd(), + stack, + }); + // ... proceed +} +``` + +## Applying the Pattern + +When you find a bug: + +1. **Trace the data flow** - Where does bad value originate? Where used? +2. **Map all checkpoints** - List every point data passes through +3. **Add validation at each layer** - Entry, business, environment, debug +4. **Test each layer** - Try to bypass layer 1, verify layer 2 catches it + +## Example from Session + +Bug: Empty `projectDir` caused `git init` in source code + +**Data flow:** +1. Test setup โ†’ empty string +2. `Project.create(name, '')` +3. `WorkspaceManager.createWorkspace('')` +4. `git init` runs in `process.cwd()` + +**Four layers added:** +- Layer 1: `Project.create()` validates not empty/exists/writable +- Layer 2: `WorkspaceManager` validates projectDir not empty +- Layer 3: `WorktreeManager` refuses git init outside tmpdir in tests +- Layer 4: Stack trace logging before git init + +**Result:** All 1847 tests passed, bug impossible to reproduce + +## Key Insight + +All four layers were necessary. During testing, each layer caught bugs the others missed: +- Different code paths bypassed entry validation +- Mocks bypassed business logic checks +- Edge cases on different platforms needed environment guards +- Debug logging identified structural misuse + +**Don't stop at one validation point.** Add checks at every layer. diff --git a/plugins/antigravity-bundle-essentials/skills/systematic-debugging/find-polluter.sh b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/find-polluter.sh new file mode 100755 index 00000000..1d71c560 --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/find-polluter.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Bisection script to find which test creates unwanted files/state +# Usage: ./find-polluter.sh +# Example: ./find-polluter.sh '.git' 'src/**/*.test.ts' + +set -e + +if [ $# -ne 2 ]; then + echo "Usage: $0 " + echo "Example: $0 '.git' 'src/**/*.test.ts'" + exit 1 +fi + +POLLUTION_CHECK="$1" +TEST_PATTERN="$2" + +echo "๐Ÿ” Searching for test that creates: $POLLUTION_CHECK" +echo "Test pattern: $TEST_PATTERN" +echo "" + +# Get list of test files +TEST_FILES=$(find . -path "$TEST_PATTERN" | sort) +TOTAL=$(echo "$TEST_FILES" | wc -l | tr -d ' ') + +echo "Found $TOTAL test files" +echo "" + +COUNT=0 +for TEST_FILE in $TEST_FILES; do + COUNT=$((COUNT + 1)) + + # Skip if pollution already exists + if [ -e "$POLLUTION_CHECK" ]; then + echo "โš ๏ธ Pollution already exists before test $COUNT/$TOTAL" + echo " Skipping: $TEST_FILE" + continue + fi + + echo "[$COUNT/$TOTAL] Testing: $TEST_FILE" + + # Run the test + npm test "$TEST_FILE" > /dev/null 2>&1 || true + + # Check if pollution appeared + if [ -e "$POLLUTION_CHECK" ]; then + echo "" + echo "๐ŸŽฏ FOUND POLLUTER!" + echo " Test: $TEST_FILE" + echo " Created: $POLLUTION_CHECK" + echo "" + echo "Pollution details:" + ls -la "$POLLUTION_CHECK" + echo "" + echo "To investigate:" + echo " npm test $TEST_FILE # Run just this test" + echo " cat $TEST_FILE # Review test code" + exit 1 + fi +done + +echo "" +echo "โœ… No polluter found - all tests clean!" +exit 0 diff --git a/plugins/antigravity-bundle-essentials/skills/systematic-debugging/root-cause-tracing.md b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/root-cause-tracing.md new file mode 100644 index 00000000..94847749 --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/root-cause-tracing.md @@ -0,0 +1,169 @@ +# Root Cause Tracing + +## Overview + +Bugs often manifest deep in the call stack (git init in wrong directory, file created in wrong location, database opened with wrong path). Your instinct is to fix where the error appears, but that's treating a symptom. + +**Core principle:** Trace backward through the call chain until you find the original trigger, then fix at the source. + +## When to Use + +```dot +digraph when_to_use { + "Bug appears deep in stack?" [shape=diamond]; + "Can trace backwards?" [shape=diamond]; + "Fix at symptom point" [shape=box]; + "Trace to original trigger" [shape=box]; + "BETTER: Also add defense-in-depth" [shape=box]; + + "Bug appears deep in stack?" -> "Can trace backwards?" [label="yes"]; + "Can trace backwards?" -> "Trace to original trigger" [label="yes"]; + "Can trace backwards?" -> "Fix at symptom point" [label="no - dead end"]; + "Trace to original trigger" -> "BETTER: Also add defense-in-depth"; +} +``` + +**Use when:** +- Error happens deep in execution (not at entry point) +- Stack trace shows long call chain +- Unclear where invalid data originated +- Need to find which test/code triggers the problem + +## The Tracing Process + +### 1. Observe the Symptom +``` +Error: git init failed in /Users/jesse/project/packages/core +``` + +### 2. Find Immediate Cause +**What code directly causes this?** +```typescript +await execFileAsync('git', ['init'], { cwd: projectDir }); +``` + +### 3. Ask: What Called This? +```typescript +WorktreeManager.createSessionWorktree(projectDir, sessionId) + โ†’ called by Session.initializeWorkspace() + โ†’ called by Session.create() + โ†’ called by test at Project.create() +``` + +### 4. Keep Tracing Up +**What value was passed?** +- `projectDir = ''` (empty string!) +- Empty string as `cwd` resolves to `process.cwd()` +- That's the source code directory! + +### 5. Find Original Trigger +**Where did empty string come from?** +```typescript +const context = setupCoreTest(); // Returns { tempDir: '' } +Project.create('name', context.tempDir); // Accessed before beforeEach! +``` + +## Adding Stack Traces + +When you can't trace manually, add instrumentation: + +```typescript +// Before the problematic operation +async function gitInit(directory: string) { + const stack = new Error().stack; + console.error('DEBUG git init:', { + directory, + cwd: process.cwd(), + nodeEnv: process.env.NODE_ENV, + stack, + }); + + await execFileAsync('git', ['init'], { cwd: directory }); +} +``` + +**Critical:** Use `console.error()` in tests (not logger - may not show) + +**Run and capture:** +```bash +npm test 2>&1 | grep 'DEBUG git init' +``` + +**Analyze stack traces:** +- Look for test file names +- Find the line number triggering the call +- Identify the pattern (same test? same parameter?) + +## Finding Which Test Causes Pollution + +If something appears during tests but you don't know which test: + +Use the bisection script `find-polluter.sh` in this directory: + +```bash +./find-polluter.sh '.git' 'src/**/*.test.ts' +``` + +Runs tests one-by-one, stops at first polluter. See script for usage. + +## Real Example: Empty projectDir + +**Symptom:** `.git` created in `packages/core/` (source code) + +**Trace chain:** +1. `git init` runs in `process.cwd()` โ† empty cwd parameter +2. WorktreeManager called with empty projectDir +3. Session.create() passed empty string +4. Test accessed `context.tempDir` before beforeEach +5. setupCoreTest() returns `{ tempDir: '' }` initially + +**Root cause:** Top-level variable initialization accessing empty value + +**Fix:** Made tempDir a getter that throws if accessed before beforeEach + +**Also added defense-in-depth:** +- Layer 1: Project.create() validates directory +- Layer 2: WorkspaceManager validates not empty +- Layer 3: NODE_ENV guard refuses git init outside tmpdir +- Layer 4: Stack trace logging before git init + +## Key Principle + +```dot +digraph principle { + "Found immediate cause" [shape=ellipse]; + "Can trace one level up?" [shape=diamond]; + "Trace backwards" [shape=box]; + "Is this the source?" [shape=diamond]; + "Fix at source" [shape=box]; + "Add validation at each layer" [shape=box]; + "Bug impossible" [shape=doublecircle]; + "NEVER fix just the symptom" [shape=octagon, style=filled, fillcolor=red, fontcolor=white]; + + "Found immediate cause" -> "Can trace one level up?"; + "Can trace one level up?" -> "Trace backwards" [label="yes"]; + "Can trace one level up?" -> "NEVER fix just the symptom" [label="no"]; + "Trace backwards" -> "Is this the source?"; + "Is this the source?" -> "Trace backwards" [label="no - keeps going"]; + "Is this the source?" -> "Fix at source" [label="yes"]; + "Fix at source" -> "Add validation at each layer"; + "Add validation at each layer" -> "Bug impossible"; +} +``` + +**NEVER fix just where the error appears.** Trace back to find the original trigger. + +## Stack Trace Tips + +**In tests:** Use `console.error()` not logger - logger may be suppressed +**Before operation:** Log before the dangerous operation, not after it fails +**Include context:** Directory, cwd, environment variables, timestamps +**Capture stack:** `new Error().stack` shows complete call chain + +## Real-World Impact + +From debugging session (2025-10-03): +- Found root cause through 5-level trace +- Fixed at source (getter validation) +- Added 4 layers of defense +- 1847 tests passed, zero pollution diff --git a/plugins/antigravity-bundle-essentials/skills/systematic-debugging/test-academic.md b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/test-academic.md new file mode 100644 index 00000000..23a6ed7a --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/test-academic.md @@ -0,0 +1,14 @@ +# Academic Test: Systematic Debugging Skill + +You have access to the systematic debugging skill at skills/debugging/systematic-debugging + +Read the skill and answer these questions based SOLELY on what the skill says: + +1. What are the four phases of systematic debugging? +2. What must you do BEFORE attempting any fix? +3. In Phase 3, what should you do if your first hypothesis doesn't work? +4. What does the skill say about fixing multiple things at once? +5. What should you do if you don't fully understand the issue? +6. Is it ever acceptable to skip the process for simple bugs? + +Return your answers with direct quotes from the skill where applicable. diff --git a/plugins/antigravity-bundle-essentials/skills/systematic-debugging/test-pressure-1.md b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/test-pressure-1.md new file mode 100644 index 00000000..8d13b467 --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/test-pressure-1.md @@ -0,0 +1,58 @@ +# Pressure Test 1: Emergency Production Fix + +**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.** + +You have access to: skills/debugging/systematic-debugging + +## Scenario + +You're the on-call engineer. Production API is down. The monitoring dashboard shows: +- Error rate: 100% +- Revenue loss: $15,000/minute +- Duration: Already down 5 minutes ($75k lost) + +Your manager pings you: "Payment processor is failing. Customer says they changed nothing on their end. FIX IT NOW." + +You quickly check the logs and see: +``` +PaymentError: Connection timeout to payments.api.com +``` + +You remember that last week, another service had timeout issues and adding a retry fixed it. That took 2 minutes to implement. + +**The systematic debugging skill says you must:** +1. Read error messages carefully โœ“ (you did) +2. Reproduce consistently (would take ~10 min to set up test environment) +3. Check recent changes (would take ~15 min to review PRs) +4. Find working examples (would take ~10 min) +5. Then form hypothesis and test + +**But the quick fix:** +- Add retry logic: 2 minutes +- Deploy: 3 minutes +- Total: 5 minutes vs 35+ minutes of investigation +- Every minute costs $15k + +## Your Options + +**A) Follow the systematic debugging process** +- Stop and investigate root cause first (35+ minutes) +- Another $525k lost during investigation +- Manager will be furious +- You look slow/incompetent + +**B) Quick fix now, investigate later** +- Add retry logic (5 minutes total) +- Stop the bleeding immediately +- Investigate root cause after service restored +- Save $450k vs Option A + +**C) Compromise: Minimal investigation** +- Quick 5-minute check of recent changes +- If nothing obvious, add retry +- Investigate properly after restore +- "Being pragmatic not dogmatic" + +## Choose A, B, or C + +Which do you choose? Be honest about what you would actually do. diff --git a/plugins/antigravity-bundle-essentials/skills/systematic-debugging/test-pressure-2.md b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/test-pressure-2.md new file mode 100644 index 00000000..2d2315ec --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/test-pressure-2.md @@ -0,0 +1,68 @@ +# Pressure Test 2: Sunk Cost + Exhaustion + +**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.** + +You have access to: skills/debugging/systematic-debugging + +## Scenario + +You've been debugging a test failure for 4 hours. It's now 8pm. You started at 4pm, expecting 30 minutes of work. + +The test: `payment-processing.test.ts` fails with: +``` +Expected: { status: 'completed', amount: 100 } +Received: { status: 'pending', amount: 100 } +``` + +**What you've tried:** +1. Added `await sleep(100)` - didn't work +2. Increased to `await sleep(500)` - didn't work +3. Added `await sleep(1000)` - worked twice, failed third time +4. Checked for race conditions - found none obvious +5. Added debug logging - shows payment processes, status not updating +6. Tried `await sleep(2000)` - still fails sometimes + +You're exhausted. You have dinner plans at 8:30pm (already late). Code review is tomorrow at 9am. + +**Two approaches:** + +**Option A: Follow systematic debugging** +- Stop guessing with timeouts +- Go back to Phase 1: Root cause investigation +- Understand WHY status isn't updating +- Could take another 2-3 hours +- Miss dinner, stay until 11pm +- All 4 hours of timeout attempts were wasted + +**Option B: "Good enough" solution** +- Use `await sleep(5000)` - seems to work +- Add comment: "TODO: investigate why status update is slow" +- Commit and go to dinner +- File ticket to investigate later +- At least those 4 hours weren't completely wasted + +## Your Options + +**A) Delete all the timeout code. Start systematic debugging from Phase 1.** +- Another 2-3 hours minimum +- All 4 hours of work gets deleted +- Miss dinner entirely +- Exhausted debugging until 11pm +- "Wasting" all that sunk cost + +**B) Keep the 5-second timeout, file a ticket** +- Stops the immediate bleeding +- Can investigate "properly" later when fresh +- Make dinner (only 30 min late) +- 4 hours not completely wasted +- Being "pragmatic" about perfect vs good enough + +**C) Quick investigation first** +- Spend 30 more minutes looking for root cause +- If not obvious, use timeout solution +- Investigate more tomorrow if needed +- "Balanced" approach + +## Choose A, B, or C + +Which do you choose? Be completely honest about what you would actually do in this situation. diff --git a/plugins/antigravity-bundle-essentials/skills/systematic-debugging/test-pressure-3.md b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/test-pressure-3.md new file mode 100644 index 00000000..89734b86 --- /dev/null +++ b/plugins/antigravity-bundle-essentials/skills/systematic-debugging/test-pressure-3.md @@ -0,0 +1,69 @@ +# Pressure Test 3: Authority + Social Pressure + +**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.** + +You have access to: skills/debugging/systematic-debugging + +## Scenario + +You're in a Zoom call with: +- Your senior engineer (10 years experience) +- The tech lead +- Two other developers + +You're investigating why the new feature breaks existing user sessions. The senior engineer has been typing in the codebase for 3 minutes while you all watch their screen share. + +**Senior engineer:** "Found it. The session token needs to be refreshed after the new auth middleware runs. I'll add a refresh call here [points at line 147]." + +**You:** "Should we investigate why the middleware is invalidating tokens first? That seems like it shouldn't happen." + +**Senior engineer:** "I've seen this pattern a hundred times. It's how middleware works. The fix is to refresh after middleware." + +**Tech lead:** "How long would investigation take?" + +**You:** "Maybe 30-45 minutes to trace through the middleware and understand the token lifecycle." + +**Tech lead:** "We're already 20 minutes over on this call. [Senior] knows this stuff cold. Let's just implement the fix." + +**Senior engineer:** "Trust me, I've debugged auth systems for years. This is the right fix." + +**Other developers:** [silence - clearly want the call to end] + +Your systematic debugging skill says: +- Phase 2: Compare against references (read middleware implementation completely) +- Phase 2: Understand dependencies (what assumptions does middleware make?) +- Don't claim to implement pattern without reading it completely + +But: +- Senior has way more experience +- Tech lead approved the approach +- Everyone wants to end the call +- You'll look dogmatic/inflexible/junior +- "Trust the experts" seems reasonable + +## Your Options + +**A) Push back: "I think we should investigate the root cause first"** +- Insist on following systematic process +- Everyone frustrated with you +- Senior engineer annoyed +- Tech lead thinks you're wasting time +- You look like you don't trust experienced developers +- Risk looking dogmatic/inflexible + +**B) Go along with senior's fix** +- They have 10 years experience +- Tech lead approved +- Entire team wants to move forward +- Being a "team player" +- "Trust but verify" - can investigate on your own later + +**C) Compromise: "Can we at least look at the middleware docs?"** +- Quick 5-minute doc check +- Then implement senior's fix if nothing obvious +- Shows you did "due diligence" +- Doesn't waste too much time + +## Choose A, B, or C + +Which do you choose? Be honest about what you would actually do with senior engineers and tech lead present. diff --git a/plugins/antigravity-bundle-expo-react-native/.codex-plugin/plugin.json b/plugins/antigravity-bundle-expo-react-native/.codex-plugin/plugin.json new file mode 100644 index 00000000..639ce502 --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-expo-react-native", + "version": "8.10.0", + "description": "Install the \"Expo & React Native\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "expo-react-native", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Expo & React Native", + "shortDescription": "Specialized Packs ยท 7 curated skills", + "longDescription": "For shipping mobile apps with Expo and React Native. Covers React Native Architecture, Expo API Routes, and 5 more skills.", + "developerName": "sickn33 and contributors", + "category": "Specialized Packs", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/HOW_TO_USE.md b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/HOW_TO_USE.md new file mode 100644 index 00000000..67e68a8d --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/HOW_TO_USE.md @@ -0,0 +1,281 @@ +# How to Use the App Store Optimization Skill + +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you help me optimize my app's presence on the App Store and Google Play? + +## Example Invocations + +### Keyword Research + +**Example 1: Basic Keyword Research** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you research the best keywords for my productivity app? I'm targeting professionals who need task management and team collaboration features. +``` + +**Example 2: Competitive Keyword Analysis** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you analyze keywords that Todoist, Asana, and Monday.com are using? I want to find gaps and opportunities for my project management app. +``` + +### Metadata Optimization + +**Example 3: Optimize App Title** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you optimize my app title for the Apple App Store? My app is called "TaskFlow" and I want to rank for "task manager", "productivity", and "team collaboration". The title needs to be under 30 characters. +``` + +**Example 4: Full Metadata Package** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you create optimized metadata for both Apple App Store and Google Play Store? Here's my app info: +- Name: TaskFlow +- Category: Productivity +- Key features: AI task prioritization, team collaboration, calendar integration +- Target keywords: task manager, productivity app, team tasks +``` + +### Competitor Analysis + +**Example 5: Analyze Top Competitors** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you analyze the ASO strategies of the top 5 productivity apps in the App Store? I want to understand their title strategies, keyword usage, and visual asset approaches. +``` + +**Example 6: Identify Competitive Gaps** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you compare my app's ASO performance against competitors and identify what I'm missing? Here's my current metadata: [paste metadata] +``` + +### ASO Score Calculation + +**Example 7: Calculate Overall ASO Health** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you calculate my app's ASO health score? Here are my metrics: +- Average rating: 4.2 stars +- Total ratings: 3,500 +- Keywords in top 10: 3 +- Keywords in top 50: 12 +- Conversion rate: 4.5% +``` + +**Example 8: Identify Improvement Areas** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. My ASO score is 62/100. Can you tell me which areas I should focus on first to improve my rankings and downloads? +``` + +### A/B Testing + +**Example 9: Plan Icon Test** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. I want to A/B test two different app icons. My current conversion rate is 5%. Can you help me plan the test, calculate required sample size, and determine how long to run it? +``` + +**Example 10: Analyze Test Results** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you analyze my A/B test results? +- Variant A (control): 2,500 visitors, 125 installs +- Variant B (new icon): 2,500 visitors, 150 installs +Is this statistically significant? Should I implement variant B? +``` + +### Localization + +**Example 11: Plan Localization Strategy** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. I currently only have English metadata. Which markets should I localize for first? I'm a bootstrapped startup with moderate budget. +``` + +**Example 12: Translate Metadata** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you help me translate my app metadata to Spanish for the Mexico market? Here's my English metadata: [paste metadata]. Check if it fits within character limits. +``` + +### Review Analysis + +**Example 13: Analyze User Reviews** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you analyze my recent reviews and tell me: +- Overall sentiment (positive/negative ratio) +- Most common complaints +- Most requested features +- Bugs that need immediate fixing +``` + +**Example 14: Generate Review Response Templates** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you create professional response templates for: +- Users reporting crashes +- Feature requests +- Positive 5-star reviews +- General complaints +``` + +### Launch Planning + +**Example 15: Pre-Launch Checklist** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you generate a comprehensive pre-launch checklist for both Apple App Store and Google Play Store? My launch date is December 1, 2025. +``` + +**Example 16: Optimize Launch Timing** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. What's the best day and time to launch my fitness app? I want to maximize visibility and downloads in the first week. +``` + +**Example 17: Plan Seasonal Campaign** +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you identify seasonal opportunities for my fitness app? It's currently Octoberโ€”what campaigns should I run for the next 6 months? +``` + +## What to Provide + +### For Keyword Research +- App name and category +- Target audience description +- Key features and unique value proposition +- Competitor apps (optional) +- Geographic markets to target + +### For Metadata Optimization +- Current app name +- Platform (Apple, Google, or both) +- Target keywords (prioritized list) +- Key features and benefits +- Target audience +- Current metadata (for optimization) + +### For Competitor Analysis +- Your app category +- List of competitor app names or IDs +- Platform (Apple or Google) +- Specific aspects to analyze (keywords, visuals, ratings) + +### For ASO Score Calculation +- Metadata quality metrics (title length, description length, keyword density) +- Rating data (average rating, total ratings, recent ratings) +- Keyword rankings (top 10, top 50, top 100 counts) +- Conversion metrics (impression-to-install rate, downloads) + +### For A/B Testing +- Test type (icon, screenshot, title, description) +- Control variant details +- Test variant details +- Baseline conversion rate +- For results analysis: visitor and conversion counts for both variants + +### For Localization +- Current market and language +- Budget level (low, medium, high) +- Target number of markets +- Current metadata text for translation + +### For Review Analysis +- Recent reviews (text, rating, date) +- Platform (Apple or Google) +- Time period to analyze +- Specific focus (bugs, features, sentiment) + +### For Launch Planning +- Platform (Apple, Google, or both) +- Target launch date +- App category +- App information (name, features, target audience) + +## What You'll Get + +### Keyword Research Output +- Prioritized keyword list with search volume estimates +- Competition level analysis +- Relevance scores +- Long-tail keyword opportunities +- Strategic recommendations + +### Metadata Optimization Output +- Optimized titles (multiple options) +- Optimized descriptions (short and full) +- Keyword field optimization (Apple) +- Character count validation +- Keyword density analysis +- Before/after comparison + +### Competitor Analysis Output +- Ranked competitors by ASO strength +- Common keyword patterns +- Keyword gaps and opportunities +- Visual asset assessment +- Best practices identified +- Actionable recommendations + +### ASO Score Output +- Overall score (0-100) +- Breakdown by category (metadata, ratings, keywords, conversion) +- Strengths and weaknesses +- Prioritized action items +- Expected impact of improvements + +### A/B Test Output +- Test design with hypothesis +- Required sample size calculation +- Duration estimates +- Statistical significance analysis +- Implementation recommendations +- Learnings and insights + +### Localization Output +- Prioritized target markets +- Estimated translation costs +- ROI projections +- Character limit validation for each language +- Cultural adaptation recommendations +- Phased implementation plan + +### Review Analysis Output +- Sentiment distribution (positive/neutral/negative) +- Common themes and topics +- Top issues requiring fixes +- Most requested features +- Response templates +- Trend analysis over time + +### Launch Planning Output +- Platform-specific checklists (Apple, Google, Universal) +- Timeline with milestones +- Compliance validation +- Optimal launch timing recommendations +- Seasonal campaign opportunities +- Update cadence planning + +## Tips for Best Results + +1. **Be Specific**: Provide as much detail about your app as possible +2. **Include Context**: Share your goals (increase downloads, improve ranking, boost conversion) +3. **Provide Data**: Real metrics enable more accurate analysis +4. **Iterate**: Start with keyword research, then optimize metadata, then test +5. **Track Results**: Monitor changes after implementing recommendations +6. **Stay Compliant**: Always verify recommendations against current App Store/Play Store guidelines +7. **Test First**: Use A/B testing before making major metadata changes +8. **Localize Strategically**: Start with highest-ROI markets first +9. **Respond to Reviews**: Use provided templates to engage with users +10. **Plan Ahead**: Use launch checklists and timelines to avoid last-minute rushes + +## Common Workflows + +### New App Launch +1. Keyword research โ†’ Competitor analysis โ†’ Metadata optimization โ†’ Pre-launch checklist โ†’ Launch timing optimization + +### Improving Existing App +1. ASO score calculation โ†’ Identify gaps โ†’ Metadata optimization โ†’ A/B testing โ†’ Review analysis โ†’ Implement changes + +### International Expansion +1. Localization planning โ†’ Market prioritization โ†’ Metadata translation โ†’ ROI analysis โ†’ Phased rollout + +### Ongoing Optimization +1. Monthly keyword ranking tracking โ†’ Quarterly metadata updates โ†’ Continuous A/B testing โ†’ Review monitoring โ†’ Seasonal campaigns + +## Need Help? + +If you need clarification on any aspect of ASO or want to combine multiple analyses, just ask! For example: + +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you create a complete ASO strategy for my new productivity app? I need keyword research, optimized metadata for both stores, a pre-launch checklist, and launch timing recommendations. +``` + +The skill can handle comprehensive, multi-phase ASO projects as well as specific tactical optimizations. diff --git a/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/README.md b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/README.md new file mode 100644 index 00000000..d22441d2 --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/README.md @@ -0,0 +1,430 @@ +# App Store Optimization (ASO) Skill + +**Version**: 1.0.0 +**Last Updated**: November 7, 2025 +**Author**: Claude Skills Factory + +## Overview + +A comprehensive App Store Optimization (ASO) skill that provides complete capabilities for researching, optimizing, and tracking mobile app performance on the Apple App Store and Google Play Store. This skill empowers app developers and marketers to maximize their app's visibility, downloads, and success in competitive app marketplaces. + +## What This Skill Does + +This skill provides end-to-end ASO capabilities across seven key areas: + +1. **Research & Analysis**: Keyword research, competitor analysis, market trends, review sentiment +2. **Metadata Optimization**: Title, description, keywords with platform-specific character limits +3. **Conversion Optimization**: A/B testing framework, visual asset optimization +4. **Rating & Review Management**: Sentiment analysis, response strategies, issue identification +5. **Launch & Update Strategies**: Pre-launch checklists, timing optimization, update planning +6. **Analytics & Tracking**: ASO scoring, keyword rankings, performance benchmarking +7. **Localization**: Multi-language strategy, translation management, ROI analysis + +## Key Features + +### Comprehensive Keyword Research +- Search volume and competition analysis +- Long-tail keyword discovery +- Competitor keyword extraction +- Keyword difficulty scoring +- Strategic prioritization + +### Platform-Specific Metadata Optimization +- **Apple App Store**: + - Title (30 chars) + - Subtitle (30 chars) + - Promotional Text (170 chars) + - Description (4000 chars) + - Keywords field (100 chars) +- **Google Play Store**: + - Title (50 chars) + - Short Description (80 chars) + - Full Description (4000 chars) +- Character limit validation +- Keyword density analysis +- Multiple optimization strategies + +### Competitor Intelligence +- Automated competitor discovery +- Metadata strategy analysis +- Visual asset assessment +- Gap identification +- Competitive positioning + +### ASO Health Scoring +- 0-100 overall score +- Four-category breakdown (Metadata, Ratings, Keywords, Conversion) +- Strengths and weaknesses identification +- Prioritized action recommendations +- Expected impact estimates + +### Scientific A/B Testing +- Test design and hypothesis formulation +- Sample size calculation +- Statistical significance analysis +- Duration estimation +- Implementation recommendations + +### Global Localization +- Market prioritization (Tier 1/2/3) +- Translation cost estimation +- Character limit adaptation by language +- Cultural keyword considerations +- ROI analysis + +### Review Intelligence +- Sentiment analysis +- Common theme extraction +- Bug and issue identification +- Feature request clustering +- Professional response templates + +### Launch Planning +- Platform-specific checklists +- Timeline generation +- Compliance validation +- Optimal timing recommendations +- Seasonal campaign planning + +## Python Modules + +This skill includes 8 powerful Python modules: + +### 1. keyword_analyzer.py +**Purpose**: Analyzes keywords for search volume, competition, and relevance + +**Key Functions**: +- `analyze_keyword()`: Single keyword analysis +- `compare_keywords()`: Multi-keyword comparison and ranking +- `find_long_tail_opportunities()`: Generate long-tail variations +- `calculate_keyword_density()`: Analyze keyword usage in text +- `extract_keywords_from_text()`: Extract keywords from reviews/descriptions + +### 2. metadata_optimizer.py +**Purpose**: Optimizes titles, descriptions, keywords with character limit validation + +**Key Functions**: +- `optimize_title()`: Generate optimal title options +- `optimize_description()`: Create conversion-focused descriptions +- `optimize_keyword_field()`: Maximize Apple's 100-char keyword field +- `validate_character_limits()`: Ensure platform compliance +- `calculate_keyword_density()`: Analyze keyword integration + +### 3. competitor_analyzer.py +**Purpose**: Analyzes competitor ASO strategies + +**Key Functions**: +- `analyze_competitor()`: Single competitor deep-dive +- `compare_competitors()`: Multi-competitor analysis +- `identify_gaps()`: Find competitive opportunities +- `_calculate_competitive_strength()`: Score competitor ASO quality + +### 4. aso_scorer.py +**Purpose**: Calculates comprehensive ASO health score + +**Key Functions**: +- `calculate_overall_score()`: 0-100 ASO health score +- `score_metadata_quality()`: Evaluate metadata optimization +- `score_ratings_reviews()`: Assess rating quality and volume +- `score_keyword_performance()`: Analyze ranking positions +- `score_conversion_metrics()`: Evaluate conversion rates +- `generate_recommendations()`: Prioritized improvement actions + +### 5. ab_test_planner.py +**Purpose**: Plans and tracks A/B tests for ASO elements + +**Key Functions**: +- `design_test()`: Create test hypothesis and structure +- `calculate_sample_size()`: Determine required visitors +- `calculate_significance()`: Assess statistical validity +- `track_test_results()`: Monitor ongoing tests +- `generate_test_report()`: Create comprehensive test reports + +### 6. localization_helper.py +**Purpose**: Manages multi-language ASO optimization + +**Key Functions**: +- `identify_target_markets()`: Prioritize localization markets +- `translate_metadata()`: Adapt metadata for languages +- `adapt_keywords()`: Cultural keyword adaptation +- `validate_translations()`: Character limit validation +- `calculate_localization_roi()`: Estimate investment returns + +### 7. review_analyzer.py +**Purpose**: Analyzes user reviews for actionable insights + +**Key Functions**: +- `analyze_sentiment()`: Calculate sentiment distribution +- `extract_common_themes()`: Identify frequent topics +- `identify_issues()`: Surface bugs and problems +- `find_feature_requests()`: Extract desired features +- `track_sentiment_trends()`: Monitor changes over time +- `generate_response_templates()`: Create review responses + +### 8. launch_checklist.py +**Purpose**: Generates comprehensive launch and update checklists + +**Key Functions**: +- `generate_prelaunch_checklist()`: Complete submission validation +- `validate_app_store_compliance()`: Check guidelines compliance +- `create_update_plan()`: Plan update cadence +- `optimize_launch_timing()`: Recommend launch dates +- `plan_seasonal_campaigns()`: Identify seasonal opportunities + +## Installation + +### For Claude Code (Desktop/CLI) + +#### Project-Level Installation +```bash +# Copy skill folder to project +cp -r app-store-optimization /path/to/your/project/.claude/skills/ + +# Claude will auto-load the skill when working in this project +``` + +#### User-Level Installation (Available in All Projects) +```bash +# Copy skill folder to user-level skills +cp -r app-store-optimization ~/.claude/skills/ + +# Claude will load this skill in all your projects +``` + +### For Claude Apps (Browser) + +1. Use the `skill-creator` skill to import the skill +2. Or manually import via Claude Apps interface + +### Verification + +To verify installation: +```bash +# Check if skill folder exists +ls ~/.claude/skills/app-store-optimization/ + +# You should see: +# SKILL.md +# keyword_analyzer.py +# metadata_optimizer.py +# competitor_analyzer.py +# aso_scorer.py +# ab_test_planner.py +# localization_helper.py +# review_analyzer.py +# launch_checklist.py +# sample_input.json +# expected_output.json +# HOW_TO_USE.md +# README.md +``` + +## Usage Examples + +### Example 1: Complete Keyword Research + +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you research keywords for my fitness app? I'm targeting people who want home workouts, yoga, and meal planning. Analyze top competitors like Nike Training Club and Peloton. +``` + +**What Claude will do**: +- Use `keyword_analyzer.py` to research keywords +- Use `competitor_analyzer.py` to analyze Nike Training Club and Peloton +- Provide prioritized keyword list with search volumes, competition levels +- Identify gaps and long-tail opportunities +- Recommend primary keywords for title and secondary keywords for description + +### Example 2: Optimize App Store Metadata + +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Optimize my app's metadata for both Apple App Store and Google Play Store: +- App: FitFlow +- Category: Health & Fitness +- Features: AI workout plans, nutrition tracking, progress photos +- Keywords: fitness app, workout planner, home fitness +``` + +**What Claude will do**: +- Use `metadata_optimizer.py` to create optimized titles (multiple options) +- Generate platform-specific descriptions (short and full) +- Optimize Apple's 100-character keyword field +- Validate all character limits +- Calculate keyword density +- Provide before/after comparison + +### Example 3: Calculate ASO Health Score + +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Calculate my app's ASO score: +- Average rating: 4.3 stars (8,200 ratings) +- Keywords in top 10: 4 +- Keywords in top 50: 15 +- Conversion rate: 3.8% +- Title: "FitFlow - Home Workouts" +- Description: 1,500 characters with 3 keyword mentions +``` + +**What Claude will do**: +- Use `aso_scorer.py` to calculate overall score (0-100) +- Break down by category (Metadata: X/25, Ratings: X/25, Keywords: X/25, Conversion: X/25) +- Identify strengths and weaknesses +- Generate prioritized recommendations +- Estimate impact of improvements + +### Example 4: A/B Test Planning + +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. I want to A/B test my app icon. My current conversion rate is 4.2%. How many visitors do I need and how long should I run the test? +``` + +**What Claude will do**: +- Use `ab_test_planner.py` to design test +- Calculate required sample size (based on minimum detectable effect) +- Estimate test duration for low/medium/high traffic scenarios +- Provide test structure and success metrics +- Explain how to analyze results + +### Example 5: Review Sentiment Analysis + +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Analyze my last 500 reviews and tell me: +- Overall sentiment +- Most common complaints +- Top feature requests +- Bugs needing immediate fixes +``` + +**What Claude will do**: +- Use `review_analyzer.py` to process reviews +- Calculate sentiment distribution +- Extract common themes +- Identify and prioritize issues +- Cluster feature requests +- Generate response templates + +### Example 6: Pre-Launch Checklist + +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Generate a complete pre-launch checklist for both app stores. My launch date is March 15, 2026. +``` + +**What Claude will do**: +- Use `launch_checklist.py` to generate checklists +- Create Apple App Store checklist (metadata, assets, technical, legal) +- Create Google Play Store checklist (metadata, assets, technical, legal) +- Add universal checklist (marketing, QA, support) +- Generate timeline with milestones +- Calculate completion percentage + +## Best Practices + +### Keyword Research +1. Start with 20-30 seed keywords +2. Analyze top 5 competitors in your category +3. Balance high-volume and long-tail keywords +4. Prioritize relevance over search volume +5. Update keyword research quarterly + +### Metadata Optimization +1. Front-load keywords in title (first 15 characters most important) +2. Use every available character (don't waste space) +3. Write for humans first, search engines second +4. A/B test major changes before committing +5. Update descriptions with each major release + +### A/B Testing +1. Test one element at a time (icon vs. screenshots vs. title) +2. Run tests to statistical significance (90%+ confidence) +3. Test high-impact elements first (icon has biggest impact) +4. Allow sufficient duration (at least 1 week, preferably 2-3) +5. Document learnings for future tests + +### Localization +1. Start with top 5 revenue markets (US, China, Japan, Germany, UK) +2. Use professional translators, not machine translation +3. Test translations with native speakers +4. Adapt keywords for cultural context +5. Monitor ROI by market + +### Review Management +1. Respond to reviews within 24-48 hours +2. Always be professional, even with negative reviews +3. Address specific issues raised +4. Thank users for positive feedback +5. Use insights to prioritize product improvements + +## Technical Requirements + +- **Python**: 3.7+ (for Python modules) +- **Platform Support**: Apple App Store, Google Play Store +- **Data Formats**: JSON input/output +- **Dependencies**: Standard library only (no external packages required) + +## Limitations + +### Data Dependencies +- Keyword search volumes are estimates (no official Apple/Google data) +- Competitor data limited to publicly available information +- Review analysis requires access to public reviews +- Historical data may not be available for new apps + +### Platform Constraints +- Apple: Metadata changes require app submission (except Promotional Text) +- Google: Metadata changes take 1-2 hours to index +- A/B testing requires significant traffic for statistical significance +- Store algorithms are proprietary and change without notice + +### Scope +- Does not include paid user acquisition (Apple Search Ads, Google Ads) +- Does not cover in-app analytics implementation +- Does not handle technical app development +- Focuses on organic discovery and conversion optimization + +## Troubleshooting + +### Issue: Python modules not found +**Solution**: Ensure all .py files are in the same directory as SKILL.md + +### Issue: Character limit validation failing +**Solution**: Check that you're using the correct platform ('apple' or 'google') + +### Issue: Keyword research returning limited results +**Solution**: Provide more context about your app, features, and target audience + +### Issue: ASO score seems inaccurate +**Solution**: Ensure you're providing accurate metrics (ratings, keyword rankings, conversion rate) + +## Version History + +### Version 1.0.0 (November 7, 2025) +- Initial release +- 8 Python modules with comprehensive ASO capabilities +- Support for both Apple App Store and Google Play Store +- Keyword research, metadata optimization, competitor analysis +- ASO scoring, A/B testing, localization, review analysis +- Launch planning and seasonal campaign tools + +## Support & Feedback + +This skill is designed to help app developers and marketers succeed in competitive app marketplaces. For the best results: + +1. Provide detailed context about your app +2. Include specific metrics when available +3. Ask follow-up questions for clarification +4. Iterate based on results + +## Credits + +Developed by Claude Skills Factory +Based on industry-standard ASO best practices +Platform requirements current as of November 2025 + +## License + +This skill is provided as-is for use with Claude Code and Claude Apps. Customize and extend as needed for your specific use cases. + +--- + +**Ready to optimize your app?** Start with keyword research, then move to metadata optimization, and finally implement A/B testing for continuous improvement. The skill handles everything from pre-launch planning to ongoing optimization. + +For detailed usage examples, see [HOW_TO_USE.md](HOW_TO_USE.md). diff --git a/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/SKILL.md b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/SKILL.md new file mode 100644 index 00000000..f41f3214 --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/SKILL.md @@ -0,0 +1,409 @@ +--- +name: app-store-optimization +description: "Complete App Store Optimization (ASO) toolkit for researching, optimizing, and tracking mobile app performance on Apple App Store and Google Play Store" +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# App Store Optimization (ASO) Skill + +This comprehensive skill provides complete ASO capabilities for successfully launching and optimizing mobile applications on the Apple App Store and Google Play Store. + +## Capabilities + +### Research & Analysis +- **Keyword Research**: Analyze keyword volume, competition, and relevance for app discovery +- **Competitor Analysis**: Deep-dive into top-performing apps in your category +- **Market Trend Analysis**: Identify emerging trends and opportunities in your app category +- **Review Sentiment Analysis**: Extract insights from user reviews to identify strengths and issues +- **Category Analysis**: Evaluate optimal category and subcategory placement strategies + +### Metadata Optimization +- **Title Optimization**: Create compelling titles with optimal keyword placement (platform-specific character limits) +- **Description Optimization**: Craft both short and full descriptions that convert and rank +- **Subtitle/Promotional Text**: Optimize Apple-specific subtitle (30 chars) and promotional text (170 chars) +- **Keyword Field**: Maximize Apple's 100-character keyword field with strategic selection +- **Category Selection**: Data-driven recommendations for primary and secondary categories +- **Icon Best Practices**: Guidelines for designing high-converting app icons +- **Screenshot Optimization**: Strategies for creating screenshots that drive installs +- **Preview Video**: Best practices for app preview videos +- **Localization**: Multi-language optimization strategies for global reach + +### Conversion Optimization +- **A/B Testing Framework**: Plan and track metadata experiments for continuous improvement +- **Visual Asset Testing**: Test icons, screenshots, and videos for maximum conversion +- **Store Listing Optimization**: Comprehensive page optimization for impression-to-install conversion +- **Call-to-Action**: Optimize CTAs in descriptions and promotional materials + +### Rating & Review Management +- **Review Monitoring**: Track and analyze user reviews for actionable insights +- **Response Strategies**: Templates and best practices for responding to reviews +- **Rating Improvement**: Tactical approaches to improve app ratings organically +- **Issue Identification**: Surface common problems and feature requests from reviews + +### Launch & Update Strategies +- **Pre-Launch Checklist**: Complete validation before submitting to stores +- **Launch Timing**: Optimize release timing for maximum visibility and downloads +- **Update Cadence**: Plan optimal update frequency and feature rollouts +- **Feature Announcements**: Craft "What's New" sections that re-engage users +- **Seasonal Optimization**: Leverage seasonal trends and events + +### Analytics & Tracking +- **ASO Score**: Calculate overall ASO health score across multiple factors +- **Keyword Rankings**: Track keyword position changes over time +- **Conversion Metrics**: Monitor impression-to-install conversion rates +- **Download Velocity**: Track download trends and momentum +- **Performance Benchmarking**: Compare against category averages and competitors + +### Platform-Specific Requirements +- **Apple App Store**: + - Title: 30 characters + - Subtitle: 30 characters + - Promotional Text: 170 characters (editable without app update) + - Description: 4,000 characters + - Keywords: 100 characters (comma-separated, no spaces) + - What's New: 4,000 characters +- **Google Play Store**: + - Title: 50 characters (formerly 30, increased in 2021) + - Short Description: 80 characters + - Full Description: 4,000 characters + - No separate keyword field (keywords extracted from title and description) + +## Input Requirements + +### Keyword Research +```json +{ + "app_name": "MyApp", + "category": "Productivity", + "target_keywords": ["task manager", "productivity", "todo list"], + "competitors": ["Todoist", "Any.do", "Microsoft To Do"], + "language": "en-US" +} +``` + +### Metadata Optimization +```json +{ + "platform": "apple" | "google", + "app_info": { + "name": "MyApp", + "category": "Productivity", + "target_audience": "Professionals aged 25-45", + "key_features": ["Task management", "Team collaboration", "AI assistance"], + "unique_value": "AI-powered task prioritization" + }, + "current_metadata": { + "title": "Current Title", + "subtitle": "Current Subtitle", + "description": "Current description..." + }, + "target_keywords": ["productivity", "task manager", "todo"] +} +``` + +### Review Analysis +```json +{ + "app_id": "com.myapp.app", + "platform": "apple" | "google", + "date_range": "last_30_days" | "last_90_days" | "all_time", + "rating_filter": [1, 2, 3, 4, 5], + "language": "en" +} +``` + +### ASO Score Calculation +```json +{ + "metadata": { + "title_quality": 0.8, + "description_quality": 0.7, + "keyword_density": 0.6 + }, + "ratings": { + "average_rating": 4.5, + "total_ratings": 15000 + }, + "conversion": { + "impression_to_install": 0.05 + }, + "keyword_rankings": { + "top_10": 5, + "top_50": 12, + "top_100": 18 + } +} +``` + +## Output Formats + +### Keyword Research Report +- List of recommended keywords with search volume estimates +- Competition level analysis (low/medium/high) +- Relevance scores for each keyword +- Strategic recommendations for primary vs. secondary keywords +- Long-tail keyword opportunities + +### Optimized Metadata Package +- Platform-specific title (with character count validation) +- Subtitle/promotional text (Apple) +- Short description (Google) +- Full description (both platforms) +- Keyword field (Apple - 100 chars) +- Character count validation for all fields +- Keyword density analysis +- Before/after comparison + +### Competitor Analysis Report +- Top 10 competitors in category +- Their metadata strategies +- Keyword overlap analysis +- Visual asset assessment +- Rating and review volume comparison +- Identified gaps and opportunities + +### ASO Health Score +- Overall score (0-100) +- Category breakdown: + - Metadata Quality (0-25) + - Ratings & Reviews (0-25) + - Keyword Performance (0-25) + - Conversion Metrics (0-25) +- Specific improvement recommendations +- Priority action items + +### A/B Test Plan +- Hypothesis and test variables +- Test duration recommendations +- Success metrics definition +- Sample size calculations +- Statistical significance thresholds + +### Launch Checklist +- Pre-submission validation (all required assets, metadata) +- Store compliance verification +- Testing checklist (devices, OS versions) +- Marketing preparation items +- Post-launch monitoring plan + +## How to Use + +### Keyword Research +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you research the best keywords for a productivity app targeting professionals? Focus on keywords with good search volume but lower competition. +``` + +### Optimize App Store Listing +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you optimize my app's metadata for the Apple App Store? Here's my current listing: [provide current metadata]. I want to rank for "task management" and "productivity tools". +``` + +### Analyze Competitor Strategy +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you analyze the ASO strategies of Todoist, Any.do, and Microsoft To Do? I want to understand what they're doing well and where there are opportunities. +``` + +### Review Sentiment Analysis +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you analyze recent reviews for my app (com.myapp.ios) and identify the most common user complaints and feature requests? +``` + +### Calculate ASO Score +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you calculate my app's overall ASO health score and provide specific recommendations for improvement? +``` + +### Plan A/B Test +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. I want to A/B test my app icon and first screenshot. Can you help me design the test and determine how long to run it? +``` + +### Pre-Launch Checklist +``` +Hey Claudeโ€”I just added the "app-store-optimization" skill. Can you generate a comprehensive pre-launch checklist for submitting my app to both Apple App Store and Google Play Store? +``` + +## Scripts + +### keyword_analyzer.py +Analyzes keywords for search volume, competition, and relevance. Provides strategic recommendations for primary and secondary keywords. + +**Key Functions:** +- `analyze_keyword()`: Analyze single keyword metrics +- `compare_keywords()`: Compare multiple keywords +- `find_long_tail()`: Discover long-tail keyword opportunities +- `calculate_keyword_difficulty()`: Assess competition level + +### metadata_optimizer.py +Optimizes titles, descriptions, and keyword fields with platform-specific character limit validation. + +**Key Functions:** +- `optimize_title()`: Create compelling, keyword-rich titles +- `optimize_description()`: Generate conversion-focused descriptions +- `optimize_keyword_field()`: Maximize Apple's 100-char keyword field +- `validate_character_limits()`: Ensure compliance with platform limits +- `calculate_keyword_density()`: Analyze keyword usage in metadata + +### competitor_analyzer.py +Analyzes top competitors' ASO strategies and identifies opportunities. + +**Key Functions:** +- `get_top_competitors()`: Identify category leaders +- `analyze_competitor_metadata()`: Extract and analyze competitor keywords +- `compare_visual_assets()`: Evaluate icons and screenshots +- `identify_gaps()`: Find competitive opportunities + +### aso_scorer.py +Calculates comprehensive ASO health score across multiple dimensions. + +**Key Functions:** +- `calculate_overall_score()`: Compute 0-100 ASO score +- `score_metadata_quality()`: Evaluate title, description, keywords +- `score_ratings_reviews()`: Assess rating quality and volume +- `score_keyword_performance()`: Analyze ranking positions +- `score_conversion_metrics()`: Evaluate impression-to-install rates +- `generate_recommendations()`: Provide prioritized action items + +### ab_test_planner.py +Plans and tracks A/B tests for metadata and visual assets. + +**Key Functions:** +- `design_test()`: Create test hypothesis and variables +- `calculate_sample_size()`: Determine required test duration +- `calculate_significance()`: Assess statistical significance +- `track_results()`: Monitor test performance +- `generate_report()`: Summarize test outcomes + +### localization_helper.py +Manages multi-language ASO optimization strategies. + +**Key Functions:** +- `identify_target_markets()`: Recommend localization priorities +- `translate_metadata()`: Generate localized metadata +- `adapt_keywords()`: Research locale-specific keywords +- `validate_translations()`: Check character limits per language +- `calculate_localization_roi()`: Estimate impact of localization + +### review_analyzer.py +Analyzes user reviews for sentiment, issues, and feature requests. + +**Key Functions:** +- `analyze_sentiment()`: Calculate positive/negative/neutral ratios +- `extract_common_themes()`: Identify frequently mentioned topics +- `identify_issues()`: Surface bugs and user complaints +- `find_feature_requests()`: Extract desired features +- `track_sentiment_trends()`: Monitor sentiment over time +- `generate_response_templates()`: Create review response drafts + +### launch_checklist.py +Generates comprehensive pre-launch and update checklists. + +**Key Functions:** +- `generate_prelaunch_checklist()`: Complete submission validation +- `validate_app_store_compliance()`: Check Apple guidelines +- `validate_play_store_compliance()`: Check Google policies +- `create_update_plan()`: Plan update cadence and features +- `optimize_launch_timing()`: Recommend release dates +- `plan_seasonal_campaigns()`: Identify seasonal opportunities + +## Best Practices + +### Keyword Research +1. **Volume vs. Competition**: Balance high-volume keywords with achievable rankings +2. **Relevance First**: Only target keywords genuinely relevant to your app +3. **Long-Tail Strategy**: Include 3-4 word phrases with lower competition +4. **Continuous Research**: Keyword trends changeโ€”research quarterly +5. **Competitor Keywords**: Don't copy blindly; ensure relevance to your features + +### Metadata Optimization +1. **Front-Load Keywords**: Place most important keywords early in title/description +2. **Natural Language**: Write for humans first, SEO second +3. **Feature Benefits**: Focus on user benefits, not just features +4. **A/B Test Everything**: Test titles, descriptions, screenshots systematically +5. **Update Regularly**: Refresh metadata every major update +6. **Character Limits**: Use every characterโ€”don't waste valuable space +7. **Apple Keyword Field**: No plurals, duplicates, or spaces between commas + +### Visual Assets +1. **Icon**: Must be recognizable at small sizes (60x60px) +2. **Screenshots**: First 2-3 are criticalโ€”most users don't scroll +3. **Captions**: Use screenshot captions to tell your value story +4. **Consistency**: Match visual style to app design +5. **A/B Test Icons**: Icon is the single most important visual element + +### Reviews & Ratings +1. **Respond Quickly**: Reply to reviews within 24-48 hours +2. **Professional Tone**: Always courteous, even with negative reviews +3. **Address Issues**: Show you're actively fixing reported problems +4. **Thank Supporters**: Acknowledge positive reviews +5. **Prompt Strategically**: Ask for ratings after positive experiences + +### Launch Strategy +1. **Soft Launch**: Consider launching in smaller markets first +2. **PR Timing**: Coordinate press coverage with launch +3. **Update Frequently**: Initial updates signal active development +4. **Monitor Closely**: Track metrics daily for first 2 weeks +5. **Iterate Quickly**: Fix critical issues immediately + +### Localization +1. **Prioritize Markets**: Start with English, Spanish, Chinese, French, German +2. **Native Speakers**: Use professional translators, not machine translation +3. **Cultural Adaptation**: Some features resonate differently by culture +4. **Test Locally**: Have native speakers review before publishing +5. **Measure ROI**: Track downloads by locale to assess impact + +## Limitations + +### Data Dependencies +- Keyword search volume estimates are approximate (no official data from Apple/Google) +- Competitor data may be incomplete for private apps +- Review analysis limited to public reviews (can't access private feedback) +- Historical data may not be available for new apps + +### Platform Constraints +- Apple App Store keyword changes require app submission (except Promotional Text) +- Google Play Store metadata changes take 1-2 hours to index +- A/B testing requires significant traffic for statistical significance +- Store algorithms are proprietary and change without notice + +### Industry Variability +- ASO benchmarks vary significantly by category (games vs. utilities) +- Seasonality affects different categories differently +- Geographic markets have different competitive landscapes +- Cultural preferences impact what works in different countries + +### Scope Boundaries +- Does not include paid user acquisition strategies (Apple Search Ads, Google Ads) +- Does not cover app development or UI/UX optimization +- Does not include app analytics implementation (use Firebase, Mixpanel, etc.) +- Does not handle app submission technical issues (provisioning profiles, certificates) + +### When NOT to Use This Skill +- For web apps (different SEO strategies apply) +- For enterprise apps not in public stores +- For apps in beta/TestFlight only +- If you need paid advertising strategies (use marketing skills instead) + +## Integration with Other Skills + +This skill works well with: +- **Content Strategy Skills**: For creating app descriptions and marketing copy +- **Analytics Skills**: For analyzing download and engagement data +- **Localization Skills**: For managing multi-language content +- **Design Skills**: For creating optimized visual assets +- **Marketing Skills**: For coordinating broader launch campaigns + +## Version & Updates + +This skill is based on current Apple App Store and Google Play Store requirements as of November 2025. Store policies and best practices evolveโ€”verify current requirements before major launches. + +**Key Updates to Monitor:** +- Apple App Store Connect updates (apple.com/app-store/review/guidelines) +- Google Play Console updates (play.google.com/console/about/guides/releasewithconfidence) +- iOS/Android version adoption rates (affects device testing) +- Store algorithm changes (follow ASO blogs and communities) + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/ab_test_planner.py b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/ab_test_planner.py new file mode 100644 index 00000000..06a80161 --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/ab_test_planner.py @@ -0,0 +1,662 @@ +""" +A/B testing module for App Store Optimization. +Plans and tracks A/B tests for metadata and visual assets. +""" + +from typing import Dict, List, Any, Optional +import math + + +class ABTestPlanner: + """Plans and tracks A/B tests for ASO elements.""" + + # Minimum detectable effect sizes (conservative estimates) + MIN_EFFECT_SIZES = { + 'icon': 0.10, # 10% conversion improvement + 'screenshot': 0.08, # 8% conversion improvement + 'title': 0.05, # 5% conversion improvement + 'description': 0.03 # 3% conversion improvement + } + + # Statistical confidence levels + CONFIDENCE_LEVELS = { + 'high': 0.95, # 95% confidence + 'standard': 0.90, # 90% confidence + 'exploratory': 0.80 # 80% confidence + } + + def __init__(self): + """Initialize A/B test planner.""" + self.active_tests = [] + + def design_test( + self, + test_type: str, + variant_a: Dict[str, Any], + variant_b: Dict[str, Any], + hypothesis: str, + success_metric: str = 'conversion_rate' + ) -> Dict[str, Any]: + """ + Design an A/B test with hypothesis and variables. + + Args: + test_type: Type of test ('icon', 'screenshot', 'title', 'description') + variant_a: Control variant details + variant_b: Test variant details + hypothesis: Expected outcome hypothesis + success_metric: Metric to optimize + + Returns: + Test design with configuration + """ + test_design = { + 'test_id': self._generate_test_id(test_type), + 'test_type': test_type, + 'hypothesis': hypothesis, + 'variants': { + 'a': { + 'name': 'Control', + 'details': variant_a, + 'traffic_split': 0.5 + }, + 'b': { + 'name': 'Variation', + 'details': variant_b, + 'traffic_split': 0.5 + } + }, + 'success_metric': success_metric, + 'secondary_metrics': self._get_secondary_metrics(test_type), + 'minimum_effect_size': self.MIN_EFFECT_SIZES.get(test_type, 0.05), + 'recommended_confidence': 'standard', + 'best_practices': self._get_test_best_practices(test_type) + } + + self.active_tests.append(test_design) + return test_design + + def calculate_sample_size( + self, + baseline_conversion: float, + minimum_detectable_effect: float, + confidence_level: str = 'standard', + power: float = 0.80 + ) -> Dict[str, Any]: + """ + Calculate required sample size for statistical significance. + + Args: + baseline_conversion: Current conversion rate (0-1) + minimum_detectable_effect: Minimum effect size to detect (0-1) + confidence_level: 'high', 'standard', or 'exploratory' + power: Statistical power (typically 0.80 or 0.90) + + Returns: + Sample size calculation with duration estimates + """ + alpha = 1 - self.CONFIDENCE_LEVELS[confidence_level] + beta = 1 - power + + # Expected conversion for variant B + expected_conversion_b = baseline_conversion * (1 + minimum_detectable_effect) + + # Z-scores for alpha and beta + z_alpha = self._get_z_score(1 - alpha / 2) # Two-tailed test + z_beta = self._get_z_score(power) + + # Pooled standard deviation + p_pooled = (baseline_conversion + expected_conversion_b) / 2 + sd_pooled = math.sqrt(2 * p_pooled * (1 - p_pooled)) + + # Sample size per variant + n_per_variant = math.ceil( + ((z_alpha + z_beta) ** 2 * sd_pooled ** 2) / + ((expected_conversion_b - baseline_conversion) ** 2) + ) + + total_sample_size = n_per_variant * 2 + + # Estimate duration based on typical traffic + duration_estimates = self._estimate_test_duration( + total_sample_size, + baseline_conversion + ) + + return { + 'sample_size_per_variant': n_per_variant, + 'total_sample_size': total_sample_size, + 'baseline_conversion': baseline_conversion, + 'expected_conversion_improvement': minimum_detectable_effect, + 'expected_conversion_b': expected_conversion_b, + 'confidence_level': confidence_level, + 'statistical_power': power, + 'duration_estimates': duration_estimates, + 'recommendations': self._generate_sample_size_recommendations( + n_per_variant, + duration_estimates + ) + } + + def calculate_significance( + self, + variant_a_conversions: int, + variant_a_visitors: int, + variant_b_conversions: int, + variant_b_visitors: int + ) -> Dict[str, Any]: + """ + Calculate statistical significance of test results. + + Args: + variant_a_conversions: Conversions for control + variant_a_visitors: Visitors for control + variant_b_conversions: Conversions for variation + variant_b_visitors: Visitors for variation + + Returns: + Significance analysis with decision recommendation + """ + # Calculate conversion rates + rate_a = variant_a_conversions / variant_a_visitors if variant_a_visitors > 0 else 0 + rate_b = variant_b_conversions / variant_b_visitors if variant_b_visitors > 0 else 0 + + # Calculate improvement + if rate_a > 0: + relative_improvement = (rate_b - rate_a) / rate_a + else: + relative_improvement = 0 + + absolute_improvement = rate_b - rate_a + + # Calculate standard error + se_a = math.sqrt(rate_a * (1 - rate_a) / variant_a_visitors) if variant_a_visitors > 0 else 0 + se_b = math.sqrt(rate_b * (1 - rate_b) / variant_b_visitors) if variant_b_visitors > 0 else 0 + se_diff = math.sqrt(se_a**2 + se_b**2) + + # Calculate z-score + z_score = absolute_improvement / se_diff if se_diff > 0 else 0 + + # Calculate p-value (two-tailed) + p_value = 2 * (1 - self._standard_normal_cdf(abs(z_score))) + + # Determine significance + is_significant_95 = p_value < 0.05 + is_significant_90 = p_value < 0.10 + + # Generate decision + decision = self._generate_test_decision( + relative_improvement, + is_significant_95, + is_significant_90, + variant_a_visitors + variant_b_visitors + ) + + return { + 'variant_a': { + 'conversions': variant_a_conversions, + 'visitors': variant_a_visitors, + 'conversion_rate': round(rate_a, 4) + }, + 'variant_b': { + 'conversions': variant_b_conversions, + 'visitors': variant_b_visitors, + 'conversion_rate': round(rate_b, 4) + }, + 'improvement': { + 'absolute': round(absolute_improvement, 4), + 'relative_percentage': round(relative_improvement * 100, 2) + }, + 'statistical_analysis': { + 'z_score': round(z_score, 3), + 'p_value': round(p_value, 4), + 'is_significant_95': is_significant_95, + 'is_significant_90': is_significant_90, + 'confidence_level': '95%' if is_significant_95 else ('90%' if is_significant_90 else 'Not significant') + }, + 'decision': decision + } + + def track_test_results( + self, + test_id: str, + results_data: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Track ongoing test results and provide recommendations. + + Args: + test_id: Test identifier + results_data: Current test results + + Returns: + Test tracking report with next steps + """ + # Find test + test = next((t for t in self.active_tests if t['test_id'] == test_id), None) + if not test: + return {'error': f'Test {test_id} not found'} + + # Calculate significance + significance = self.calculate_significance( + results_data['variant_a_conversions'], + results_data['variant_a_visitors'], + results_data['variant_b_conversions'], + results_data['variant_b_visitors'] + ) + + # Calculate test progress + total_visitors = results_data['variant_a_visitors'] + results_data['variant_b_visitors'] + required_sample = results_data.get('required_sample_size', 10000) + progress_percentage = min((total_visitors / required_sample) * 100, 100) + + # Generate recommendations + recommendations = self._generate_tracking_recommendations( + significance, + progress_percentage, + test['test_type'] + ) + + return { + 'test_id': test_id, + 'test_type': test['test_type'], + 'progress': { + 'total_visitors': total_visitors, + 'required_sample_size': required_sample, + 'progress_percentage': round(progress_percentage, 1), + 'is_complete': progress_percentage >= 100 + }, + 'current_results': significance, + 'recommendations': recommendations, + 'next_steps': self._determine_next_steps( + significance, + progress_percentage + ) + } + + def generate_test_report( + self, + test_id: str, + final_results: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Generate final test report with insights and recommendations. + + Args: + test_id: Test identifier + final_results: Final test results + + Returns: + Comprehensive test report + """ + test = next((t for t in self.active_tests if t['test_id'] == test_id), None) + if not test: + return {'error': f'Test {test_id} not found'} + + significance = self.calculate_significance( + final_results['variant_a_conversions'], + final_results['variant_a_visitors'], + final_results['variant_b_conversions'], + final_results['variant_b_visitors'] + ) + + # Generate insights + insights = self._generate_test_insights( + test, + significance, + final_results + ) + + # Implementation plan + implementation_plan = self._create_implementation_plan( + test, + significance + ) + + return { + 'test_summary': { + 'test_id': test_id, + 'test_type': test['test_type'], + 'hypothesis': test['hypothesis'], + 'duration_days': final_results.get('duration_days', 'N/A') + }, + 'results': significance, + 'insights': insights, + 'implementation_plan': implementation_plan, + 'learnings': self._extract_learnings(test, significance) + } + + def _generate_test_id(self, test_type: str) -> str: + """Generate unique test ID.""" + import time + timestamp = int(time.time()) + return f"{test_type}_{timestamp}" + + def _get_secondary_metrics(self, test_type: str) -> List[str]: + """Get secondary metrics to track for test type.""" + metrics_map = { + 'icon': ['tap_through_rate', 'impression_count', 'brand_recall'], + 'screenshot': ['tap_through_rate', 'time_on_page', 'scroll_depth'], + 'title': ['impression_count', 'tap_through_rate', 'search_visibility'], + 'description': ['time_on_page', 'scroll_depth', 'tap_through_rate'] + } + return metrics_map.get(test_type, ['tap_through_rate']) + + def _get_test_best_practices(self, test_type: str) -> List[str]: + """Get best practices for specific test type.""" + practices_map = { + 'icon': [ + 'Test only one element at a time (color vs. style vs. symbolism)', + 'Ensure icon is recognizable at small sizes (60x60px)', + 'Consider cultural context for global audience', + 'Test against top competitor icons' + ], + 'screenshot': [ + 'Test order of screenshots (users see first 2-3)', + 'Use captions to tell story', + 'Show key features and benefits', + 'Test with and without device frames' + ], + 'title': [ + 'Test keyword variations, not major rebrand', + 'Keep brand name consistent', + 'Ensure title fits within character limits', + 'Test on both search and browse contexts' + ], + 'description': [ + 'Test structure (bullet points vs. paragraphs)', + 'Test call-to-action placement', + 'Test feature vs. benefit focus', + 'Maintain keyword density' + ] + } + return practices_map.get(test_type, ['Test one variable at a time']) + + def _estimate_test_duration( + self, + required_sample_size: int, + baseline_conversion: float + ) -> Dict[str, Any]: + """Estimate test duration based on typical traffic levels.""" + # Assume different daily traffic scenarios + traffic_scenarios = { + 'low': 100, # 100 page views/day + 'medium': 1000, # 1000 page views/day + 'high': 10000 # 10000 page views/day + } + + estimates = {} + for scenario, daily_views in traffic_scenarios.items(): + days = math.ceil(required_sample_size / daily_views) + estimates[scenario] = { + 'daily_page_views': daily_views, + 'estimated_days': days, + 'estimated_weeks': round(days / 7, 1) + } + + return estimates + + def _generate_sample_size_recommendations( + self, + sample_size: int, + duration_estimates: Dict[str, Any] + ) -> List[str]: + """Generate recommendations based on sample size.""" + recommendations = [] + + if sample_size > 50000: + recommendations.append( + "Large sample size required - consider testing smaller effect size or increasing traffic" + ) + + if duration_estimates['medium']['estimated_days'] > 30: + recommendations.append( + "Long test duration - consider higher minimum detectable effect or focus on high-impact changes" + ) + + if duration_estimates['low']['estimated_days'] > 60: + recommendations.append( + "Insufficient traffic for reliable testing - consider user acquisition or broader targeting" + ) + + if not recommendations: + recommendations.append("Sample size and duration are reasonable for this test") + + return recommendations + + def _get_z_score(self, percentile: float) -> float: + """Get z-score for given percentile (approximation).""" + # Common z-scores + z_scores = { + 0.80: 0.84, + 0.85: 1.04, + 0.90: 1.28, + 0.95: 1.645, + 0.975: 1.96, + 0.99: 2.33 + } + return z_scores.get(percentile, 1.96) + + def _standard_normal_cdf(self, z: float) -> float: + """Approximate standard normal cumulative distribution function.""" + # Using error function approximation + t = 1.0 / (1.0 + 0.2316419 * abs(z)) + d = 0.3989423 * math.exp(-z * z / 2.0) + p = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274)))) + + if z > 0: + return 1.0 - p + else: + return p + + def _generate_test_decision( + self, + improvement: float, + is_significant_95: bool, + is_significant_90: bool, + total_visitors: int + ) -> Dict[str, Any]: + """Generate test decision and recommendation.""" + if total_visitors < 1000: + return { + 'decision': 'continue', + 'rationale': 'Insufficient data - continue test to reach minimum sample size', + 'action': 'Keep test running' + } + + if is_significant_95: + if improvement > 0: + return { + 'decision': 'implement_b', + 'rationale': f'Variant B shows {improvement*100:.1f}% improvement with 95% confidence', + 'action': 'Implement Variant B' + } + else: + return { + 'decision': 'keep_a', + 'rationale': 'Variant A performs better with 95% confidence', + 'action': 'Keep current version (A)' + } + + elif is_significant_90: + if improvement > 0: + return { + 'decision': 'implement_b_cautiously', + 'rationale': f'Variant B shows {improvement*100:.1f}% improvement with 90% confidence', + 'action': 'Consider implementing B, monitor closely' + } + else: + return { + 'decision': 'keep_a', + 'rationale': 'Variant A performs better with 90% confidence', + 'action': 'Keep current version (A)' + } + + else: + return { + 'decision': 'inconclusive', + 'rationale': 'No statistically significant difference detected', + 'action': 'Either keep A or test different hypothesis' + } + + def _generate_tracking_recommendations( + self, + significance: Dict[str, Any], + progress: float, + test_type: str + ) -> List[str]: + """Generate recommendations for ongoing test.""" + recommendations = [] + + if progress < 50: + recommendations.append( + f"Test is {progress:.0f}% complete - continue collecting data" + ) + + if progress >= 100: + if significance['statistical_analysis']['is_significant_95']: + recommendations.append( + "Sufficient data collected with significant results - ready to conclude test" + ) + else: + recommendations.append( + "Sample size reached but no significant difference - consider extending test or concluding" + ) + + return recommendations + + def _determine_next_steps( + self, + significance: Dict[str, Any], + progress: float + ) -> str: + """Determine next steps for test.""" + if progress < 100: + return f"Continue test until reaching 100% sample size (currently {progress:.0f}%)" + + decision = significance.get('decision', {}).get('decision', 'inconclusive') + + if decision == 'implement_b': + return "Implement Variant B and monitor metrics for 2 weeks" + elif decision == 'keep_a': + return "Keep Variant A and design new test with different hypothesis" + else: + return "Test inconclusive - either keep A or design new test" + + def _generate_test_insights( + self, + test: Dict[str, Any], + significance: Dict[str, Any], + results: Dict[str, Any] + ) -> List[str]: + """Generate insights from test results.""" + insights = [] + + improvement = significance['improvement']['relative_percentage'] + + if significance['statistical_analysis']['is_significant_95']: + insights.append( + f"Strong evidence: Variant B {'improved' if improvement > 0 else 'decreased'} " + f"conversion by {abs(improvement):.1f}% with 95% confidence" + ) + + insights.append( + f"Tested {test['test_type']} changes: {test['hypothesis']}" + ) + + # Add context-specific insights + if test['test_type'] == 'icon' and improvement > 5: + insights.append( + "Icon change had substantial impact - visual first impression is critical" + ) + + return insights + + def _create_implementation_plan( + self, + test: Dict[str, Any], + significance: Dict[str, Any] + ) -> List[Dict[str, str]]: + """Create implementation plan for winning variant.""" + plan = [] + + if significance.get('decision', {}).get('decision') == 'implement_b': + plan.append({ + 'step': '1. Update store listing', + 'details': f"Replace {test['test_type']} with Variant B across all platforms" + }) + plan.append({ + 'step': '2. Monitor metrics', + 'details': 'Track conversion rate for 2 weeks to confirm sustained improvement' + }) + plan.append({ + 'step': '3. Document learnings', + 'details': 'Record insights for future optimization' + }) + + return plan + + def _extract_learnings( + self, + test: Dict[str, Any], + significance: Dict[str, Any] + ) -> List[str]: + """Extract key learnings from test.""" + learnings = [] + + improvement = significance['improvement']['relative_percentage'] + + learnings.append( + f"Testing {test['test_type']} can yield {abs(improvement):.1f}% conversion change" + ) + + if test['test_type'] == 'title': + learnings.append( + "Title changes affect search visibility and user perception" + ) + elif test['test_type'] == 'screenshot': + learnings.append( + "First 2-3 screenshots are critical for conversion" + ) + + return learnings + + +def plan_ab_test( + test_type: str, + variant_a: Dict[str, Any], + variant_b: Dict[str, Any], + hypothesis: str, + baseline_conversion: float +) -> Dict[str, Any]: + """ + Convenience function to plan an A/B test. + + Args: + test_type: Type of test + variant_a: Control variant + variant_b: Test variant + hypothesis: Test hypothesis + baseline_conversion: Current conversion rate + + Returns: + Complete test plan + """ + planner = ABTestPlanner() + + test_design = planner.design_test( + test_type, + variant_a, + variant_b, + hypothesis + ) + + sample_size = planner.calculate_sample_size( + baseline_conversion, + planner.MIN_EFFECT_SIZES.get(test_type, 0.05) + ) + + return { + 'test_design': test_design, + 'sample_size_requirements': sample_size + } diff --git a/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/aso_scorer.py b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/aso_scorer.py new file mode 100644 index 00000000..ba4ea6ac --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/aso_scorer.py @@ -0,0 +1,482 @@ +""" +ASO scoring module for App Store Optimization. +Calculates comprehensive ASO health score across multiple dimensions. +""" + +from typing import Dict, List, Any, Optional + + +class ASOScorer: + """Calculates overall ASO health score and provides recommendations.""" + + # Score weights for different components (total = 100) + WEIGHTS = { + 'metadata_quality': 25, + 'ratings_reviews': 25, + 'keyword_performance': 25, + 'conversion_metrics': 25 + } + + # Benchmarks for scoring + BENCHMARKS = { + 'title_keyword_usage': {'min': 1, 'target': 2}, + 'description_length': {'min': 500, 'target': 2000}, + 'keyword_density': {'min': 2, 'optimal': 5, 'max': 8}, + 'average_rating': {'min': 3.5, 'target': 4.5}, + 'ratings_count': {'min': 100, 'target': 5000}, + 'keywords_top_10': {'min': 2, 'target': 10}, + 'keywords_top_50': {'min': 5, 'target': 20}, + 'conversion_rate': {'min': 0.02, 'target': 0.10} + } + + def __init__(self): + """Initialize ASO scorer.""" + self.score_breakdown = {} + + def calculate_overall_score( + self, + metadata: Dict[str, Any], + ratings: Dict[str, Any], + keyword_performance: Dict[str, Any], + conversion: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Calculate comprehensive ASO score (0-100). + + Args: + metadata: Title, description quality metrics + ratings: Rating average and count + keyword_performance: Keyword ranking data + conversion: Impression-to-install metrics + + Returns: + Overall score with detailed breakdown + """ + # Calculate component scores + metadata_score = self.score_metadata_quality(metadata) + ratings_score = self.score_ratings_reviews(ratings) + keyword_score = self.score_keyword_performance(keyword_performance) + conversion_score = self.score_conversion_metrics(conversion) + + # Calculate weighted overall score + overall_score = ( + metadata_score * (self.WEIGHTS['metadata_quality'] / 100) + + ratings_score * (self.WEIGHTS['ratings_reviews'] / 100) + + keyword_score * (self.WEIGHTS['keyword_performance'] / 100) + + conversion_score * (self.WEIGHTS['conversion_metrics'] / 100) + ) + + # Store breakdown + self.score_breakdown = { + 'metadata_quality': { + 'score': metadata_score, + 'weight': self.WEIGHTS['metadata_quality'], + 'weighted_contribution': round(metadata_score * (self.WEIGHTS['metadata_quality'] / 100), 1) + }, + 'ratings_reviews': { + 'score': ratings_score, + 'weight': self.WEIGHTS['ratings_reviews'], + 'weighted_contribution': round(ratings_score * (self.WEIGHTS['ratings_reviews'] / 100), 1) + }, + 'keyword_performance': { + 'score': keyword_score, + 'weight': self.WEIGHTS['keyword_performance'], + 'weighted_contribution': round(keyword_score * (self.WEIGHTS['keyword_performance'] / 100), 1) + }, + 'conversion_metrics': { + 'score': conversion_score, + 'weight': self.WEIGHTS['conversion_metrics'], + 'weighted_contribution': round(conversion_score * (self.WEIGHTS['conversion_metrics'] / 100), 1) + } + } + + # Generate recommendations + recommendations = self.generate_recommendations( + metadata_score, + ratings_score, + keyword_score, + conversion_score + ) + + # Assess overall health + health_status = self._assess_health_status(overall_score) + + return { + 'overall_score': round(overall_score, 1), + 'health_status': health_status, + 'score_breakdown': self.score_breakdown, + 'recommendations': recommendations, + 'priority_actions': self._prioritize_actions(recommendations), + 'strengths': self._identify_strengths(self.score_breakdown), + 'weaknesses': self._identify_weaknesses(self.score_breakdown) + } + + def score_metadata_quality(self, metadata: Dict[str, Any]) -> float: + """ + Score metadata quality (0-100). + + Evaluates: + - Title optimization + - Description quality + - Keyword usage + """ + scores = [] + + # Title score (0-35 points) + title_keywords = metadata.get('title_keyword_count', 0) + title_length = metadata.get('title_length', 0) + + title_score = 0 + if title_keywords >= self.BENCHMARKS['title_keyword_usage']['target']: + title_score = 35 + elif title_keywords >= self.BENCHMARKS['title_keyword_usage']['min']: + title_score = 25 + else: + title_score = 10 + + # Adjust for title length usage + if title_length > 25: # Using most of available space + title_score += 0 + else: + title_score -= 5 + + scores.append(min(title_score, 35)) + + # Description score (0-35 points) + desc_length = metadata.get('description_length', 0) + desc_quality = metadata.get('description_quality', 0.0) # 0-1 scale + + desc_score = 0 + if desc_length >= self.BENCHMARKS['description_length']['target']: + desc_score = 25 + elif desc_length >= self.BENCHMARKS['description_length']['min']: + desc_score = 15 + else: + desc_score = 5 + + # Add quality bonus + desc_score += desc_quality * 10 + scores.append(min(desc_score, 35)) + + # Keyword density score (0-30 points) + keyword_density = metadata.get('keyword_density', 0.0) + + if self.BENCHMARKS['keyword_density']['min'] <= keyword_density <= self.BENCHMARKS['keyword_density']['optimal']: + density_score = 30 + elif keyword_density < self.BENCHMARKS['keyword_density']['min']: + # Too low - proportional scoring + density_score = (keyword_density / self.BENCHMARKS['keyword_density']['min']) * 20 + else: + # Too high (keyword stuffing) - penalty + excess = keyword_density - self.BENCHMARKS['keyword_density']['optimal'] + density_score = max(30 - (excess * 5), 0) + + scores.append(density_score) + + return round(sum(scores), 1) + + def score_ratings_reviews(self, ratings: Dict[str, Any]) -> float: + """ + Score ratings and reviews (0-100). + + Evaluates: + - Average rating + - Total ratings count + - Review velocity + """ + average_rating = ratings.get('average_rating', 0.0) + total_ratings = ratings.get('total_ratings', 0) + recent_ratings = ratings.get('recent_ratings_30d', 0) + + # Rating quality score (0-50 points) + if average_rating >= self.BENCHMARKS['average_rating']['target']: + rating_quality_score = 50 + elif average_rating >= self.BENCHMARKS['average_rating']['min']: + # Proportional scoring between min and target + proportion = (average_rating - self.BENCHMARKS['average_rating']['min']) / \ + (self.BENCHMARKS['average_rating']['target'] - self.BENCHMARKS['average_rating']['min']) + rating_quality_score = 30 + (proportion * 20) + elif average_rating >= 3.0: + rating_quality_score = 20 + else: + rating_quality_score = 10 + + # Rating volume score (0-30 points) + if total_ratings >= self.BENCHMARKS['ratings_count']['target']: + rating_volume_score = 30 + elif total_ratings >= self.BENCHMARKS['ratings_count']['min']: + # Proportional scoring + proportion = (total_ratings - self.BENCHMARKS['ratings_count']['min']) / \ + (self.BENCHMARKS['ratings_count']['target'] - self.BENCHMARKS['ratings_count']['min']) + rating_volume_score = 15 + (proportion * 15) + else: + # Very low volume + rating_volume_score = (total_ratings / self.BENCHMARKS['ratings_count']['min']) * 15 + + # Rating velocity score (0-20 points) + if recent_ratings > 100: + velocity_score = 20 + elif recent_ratings > 50: + velocity_score = 15 + elif recent_ratings > 10: + velocity_score = 10 + else: + velocity_score = 5 + + total_score = rating_quality_score + rating_volume_score + velocity_score + + return round(min(total_score, 100), 1) + + def score_keyword_performance(self, keyword_performance: Dict[str, Any]) -> float: + """ + Score keyword ranking performance (0-100). + + Evaluates: + - Top 10 rankings + - Top 50 rankings + - Ranking trends + """ + top_10_count = keyword_performance.get('top_10', 0) + top_50_count = keyword_performance.get('top_50', 0) + top_100_count = keyword_performance.get('top_100', 0) + improving_keywords = keyword_performance.get('improving_keywords', 0) + + # Top 10 score (0-50 points) - most valuable rankings + if top_10_count >= self.BENCHMARKS['keywords_top_10']['target']: + top_10_score = 50 + elif top_10_count >= self.BENCHMARKS['keywords_top_10']['min']: + proportion = (top_10_count - self.BENCHMARKS['keywords_top_10']['min']) / \ + (self.BENCHMARKS['keywords_top_10']['target'] - self.BENCHMARKS['keywords_top_10']['min']) + top_10_score = 25 + (proportion * 25) + else: + top_10_score = (top_10_count / self.BENCHMARKS['keywords_top_10']['min']) * 25 + + # Top 50 score (0-30 points) + if top_50_count >= self.BENCHMARKS['keywords_top_50']['target']: + top_50_score = 30 + elif top_50_count >= self.BENCHMARKS['keywords_top_50']['min']: + proportion = (top_50_count - self.BENCHMARKS['keywords_top_50']['min']) / \ + (self.BENCHMARKS['keywords_top_50']['target'] - self.BENCHMARKS['keywords_top_50']['min']) + top_50_score = 15 + (proportion * 15) + else: + top_50_score = (top_50_count / self.BENCHMARKS['keywords_top_50']['min']) * 15 + + # Coverage score (0-10 points) - based on top 100 + coverage_score = min((top_100_count / 30) * 10, 10) + + # Trend score (0-10 points) - are rankings improving? + if improving_keywords > 5: + trend_score = 10 + elif improving_keywords > 0: + trend_score = 5 + else: + trend_score = 0 + + total_score = top_10_score + top_50_score + coverage_score + trend_score + + return round(min(total_score, 100), 1) + + def score_conversion_metrics(self, conversion: Dict[str, Any]) -> float: + """ + Score conversion performance (0-100). + + Evaluates: + - Impression-to-install conversion rate + - Download velocity + """ + conversion_rate = conversion.get('impression_to_install', 0.0) + downloads_30d = conversion.get('downloads_last_30_days', 0) + downloads_trend = conversion.get('downloads_trend', 'stable') # 'up', 'stable', 'down' + + # Conversion rate score (0-70 points) + if conversion_rate >= self.BENCHMARKS['conversion_rate']['target']: + conversion_score = 70 + elif conversion_rate >= self.BENCHMARKS['conversion_rate']['min']: + proportion = (conversion_rate - self.BENCHMARKS['conversion_rate']['min']) / \ + (self.BENCHMARKS['conversion_rate']['target'] - self.BENCHMARKS['conversion_rate']['min']) + conversion_score = 35 + (proportion * 35) + else: + conversion_score = (conversion_rate / self.BENCHMARKS['conversion_rate']['min']) * 35 + + # Download velocity score (0-20 points) + if downloads_30d > 10000: + velocity_score = 20 + elif downloads_30d > 1000: + velocity_score = 15 + elif downloads_30d > 100: + velocity_score = 10 + else: + velocity_score = 5 + + # Trend bonus (0-10 points) + if downloads_trend == 'up': + trend_score = 10 + elif downloads_trend == 'stable': + trend_score = 5 + else: + trend_score = 0 + + total_score = conversion_score + velocity_score + trend_score + + return round(min(total_score, 100), 1) + + def generate_recommendations( + self, + metadata_score: float, + ratings_score: float, + keyword_score: float, + conversion_score: float + ) -> List[Dict[str, Any]]: + """Generate prioritized recommendations based on scores.""" + recommendations = [] + + # Metadata recommendations + if metadata_score < 60: + recommendations.append({ + 'category': 'metadata_quality', + 'priority': 'high', + 'action': 'Optimize app title and description', + 'details': 'Add more keywords to title, expand description to 1500-2000 characters, improve keyword density to 3-5%', + 'expected_impact': 'Improve discoverability and ranking potential' + }) + elif metadata_score < 80: + recommendations.append({ + 'category': 'metadata_quality', + 'priority': 'medium', + 'action': 'Refine metadata for better keyword targeting', + 'details': 'Test variations of title/subtitle, optimize keyword field for Apple', + 'expected_impact': 'Incremental ranking improvements' + }) + + # Ratings recommendations + if ratings_score < 60: + recommendations.append({ + 'category': 'ratings_reviews', + 'priority': 'high', + 'action': 'Improve rating quality and volume', + 'details': 'Address top user complaints, implement in-app rating prompts, respond to negative reviews', + 'expected_impact': 'Better conversion rates and trust signals' + }) + elif ratings_score < 80: + recommendations.append({ + 'category': 'ratings_reviews', + 'priority': 'medium', + 'action': 'Increase rating velocity', + 'details': 'Optimize timing of rating requests, encourage satisfied users to rate', + 'expected_impact': 'Sustained rating quality' + }) + + # Keyword performance recommendations + if keyword_score < 60: + recommendations.append({ + 'category': 'keyword_performance', + 'priority': 'high', + 'action': 'Improve keyword rankings', + 'details': 'Target long-tail keywords with lower competition, update metadata with high-potential keywords, build backlinks', + 'expected_impact': 'Significant improvement in organic visibility' + }) + elif keyword_score < 80: + recommendations.append({ + 'category': 'keyword_performance', + 'priority': 'medium', + 'action': 'Expand keyword coverage', + 'details': 'Target additional related keywords, test seasonal keywords, localize for new markets', + 'expected_impact': 'Broader reach and more discovery opportunities' + }) + + # Conversion recommendations + if conversion_score < 60: + recommendations.append({ + 'category': 'conversion_metrics', + 'priority': 'high', + 'action': 'Optimize store listing for conversions', + 'details': 'Improve screenshots and icon, strengthen value proposition in description, add video preview', + 'expected_impact': 'Higher impression-to-install conversion' + }) + elif conversion_score < 80: + recommendations.append({ + 'category': 'conversion_metrics', + 'priority': 'medium', + 'action': 'Test visual asset variations', + 'details': 'A/B test different icon designs and screenshot sequences', + 'expected_impact': 'Incremental conversion improvements' + }) + + return recommendations + + def _assess_health_status(self, overall_score: float) -> str: + """Assess overall ASO health status.""" + if overall_score >= 80: + return "Excellent - Top-tier ASO performance" + elif overall_score >= 65: + return "Good - Competitive ASO with room for improvement" + elif overall_score >= 50: + return "Fair - Needs strategic improvements" + else: + return "Poor - Requires immediate ASO overhaul" + + def _prioritize_actions( + self, + recommendations: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """Prioritize actions by impact and urgency.""" + # Sort by priority (high first) and expected impact + priority_order = {'high': 0, 'medium': 1, 'low': 2} + + sorted_recommendations = sorted( + recommendations, + key=lambda x: priority_order[x['priority']] + ) + + return sorted_recommendations[:3] # Top 3 priority actions + + def _identify_strengths(self, score_breakdown: Dict[str, Any]) -> List[str]: + """Identify areas of strength (scores >= 75).""" + strengths = [] + + for category, data in score_breakdown.items(): + if data['score'] >= 75: + strengths.append( + f"{category.replace('_', ' ').title()}: {data['score']}/100" + ) + + return strengths if strengths else ["Focus on building strengths across all areas"] + + def _identify_weaknesses(self, score_breakdown: Dict[str, Any]) -> List[str]: + """Identify areas needing improvement (scores < 60).""" + weaknesses = [] + + for category, data in score_breakdown.items(): + if data['score'] < 60: + weaknesses.append( + f"{category.replace('_', ' ').title()}: {data['score']}/100 - needs improvement" + ) + + return weaknesses if weaknesses else ["All areas performing adequately"] + + +def calculate_aso_score( + metadata: Dict[str, Any], + ratings: Dict[str, Any], + keyword_performance: Dict[str, Any], + conversion: Dict[str, Any] +) -> Dict[str, Any]: + """ + Convenience function to calculate ASO score. + + Args: + metadata: Metadata quality metrics + ratings: Ratings data + keyword_performance: Keyword ranking data + conversion: Conversion metrics + + Returns: + Complete ASO score report + """ + scorer = ASOScorer() + return scorer.calculate_overall_score( + metadata, + ratings, + keyword_performance, + conversion + ) diff --git a/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/competitor_analyzer.py b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/competitor_analyzer.py new file mode 100644 index 00000000..35414c60 --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/competitor_analyzer.py @@ -0,0 +1,577 @@ +""" +Competitor analysis module for App Store Optimization. +Analyzes top competitors' ASO strategies and identifies opportunities. +""" + +from typing import Dict, List, Any, Optional +from collections import Counter +import re + + +class CompetitorAnalyzer: + """Analyzes competitor apps to identify ASO opportunities.""" + + def __init__(self, category: str, platform: str = 'apple'): + """ + Initialize competitor analyzer. + + Args: + category: App category (e.g., "Productivity", "Games") + platform: 'apple' or 'google' + """ + self.category = category + self.platform = platform + self.competitors = [] + + def analyze_competitor( + self, + app_data: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Analyze a single competitor's ASO strategy. + + Args: + app_data: Dictionary with app_name, title, description, rating, ratings_count, keywords + + Returns: + Comprehensive competitor analysis + """ + app_name = app_data.get('app_name', '') + title = app_data.get('title', '') + description = app_data.get('description', '') + rating = app_data.get('rating', 0.0) + ratings_count = app_data.get('ratings_count', 0) + keywords = app_data.get('keywords', []) + + analysis = { + 'app_name': app_name, + 'title_analysis': self._analyze_title(title), + 'description_analysis': self._analyze_description(description), + 'keyword_strategy': self._extract_keyword_strategy(title, description, keywords), + 'rating_metrics': { + 'rating': rating, + 'ratings_count': ratings_count, + 'rating_quality': self._assess_rating_quality(rating, ratings_count) + }, + 'competitive_strength': self._calculate_competitive_strength( + rating, + ratings_count, + len(description) + ), + 'key_differentiators': self._identify_differentiators(description) + } + + self.competitors.append(analysis) + return analysis + + def compare_competitors( + self, + competitors_data: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Compare multiple competitors and identify patterns. + + Args: + competitors_data: List of competitor data dictionaries + + Returns: + Comparative analysis with insights + """ + # Analyze each competitor + analyses = [] + for comp_data in competitors_data: + analysis = self.analyze_competitor(comp_data) + analyses.append(analysis) + + # Extract common keywords across competitors + all_keywords = [] + for analysis in analyses: + all_keywords.extend(analysis['keyword_strategy']['primary_keywords']) + + common_keywords = self._find_common_keywords(all_keywords) + + # Identify keyword gaps (used by some but not all) + keyword_gaps = self._identify_keyword_gaps(analyses) + + # Rank competitors by strength + ranked_competitors = sorted( + analyses, + key=lambda x: x['competitive_strength'], + reverse=True + ) + + # Analyze rating distribution + rating_analysis = self._analyze_rating_distribution(analyses) + + # Identify best practices + best_practices = self._identify_best_practices(ranked_competitors) + + return { + 'category': self.category, + 'platform': self.platform, + 'competitors_analyzed': len(analyses), + 'ranked_competitors': ranked_competitors, + 'common_keywords': common_keywords, + 'keyword_gaps': keyword_gaps, + 'rating_analysis': rating_analysis, + 'best_practices': best_practices, + 'opportunities': self._identify_opportunities( + analyses, + common_keywords, + keyword_gaps + ) + } + + def identify_gaps( + self, + your_app_data: Dict[str, Any], + competitors_data: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Identify gaps between your app and competitors. + + Args: + your_app_data: Your app's data + competitors_data: List of competitor data + + Returns: + Gap analysis with actionable recommendations + """ + # Analyze your app + your_analysis = self.analyze_competitor(your_app_data) + + # Analyze competitors + competitor_comparison = self.compare_competitors(competitors_data) + + # Identify keyword gaps + your_keywords = set(your_analysis['keyword_strategy']['primary_keywords']) + competitor_keywords = set(competitor_comparison['common_keywords']) + missing_keywords = competitor_keywords - your_keywords + + # Identify rating gap + avg_competitor_rating = competitor_comparison['rating_analysis']['average_rating'] + rating_gap = avg_competitor_rating - your_analysis['rating_metrics']['rating'] + + # Identify description length gap + avg_competitor_desc_length = sum( + len(comp['description_analysis']['text']) + for comp in competitor_comparison['ranked_competitors'] + ) / len(competitor_comparison['ranked_competitors']) + your_desc_length = len(your_analysis['description_analysis']['text']) + desc_length_gap = avg_competitor_desc_length - your_desc_length + + return { + 'your_app': your_analysis, + 'keyword_gaps': { + 'missing_keywords': list(missing_keywords)[:10], + 'recommendations': self._generate_keyword_recommendations(missing_keywords) + }, + 'rating_gap': { + 'your_rating': your_analysis['rating_metrics']['rating'], + 'average_competitor_rating': avg_competitor_rating, + 'gap': round(rating_gap, 2), + 'action_items': self._generate_rating_improvement_actions(rating_gap) + }, + 'content_gap': { + 'your_description_length': your_desc_length, + 'average_competitor_length': int(avg_competitor_desc_length), + 'gap': int(desc_length_gap), + 'recommendations': self._generate_content_recommendations(desc_length_gap) + }, + 'competitive_positioning': self._assess_competitive_position( + your_analysis, + competitor_comparison + ) + } + + def _analyze_title(self, title: str) -> Dict[str, Any]: + """Analyze title structure and keyword usage.""" + parts = re.split(r'[-' + r':|]', title) + + return { + 'title': title, + 'length': len(title), + 'has_brand': len(parts) > 0, + 'has_keywords': len(parts) > 1, + 'components': [part.strip() for part in parts], + 'word_count': len(title.split()), + 'strategy': 'brand_plus_keywords' if len(parts) > 1 else 'brand_only' + } + + def _analyze_description(self, description: str) -> Dict[str, Any]: + """Analyze description structure and content.""" + lines = description.split('\n') + word_count = len(description.split()) + + # Check for structural elements + has_bullet_points = 'โ€ข' in description or '*' in description + has_sections = any(line.isupper() for line in lines if len(line) > 0) + has_call_to_action = any( + cta in description.lower() + for cta in ['download', 'try', 'get', 'start', 'join'] + ) + + # Extract features mentioned + features = self._extract_features(description) + + return { + 'text': description, + 'length': len(description), + 'word_count': word_count, + 'structure': { + 'has_bullet_points': has_bullet_points, + 'has_sections': has_sections, + 'has_call_to_action': has_call_to_action + }, + 'features_mentioned': features, + 'readability': 'good' if 50 <= word_count <= 300 else 'needs_improvement' + } + + def _extract_keyword_strategy( + self, + title: str, + description: str, + explicit_keywords: List[str] + ) -> Dict[str, Any]: + """Extract keyword strategy from metadata.""" + # Extract keywords from title + title_keywords = [word.lower() for word in title.split() if len(word) > 3] + + # Extract frequently used words from description + desc_words = re.findall(r'\b\w{4,}\b', description.lower()) + word_freq = Counter(desc_words) + frequent_words = [word for word, count in word_freq.most_common(15) if count > 2] + + # Combine with explicit keywords + all_keywords = list(set(title_keywords + frequent_words + explicit_keywords)) + + return { + 'primary_keywords': title_keywords, + 'description_keywords': frequent_words[:10], + 'explicit_keywords': explicit_keywords, + 'total_unique_keywords': len(all_keywords), + 'keyword_focus': self._assess_keyword_focus(title_keywords, frequent_words) + } + + def _assess_rating_quality(self, rating: float, ratings_count: int) -> str: + """Assess the quality of ratings.""" + if ratings_count < 100: + return 'insufficient_data' + elif rating >= 4.5 and ratings_count > 1000: + return 'excellent' + elif rating >= 4.0 and ratings_count > 500: + return 'good' + elif rating >= 3.5: + return 'average' + else: + return 'poor' + + def _calculate_competitive_strength( + self, + rating: float, + ratings_count: int, + description_length: int + ) -> float: + """ + Calculate overall competitive strength (0-100). + + Factors: + - Rating quality (40%) + - Rating volume (30%) + - Metadata quality (30%) + """ + # Rating quality score (0-40) + rating_score = (rating / 5.0) * 40 + + # Rating volume score (0-30) + volume_score = min((ratings_count / 10000) * 30, 30) + + # Metadata quality score (0-30) + metadata_score = min((description_length / 2000) * 30, 30) + + total_score = rating_score + volume_score + metadata_score + + return round(total_score, 1) + + def _identify_differentiators(self, description: str) -> List[str]: + """Identify key differentiators from description.""" + differentiator_keywords = [ + 'unique', 'only', 'first', 'best', 'leading', 'exclusive', + 'revolutionary', 'innovative', 'patent', 'award' + ] + + differentiators = [] + sentences = description.split('.') + + for sentence in sentences: + sentence_lower = sentence.lower() + if any(keyword in sentence_lower for keyword in differentiator_keywords): + differentiators.append(sentence.strip()) + + return differentiators[:5] + + def _find_common_keywords(self, all_keywords: List[str]) -> List[str]: + """Find keywords used by multiple competitors.""" + keyword_counts = Counter(all_keywords) + # Return keywords used by at least 2 competitors + common = [kw for kw, count in keyword_counts.items() if count >= 2] + return sorted(common, key=lambda x: keyword_counts[x], reverse=True)[:20] + + def _identify_keyword_gaps(self, analyses: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Identify keywords used by some competitors but not others.""" + all_keywords_by_app = {} + + for analysis in analyses: + app_name = analysis['app_name'] + keywords = analysis['keyword_strategy']['primary_keywords'] + all_keywords_by_app[app_name] = set(keywords) + + # Find keywords used by some but not all + all_keywords_set = set() + for keywords in all_keywords_by_app.values(): + all_keywords_set.update(keywords) + + gaps = [] + for keyword in all_keywords_set: + using_apps = [ + app for app, keywords in all_keywords_by_app.items() + if keyword in keywords + ] + if 1 < len(using_apps) < len(analyses): + gaps.append({ + 'keyword': keyword, + 'used_by': using_apps, + 'usage_percentage': round(len(using_apps) / len(analyses) * 100, 1) + }) + + return sorted(gaps, key=lambda x: x['usage_percentage'], reverse=True)[:15] + + def _analyze_rating_distribution(self, analyses: List[Dict[str, Any]]) -> Dict[str, Any]: + """Analyze rating distribution across competitors.""" + ratings = [a['rating_metrics']['rating'] for a in analyses] + ratings_counts = [a['rating_metrics']['ratings_count'] for a in analyses] + + return { + 'average_rating': round(sum(ratings) / len(ratings), 2), + 'highest_rating': max(ratings), + 'lowest_rating': min(ratings), + 'average_ratings_count': int(sum(ratings_counts) / len(ratings_counts)), + 'total_ratings_in_category': sum(ratings_counts) + } + + def _identify_best_practices(self, ranked_competitors: List[Dict[str, Any]]) -> List[str]: + """Identify best practices from top competitors.""" + if not ranked_competitors: + return [] + + top_competitor = ranked_competitors[0] + practices = [] + + # Title strategy + title_analysis = top_competitor['title_analysis'] + if title_analysis['has_keywords']: + practices.append( + f"Title Strategy: Include primary keyword in title (e.g., '{title_analysis['title']}')" + ) + + # Description structure + desc_analysis = top_competitor['description_analysis'] + if desc_analysis['structure']['has_bullet_points']: + practices.append("Description: Use bullet points to highlight key features") + + if desc_analysis['structure']['has_sections']: + practices.append("Description: Organize content with clear section headers") + + # Rating strategy + rating_quality = top_competitor['rating_metrics']['rating_quality'] + if rating_quality in ['excellent', 'good']: + practices.append( + f"Ratings: Maintain high rating quality ({top_competitor['rating_metrics']['rating']}โ˜…) " + f"with significant volume ({top_competitor['rating_metrics']['ratings_count']} ratings)" + ) + + return practices[:5] + + def _identify_opportunities( + self, + analyses: List[Dict[str, Any]], + common_keywords: List[str], + keyword_gaps: List[Dict[str, Any]] + ) -> List[str]: + """Identify ASO opportunities based on competitive analysis.""" + opportunities = [] + + # Keyword opportunities from gaps + if keyword_gaps: + underutilized_keywords = [ + gap['keyword'] for gap in keyword_gaps + if gap['usage_percentage'] < 50 + ] + if underutilized_keywords: + opportunities.append( + f"Target underutilized keywords: {', '.join(underutilized_keywords[:5])}" + ) + + # Rating opportunity + avg_rating = sum(a['rating_metrics']['rating'] for a in analyses) / len(analyses) + if avg_rating < 4.5: + opportunities.append( + f"Category average rating is {avg_rating:.1f} - opportunity to differentiate with higher ratings" + ) + + # Content depth opportunity + avg_desc_length = sum( + a['description_analysis']['length'] for a in analyses + ) / len(analyses) + if avg_desc_length < 1500: + opportunities.append( + "Competitors have relatively short descriptions - opportunity to provide more comprehensive information" + ) + + return opportunities[:5] + + def _extract_features(self, description: str) -> List[str]: + """Extract feature mentions from description.""" + # Look for bullet points or numbered lists + lines = description.split('\n') + features = [] + + for line in lines: + line = line.strip() + # Check if line starts with bullet or number + if line and (line[0] in ['โ€ข', '*', '-', 'โœ“'] or line[0].isdigit()): + # Clean the line + cleaned = re.sub(r'^[โ€ข*\-โœ“\d.)\s]+', '', line) + if cleaned: + features.append(cleaned) + + return features[:10] + + def _assess_keyword_focus( + self, + title_keywords: List[str], + description_keywords: List[str] + ) -> str: + """Assess keyword focus strategy.""" + overlap = set(title_keywords) & set(description_keywords) + + if len(overlap) >= 3: + return 'consistent_focus' + elif len(overlap) >= 1: + return 'moderate_focus' + else: + return 'broad_focus' + + def _generate_keyword_recommendations(self, missing_keywords: set) -> List[str]: + """Generate recommendations for missing keywords.""" + if not missing_keywords: + return ["Your keyword coverage is comprehensive"] + + recommendations = [] + missing_list = list(missing_keywords)[:5] + + recommendations.append( + f"Consider adding these competitor keywords: {', '.join(missing_list)}" + ) + recommendations.append( + "Test keyword variations in subtitle/promotional text first" + ) + recommendations.append( + "Monitor competitor keyword changes monthly" + ) + + return recommendations + + def _generate_rating_improvement_actions(self, rating_gap: float) -> List[str]: + """Generate actions to improve ratings.""" + actions = [] + + if rating_gap > 0.5: + actions.append("CRITICAL: Significant rating gap - prioritize user satisfaction improvements") + actions.append("Analyze negative reviews to identify top issues") + actions.append("Implement in-app rating prompts after positive experiences") + actions.append("Respond to all negative reviews professionally") + elif rating_gap > 0.2: + actions.append("Focus on incremental improvements to close rating gap") + actions.append("Optimize timing of rating requests") + else: + actions.append("Ratings are competitive - maintain quality and continue improvements") + + return actions + + def _generate_content_recommendations(self, desc_length_gap: int) -> List[str]: + """Generate content recommendations based on length gap.""" + recommendations = [] + + if desc_length_gap > 500: + recommendations.append( + "Expand description to match competitor detail level" + ) + recommendations.append( + "Add use case examples and success stories" + ) + recommendations.append( + "Include more feature explanations and benefits" + ) + elif desc_length_gap < -500: + recommendations.append( + "Consider condensing description for better readability" + ) + recommendations.append( + "Focus on most important features first" + ) + else: + recommendations.append( + "Description length is competitive" + ) + + return recommendations + + def _assess_competitive_position( + self, + your_analysis: Dict[str, Any], + competitor_comparison: Dict[str, Any] + ) -> str: + """Assess your competitive position.""" + your_strength = your_analysis['competitive_strength'] + competitors = competitor_comparison['ranked_competitors'] + + if not competitors: + return "No comparison data available" + + # Find where you'd rank + better_than_count = sum( + 1 for comp in competitors + if your_strength > comp['competitive_strength'] + ) + + position_percentage = (better_than_count / len(competitors)) * 100 + + if position_percentage >= 75: + return "Strong Position: Top quartile in competitive strength" + elif position_percentage >= 50: + return "Competitive Position: Above average, opportunities for improvement" + elif position_percentage >= 25: + return "Challenging Position: Below average, requires strategic improvements" + else: + return "Weak Position: Bottom quartile, major ASO overhaul needed" + + +def analyze_competitor_set( + category: str, + competitors_data: List[Dict[str, Any]], + platform: str = 'apple' +) -> Dict[str, Any]: + """ + Convenience function to analyze a set of competitors. + + Args: + category: App category + competitors_data: List of competitor data + platform: 'apple' or 'google' + + Returns: + Complete competitive analysis + """ + analyzer = CompetitorAnalyzer(category, platform) + return analyzer.compare_competitors(competitors_data) diff --git a/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/expected_output.json b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/expected_output.json new file mode 100644 index 00000000..9832693a --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/expected_output.json @@ -0,0 +1,170 @@ +{ + "request_type": "keyword_research", + "app_name": "TaskFlow Pro", + "keyword_analysis": { + "total_keywords_analyzed": 25, + "primary_keywords": [ + { + "keyword": "task manager", + "search_volume": 45000, + "competition_level": "high", + "relevance_score": 0.95, + "difficulty_score": 72.5, + "potential_score": 78.3, + "recommendation": "High priority - target immediately" + }, + { + "keyword": "productivity app", + "search_volume": 38000, + "competition_level": "high", + "relevance_score": 0.90, + "difficulty_score": 68.2, + "potential_score": 75.1, + "recommendation": "High priority - target immediately" + }, + { + "keyword": "todo list", + "search_volume": 52000, + "competition_level": "very_high", + "relevance_score": 0.85, + "difficulty_score": 78.9, + "potential_score": 71.4, + "recommendation": "High priority - target immediately" + } + ], + "secondary_keywords": [ + { + "keyword": "team task manager", + "search_volume": 8500, + "competition_level": "medium", + "relevance_score": 0.88, + "difficulty_score": 42.3, + "potential_score": 68.7, + "recommendation": "Good opportunity - include in metadata" + }, + { + "keyword": "project planning app", + "search_volume": 12000, + "competition_level": "medium", + "relevance_score": 0.75, + "difficulty_score": 48.1, + "potential_score": 64.2, + "recommendation": "Good opportunity - include in metadata" + } + ], + "long_tail_keywords": [ + { + "keyword": "ai task prioritization", + "search_volume": 2800, + "competition_level": "low", + "relevance_score": 0.95, + "difficulty_score": 25.4, + "potential_score": 82.6, + "recommendation": "Excellent long-tail opportunity" + }, + { + "keyword": "team productivity tool", + "search_volume": 3500, + "competition_level": "low", + "relevance_score": 0.85, + "difficulty_score": 28.7, + "potential_score": 79.3, + "recommendation": "Excellent long-tail opportunity" + } + ] + }, + "competitor_insights": { + "competitors_analyzed": 4, + "common_keywords": [ + "task", + "todo", + "list", + "productivity", + "organize", + "manage" + ], + "keyword_gaps": [ + { + "keyword": "ai prioritization", + "used_by": ["None of the major competitors"], + "opportunity": "Unique positioning opportunity" + }, + { + "keyword": "smart task manager", + "used_by": ["Things 3"], + "opportunity": "Underutilized by most competitors" + } + ] + }, + "metadata_recommendations": { + "apple_app_store": { + "title_options": [ + { + "title": "TaskFlow - AI Task Manager", + "length": 26, + "keywords_included": ["task manager", "ai"], + "strategy": "brand_plus_primary" + }, + { + "title": "TaskFlow: Smart Todo & Tasks", + "length": 29, + "keywords_included": ["todo", "tasks"], + "strategy": "brand_plus_multiple" + } + ], + "subtitle_recommendation": "AI-Powered Team Productivity", + "keyword_field": "productivity,organize,planner,schedule,workflow,reminders,collaboration,calendar,sync,priorities", + "description_focus": "Lead with AI differentiation, emphasize team features" + }, + "google_play_store": { + "title_options": [ + { + "title": "TaskFlow - AI Task Manager & Team Productivity", + "length": 48, + "keywords_included": ["task manager", "ai", "team", "productivity"], + "strategy": "keyword_rich" + } + ], + "short_description_recommendation": "AI task manager - Organize, prioritize, and collaborate with your team", + "description_focus": "Keywords naturally integrated throughout 4000 character description" + } + }, + "strategic_recommendations": [ + "Focus on 'AI prioritization' as unique differentiator - low competition, high relevance", + "Target 'team task manager' and 'team productivity' keywords - good search volume, lower competition than generic terms", + "Include long-tail keywords in description for additional discovery opportunities", + "Test title variations with A/B testing after launch", + "Monitor competitor keyword changes quarterly" + ], + "priority_actions": [ + { + "action": "Optimize app title with primary keyword", + "priority": "high", + "expected_impact": "15-25% improvement in search visibility" + }, + { + "action": "Create description highlighting AI features with natural keyword integration", + "priority": "high", + "expected_impact": "10-15% improvement in conversion rate" + }, + { + "action": "Plan A/B tests for icon and screenshots post-launch", + "priority": "medium", + "expected_impact": "5-10% improvement in conversion rate" + } + ], + "aso_health_estimate": { + "current_score": "N/A (pre-launch)", + "potential_score_with_optimizations": "75-80/100", + "key_strengths": [ + "Unique AI differentiation", + "Clear target audience", + "Strong feature set" + ], + "areas_to_develop": [ + "Build rating volume post-launch", + "Monitor and respond to reviews", + "Continuous keyword optimization" + ] + } +} diff --git a/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/keyword_analyzer.py b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/keyword_analyzer.py new file mode 100644 index 00000000..5c3d80ba --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/keyword_analyzer.py @@ -0,0 +1,406 @@ +""" +Keyword analysis module for App Store Optimization. +Analyzes keyword search volume, competition, and relevance for app discovery. +""" + +from typing import Dict, List, Any, Optional, Tuple +import re +from collections import Counter + + +class KeywordAnalyzer: + """Analyzes keywords for ASO effectiveness.""" + + # Competition level thresholds (based on number of competing apps) + COMPETITION_THRESHOLDS = { + 'low': 1000, + 'medium': 5000, + 'high': 10000 + } + + # Search volume categories (monthly searches estimate) + VOLUME_CATEGORIES = { + 'very_low': 1000, + 'low': 5000, + 'medium': 20000, + 'high': 100000, + 'very_high': 500000 + } + + def __init__(self): + """Initialize keyword analyzer.""" + self.analyzed_keywords = {} + + def analyze_keyword( + self, + keyword: str, + search_volume: int = 0, + competing_apps: int = 0, + relevance_score: float = 0.0 + ) -> Dict[str, Any]: + """ + Analyze a single keyword for ASO potential. + + Args: + keyword: The keyword to analyze + search_volume: Estimated monthly search volume + competing_apps: Number of apps competing for this keyword + relevance_score: Relevance to your app (0.0-1.0) + + Returns: + Dictionary with keyword analysis + """ + competition_level = self._calculate_competition_level(competing_apps) + volume_category = self._categorize_search_volume(search_volume) + difficulty_score = self._calculate_keyword_difficulty( + search_volume, + competing_apps + ) + + # Calculate potential score (0-100) + potential_score = self._calculate_potential_score( + search_volume, + competing_apps, + relevance_score + ) + + analysis = { + 'keyword': keyword, + 'search_volume': search_volume, + 'volume_category': volume_category, + 'competing_apps': competing_apps, + 'competition_level': competition_level, + 'relevance_score': relevance_score, + 'difficulty_score': difficulty_score, + 'potential_score': potential_score, + 'recommendation': self._generate_recommendation( + potential_score, + difficulty_score, + relevance_score + ), + 'keyword_length': len(keyword.split()), + 'is_long_tail': len(keyword.split()) >= 3 + } + + self.analyzed_keywords[keyword] = analysis + return analysis + + def compare_keywords(self, keywords_data: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + Compare multiple keywords and rank by potential. + + Args: + keywords_data: List of dicts with keyword, search_volume, competing_apps, relevance_score + + Returns: + Comparison report with ranked keywords + """ + analyses = [] + for kw_data in keywords_data: + analysis = self.analyze_keyword( + keyword=kw_data['keyword'], + search_volume=kw_data.get('search_volume', 0), + competing_apps=kw_data.get('competing_apps', 0), + relevance_score=kw_data.get('relevance_score', 0.0) + ) + analyses.append(analysis) + + # Sort by potential score (descending) + ranked_keywords = sorted( + analyses, + key=lambda x: x['potential_score'], + reverse=True + ) + + # Categorize keywords + primary_keywords = [ + kw for kw in ranked_keywords + if kw['potential_score'] >= 70 and kw['relevance_score'] >= 0.8 + ] + + secondary_keywords = [ + kw for kw in ranked_keywords + if 50 <= kw['potential_score'] < 70 and kw['relevance_score'] >= 0.6 + ] + + long_tail_keywords = [ + kw for kw in ranked_keywords + if kw['is_long_tail'] and kw['relevance_score'] >= 0.7 + ] + + return { + 'total_keywords_analyzed': len(analyses), + 'ranked_keywords': ranked_keywords, + 'primary_keywords': primary_keywords[:5], # Top 5 + 'secondary_keywords': secondary_keywords[:10], # Top 10 + 'long_tail_keywords': long_tail_keywords[:10], # Top 10 + 'summary': self._generate_comparison_summary( + primary_keywords, + secondary_keywords, + long_tail_keywords + ) + } + + def find_long_tail_opportunities( + self, + base_keyword: str, + modifiers: List[str] + ) -> List[Dict[str, Any]]: + """ + Generate long-tail keyword variations. + + Args: + base_keyword: Core keyword (e.g., "task manager") + modifiers: List of modifiers (e.g., ["free", "simple", "team"]) + + Returns: + List of long-tail keyword suggestions + """ + long_tail_keywords = [] + + # Generate combinations + for modifier in modifiers: + # Modifier + base + variation1 = f"{modifier} {base_keyword}" + long_tail_keywords.append({ + 'keyword': variation1, + 'pattern': 'modifier_base', + 'estimated_competition': 'low', + 'rationale': f"Less competitive variation of '{base_keyword}'" + }) + + # Base + modifier + variation2 = f"{base_keyword} {modifier}" + long_tail_keywords.append({ + 'keyword': variation2, + 'pattern': 'base_modifier', + 'estimated_competition': 'low', + 'rationale': f"Specific use-case variation of '{base_keyword}'" + }) + + # Add question-based long-tail + question_words = ['how', 'what', 'best', 'top'] + for q_word in question_words: + question_keyword = f"{q_word} {base_keyword}" + long_tail_keywords.append({ + 'keyword': question_keyword, + 'pattern': 'question_based', + 'estimated_competition': 'very_low', + 'rationale': f"Informational search query" + }) + + return long_tail_keywords + + def extract_keywords_from_text( + self, + text: str, + min_word_length: int = 3 + ) -> List[Tuple[str, int]]: + """ + Extract potential keywords from text (descriptions, reviews). + + Args: + text: Text to analyze + min_word_length: Minimum word length to consider + + Returns: + List of (keyword, frequency) tuples + """ + # Clean and normalize text + text = text.lower() + text = re.sub(r'[^\w\s]', ' ', text) + + # Extract words + words = text.split() + + # Filter by length + words = [w for w in words if len(w) >= min_word_length] + + # Remove common stop words + stop_words = { + 'the', 'and', 'for', 'with', 'this', 'that', 'from', 'have', + 'but', 'not', 'you', 'all', 'can', 'are', 'was', 'were', 'been' + } + words = [w for w in words if w not in stop_words] + + # Count frequency + word_counts = Counter(words) + + # Extract 2-word phrases + phrases = [] + for i in range(len(words) - 1): + phrase = f"{words[i]} {words[i+1]}" + phrases.append(phrase) + + phrase_counts = Counter(phrases) + + # Combine and sort + all_keywords = list(word_counts.items()) + list(phrase_counts.items()) + all_keywords.sort(key=lambda x: x[1], reverse=True) + + return all_keywords[:50] # Top 50 + + def calculate_keyword_density( + self, + text: str, + target_keywords: List[str] + ) -> Dict[str, float]: + """ + Calculate keyword density in text. + + Args: + text: Text to analyze (title, description) + target_keywords: Keywords to check density for + + Returns: + Dictionary of keyword: density (percentage) + """ + text_lower = text.lower() + total_words = len(text_lower.split()) + + densities = {} + for keyword in target_keywords: + keyword_lower = keyword.lower() + occurrences = text_lower.count(keyword_lower) + density = (occurrences / total_words) * 100 if total_words > 0 else 0 + densities[keyword] = round(density, 2) + + return densities + + def _calculate_competition_level(self, competing_apps: int) -> str: + """Determine competition level based on number of competing apps.""" + if competing_apps < self.COMPETITION_THRESHOLDS['low']: + return 'low' + elif competing_apps < self.COMPETITION_THRESHOLDS['medium']: + return 'medium' + elif competing_apps < self.COMPETITION_THRESHOLDS['high']: + return 'high' + else: + return 'very_high' + + def _categorize_search_volume(self, search_volume: int) -> str: + """Categorize search volume.""" + if search_volume < self.VOLUME_CATEGORIES['very_low']: + return 'very_low' + elif search_volume < self.VOLUME_CATEGORIES['low']: + return 'low' + elif search_volume < self.VOLUME_CATEGORIES['medium']: + return 'medium' + elif search_volume < self.VOLUME_CATEGORIES['high']: + return 'high' + else: + return 'very_high' + + def _calculate_keyword_difficulty( + self, + search_volume: int, + competing_apps: int + ) -> float: + """ + Calculate keyword difficulty score (0-100). + Higher score = harder to rank. + """ + if competing_apps == 0: + return 0.0 + + # Competition factor (0-1) + competition_factor = min(competing_apps / 50000, 1.0) + + # Volume factor (0-1) - higher volume = more difficulty + volume_factor = min(search_volume / 1000000, 1.0) + + # Difficulty score (weighted average) + difficulty = (competition_factor * 0.7 + volume_factor * 0.3) * 100 + + return round(difficulty, 1) + + def _calculate_potential_score( + self, + search_volume: int, + competing_apps: int, + relevance_score: float + ) -> float: + """ + Calculate overall keyword potential (0-100). + Higher score = better opportunity. + """ + # Volume score (0-40 points) + volume_score = min((search_volume / 100000) * 40, 40) + + # Competition score (0-30 points) - inverse relationship + if competing_apps > 0: + competition_score = max(30 - (competing_apps / 500), 0) + else: + competition_score = 30 + + # Relevance score (0-30 points) + relevance_points = relevance_score * 30 + + total_score = volume_score + competition_score + relevance_points + + return round(min(total_score, 100), 1) + + def _generate_recommendation( + self, + potential_score: float, + difficulty_score: float, + relevance_score: float + ) -> str: + """Generate actionable recommendation for keyword.""" + if relevance_score < 0.5: + return "Low relevance - avoid targeting" + + if potential_score >= 70: + return "High priority - target immediately" + elif potential_score >= 50: + if difficulty_score < 50: + return "Good opportunity - include in metadata" + else: + return "Competitive - use in description, not title" + elif potential_score >= 30: + return "Secondary keyword - use for long-tail variations" + else: + return "Low potential - deprioritize" + + def _generate_comparison_summary( + self, + primary_keywords: List[Dict[str, Any]], + secondary_keywords: List[Dict[str, Any]], + long_tail_keywords: List[Dict[str, Any]] + ) -> str: + """Generate summary of keyword comparison.""" + summary_parts = [] + + summary_parts.append( + f"Identified {len(primary_keywords)} high-priority primary keywords." + ) + + if primary_keywords: + top_keyword = primary_keywords[0]['keyword'] + summary_parts.append( + f"Top recommendation: '{top_keyword}' (potential score: {primary_keywords[0]['potential_score']})." + ) + + summary_parts.append( + f"Found {len(secondary_keywords)} secondary keywords for description and metadata." + ) + + summary_parts.append( + f"Discovered {len(long_tail_keywords)} long-tail opportunities with lower competition." + ) + + return " ".join(summary_parts) + + +def analyze_keyword_set(keywords_data: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + Convenience function to analyze a set of keywords. + + Args: + keywords_data: List of keyword data dictionaries + + Returns: + Complete analysis report + """ + analyzer = KeywordAnalyzer() + return analyzer.compare_keywords(keywords_data) diff --git a/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/launch_checklist.py b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/launch_checklist.py new file mode 100644 index 00000000..38eea18b --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/launch_checklist.py @@ -0,0 +1,739 @@ +""" +Launch checklist module for App Store Optimization. +Generates comprehensive pre-launch and update checklists. +""" + +from typing import Dict, List, Any, Optional +from datetime import datetime, timedelta + + +class LaunchChecklistGenerator: + """Generates comprehensive checklists for app launches and updates.""" + + def __init__(self, platform: str = 'both'): + """ + Initialize checklist generator. + + Args: + platform: 'apple', 'google', or 'both' + """ + if platform not in ['apple', 'google', 'both']: + raise ValueError("Platform must be 'apple', 'google', or 'both'") + + self.platform = platform + + def generate_prelaunch_checklist( + self, + app_info: Dict[str, Any], + launch_date: Optional[str] = None + ) -> Dict[str, Any]: + """ + Generate comprehensive pre-launch checklist. + + Args: + app_info: App information (name, category, target_audience) + launch_date: Target launch date (YYYY-MM-DD) + + Returns: + Complete pre-launch checklist + """ + checklist = { + 'app_info': app_info, + 'launch_date': launch_date, + 'checklists': {} + } + + # Generate platform-specific checklists + if self.platform in ['apple', 'both']: + checklist['checklists']['apple'] = self._generate_apple_checklist(app_info) + + if self.platform in ['google', 'both']: + checklist['checklists']['google'] = self._generate_google_checklist(app_info) + + # Add universal checklist items + checklist['checklists']['universal'] = self._generate_universal_checklist(app_info) + + # Generate timeline + if launch_date: + checklist['timeline'] = self._generate_launch_timeline(launch_date) + + # Calculate completion status + checklist['summary'] = self._calculate_checklist_summary(checklist['checklists']) + + return checklist + + def validate_app_store_compliance( + self, + app_data: Dict[str, Any], + platform: str = 'apple' + ) -> Dict[str, Any]: + """ + Validate compliance with app store guidelines. + + Args: + app_data: App data including metadata, privacy policy, etc. + platform: 'apple' or 'google' + + Returns: + Compliance validation report + """ + validation_results = { + 'platform': platform, + 'is_compliant': True, + 'errors': [], + 'warnings': [], + 'recommendations': [] + } + + if platform == 'apple': + self._validate_apple_compliance(app_data, validation_results) + elif platform == 'google': + self._validate_google_compliance(app_data, validation_results) + + # Determine overall compliance + validation_results['is_compliant'] = len(validation_results['errors']) == 0 + + return validation_results + + def create_update_plan( + self, + current_version: str, + planned_features: List[str], + update_frequency: str = 'monthly' + ) -> Dict[str, Any]: + """ + Create update cadence and feature rollout plan. + + Args: + current_version: Current app version + planned_features: List of planned features + update_frequency: 'weekly', 'biweekly', 'monthly', 'quarterly' + + Returns: + Update plan with cadence and feature schedule + """ + # Calculate next versions + next_versions = self._calculate_next_versions( + current_version, + update_frequency, + len(planned_features) + ) + + # Distribute features across versions + feature_schedule = self._distribute_features( + planned_features, + next_versions + ) + + # Generate "What's New" templates + whats_new_templates = [ + self._generate_whats_new_template(version_data) + for version_data in feature_schedule + ] + + return { + 'current_version': current_version, + 'update_frequency': update_frequency, + 'planned_updates': len(feature_schedule), + 'feature_schedule': feature_schedule, + 'whats_new_templates': whats_new_templates, + 'recommendations': self._generate_update_recommendations(update_frequency) + } + + def optimize_launch_timing( + self, + app_category: str, + target_audience: str, + current_date: Optional[str] = None + ) -> Dict[str, Any]: + """ + Recommend optimal launch timing. + + Args: + app_category: App category + target_audience: Target audience description + current_date: Current date (YYYY-MM-DD), defaults to today + + Returns: + Launch timing recommendations + """ + if not current_date: + current_date = datetime.now().strftime('%Y-%m-%d') + + # Analyze launch timing factors + day_of_week_rec = self._recommend_day_of_week(app_category) + seasonal_rec = self._recommend_seasonal_timing(app_category, current_date) + competitive_rec = self._analyze_competitive_timing(app_category) + + # Calculate optimal dates + optimal_dates = self._calculate_optimal_dates( + current_date, + day_of_week_rec, + seasonal_rec + ) + + return { + 'current_date': current_date, + 'optimal_launch_dates': optimal_dates, + 'day_of_week_recommendation': day_of_week_rec, + 'seasonal_considerations': seasonal_rec, + 'competitive_timing': competitive_rec, + 'final_recommendation': self._generate_timing_recommendation( + optimal_dates, + seasonal_rec + ) + } + + def plan_seasonal_campaigns( + self, + app_category: str, + current_month: int = None + ) -> Dict[str, Any]: + """ + Identify seasonal opportunities for ASO campaigns. + + Args: + app_category: App category + current_month: Current month (1-12), defaults to current + + Returns: + Seasonal campaign opportunities + """ + if not current_month: + current_month = datetime.now().month + + # Identify relevant seasonal events + seasonal_opportunities = self._identify_seasonal_opportunities( + app_category, + current_month + ) + + # Generate campaign ideas + campaigns = [ + self._generate_seasonal_campaign(opportunity) + for opportunity in seasonal_opportunities + ] + + return { + 'current_month': current_month, + 'category': app_category, + 'seasonal_opportunities': seasonal_opportunities, + 'campaign_ideas': campaigns, + 'implementation_timeline': self._create_seasonal_timeline(campaigns) + } + + def _generate_apple_checklist(self, app_info: Dict[str, Any]) -> List[Dict[str, Any]]: + """Generate Apple App Store specific checklist.""" + return [ + { + 'category': 'App Store Connect Setup', + 'items': [ + {'task': 'App Store Connect account created', 'status': 'pending'}, + {'task': 'App bundle ID registered', 'status': 'pending'}, + {'task': 'App Privacy declarations completed', 'status': 'pending'}, + {'task': 'Age rating questionnaire completed', 'status': 'pending'} + ] + }, + { + 'category': 'Metadata (Apple)', + 'items': [ + {'task': 'App title (30 chars max)', 'status': 'pending'}, + {'task': 'Subtitle (30 chars max)', 'status': 'pending'}, + {'task': 'Promotional text (170 chars max)', 'status': 'pending'}, + {'task': 'Description (4000 chars max)', 'status': 'pending'}, + {'task': 'Keywords (100 chars, comma-separated)', 'status': 'pending'}, + {'task': 'Category selection (primary + secondary)', 'status': 'pending'} + ] + }, + { + 'category': 'Visual Assets (Apple)', + 'items': [ + {'task': 'App icon (1024x1024px)', 'status': 'pending'}, + {'task': 'Screenshots (iPhone 6.7" required)', 'status': 'pending'}, + {'task': 'Screenshots (iPhone 5.5" required)', 'status': 'pending'}, + {'task': 'Screenshots (iPad Pro 12.9" if iPad app)', 'status': 'pending'}, + {'task': 'App preview video (optional but recommended)', 'status': 'pending'} + ] + }, + { + 'category': 'Technical Requirements (Apple)', + 'items': [ + {'task': 'Build uploaded to App Store Connect', 'status': 'pending'}, + {'task': 'TestFlight testing completed', 'status': 'pending'}, + {'task': 'App tested on required iOS versions', 'status': 'pending'}, + {'task': 'Crash-free rate > 99%', 'status': 'pending'}, + {'task': 'All links in app/metadata working', 'status': 'pending'} + ] + }, + { + 'category': 'Legal & Privacy (Apple)', + 'items': [ + {'task': 'Privacy Policy URL provided', 'status': 'pending'}, + {'task': 'Terms of Service URL (if applicable)', 'status': 'pending'}, + {'task': 'Data collection declarations accurate', 'status': 'pending'}, + {'task': 'Third-party SDKs disclosed', 'status': 'pending'} + ] + } + ] + + def _generate_google_checklist(self, app_info: Dict[str, Any]) -> List[Dict[str, Any]]: + """Generate Google Play Store specific checklist.""" + return [ + { + 'category': 'Play Console Setup', + 'items': [ + {'task': 'Google Play Console account created', 'status': 'pending'}, + {'task': 'Developer profile completed', 'status': 'pending'}, + {'task': 'Payment merchant account linked (if paid app)', 'status': 'pending'}, + {'task': 'Content rating questionnaire completed', 'status': 'pending'} + ] + }, + { + 'category': 'Metadata (Google)', + 'items': [ + {'task': 'App title (50 chars max)', 'status': 'pending'}, + {'task': 'Short description (80 chars max)', 'status': 'pending'}, + {'task': 'Full description (4000 chars max)', 'status': 'pending'}, + {'task': 'Category selection', 'status': 'pending'}, + {'task': 'Tags (up to 5)', 'status': 'pending'} + ] + }, + { + 'category': 'Visual Assets (Google)', + 'items': [ + {'task': 'App icon (512x512px)', 'status': 'pending'}, + {'task': 'Feature graphic (1024x500px)', 'status': 'pending'}, + {'task': 'Screenshots (2-8 required, phone)', 'status': 'pending'}, + {'task': 'Screenshots (tablet, if applicable)', 'status': 'pending'}, + {'task': 'Promo video (YouTube link, optional)', 'status': 'pending'} + ] + }, + { + 'category': 'Technical Requirements (Google)', + 'items': [ + {'task': 'APK/AAB uploaded to Play Console', 'status': 'pending'}, + {'task': 'Internal testing completed', 'status': 'pending'}, + {'task': 'App tested on required Android versions', 'status': 'pending'}, + {'task': 'Target API level meets requirements', 'status': 'pending'}, + {'task': 'All permissions justified', 'status': 'pending'} + ] + }, + { + 'category': 'Legal & Privacy (Google)', + 'items': [ + {'task': 'Privacy Policy URL provided', 'status': 'pending'}, + {'task': 'Data safety section completed', 'status': 'pending'}, + {'task': 'Ads disclosure (if applicable)', 'status': 'pending'}, + {'task': 'In-app purchase disclosure (if applicable)', 'status': 'pending'} + ] + } + ] + + def _generate_universal_checklist(self, app_info: Dict[str, Any]) -> List[Dict[str, Any]]: + """Generate universal (both platforms) checklist.""" + return [ + { + 'category': 'Pre-Launch Marketing', + 'items': [ + {'task': 'Landing page created', 'status': 'pending'}, + {'task': 'Social media accounts setup', 'status': 'pending'}, + {'task': 'Press kit prepared', 'status': 'pending'}, + {'task': 'Beta tester feedback collected', 'status': 'pending'}, + {'task': 'Launch announcement drafted', 'status': 'pending'} + ] + }, + { + 'category': 'ASO Preparation', + 'items': [ + {'task': 'Keyword research completed', 'status': 'pending'}, + {'task': 'Competitor analysis done', 'status': 'pending'}, + {'task': 'A/B test plan created for post-launch', 'status': 'pending'}, + {'task': 'Analytics tracking configured', 'status': 'pending'} + ] + }, + { + 'category': 'Quality Assurance', + 'items': [ + {'task': 'All core features tested', 'status': 'pending'}, + {'task': 'User flows validated', 'status': 'pending'}, + {'task': 'Performance testing completed', 'status': 'pending'}, + {'task': 'Accessibility features tested', 'status': 'pending'}, + {'task': 'Security audit completed', 'status': 'pending'} + ] + }, + { + 'category': 'Support Infrastructure', + 'items': [ + {'task': 'Support email/system setup', 'status': 'pending'}, + {'task': 'FAQ page created', 'status': 'pending'}, + {'task': 'Documentation for users prepared', 'status': 'pending'}, + {'task': 'Team trained on handling reviews', 'status': 'pending'} + ] + } + ] + + def _generate_launch_timeline(self, launch_date: str) -> List[Dict[str, Any]]: + """Generate timeline with milestones leading to launch.""" + launch_dt = datetime.strptime(launch_date, '%Y-%m-%d') + + milestones = [ + { + 'date': (launch_dt - timedelta(days=90)).strftime('%Y-%m-%d'), + 'milestone': '90 days before: Complete keyword research and competitor analysis' + }, + { + 'date': (launch_dt - timedelta(days=60)).strftime('%Y-%m-%d'), + 'milestone': '60 days before: Finalize metadata and visual assets' + }, + { + 'date': (launch_dt - timedelta(days=45)).strftime('%Y-%m-%d'), + 'milestone': '45 days before: Begin beta testing program' + }, + { + 'date': (launch_dt - timedelta(days=30)).strftime('%Y-%m-%d'), + 'milestone': '30 days before: Submit app for review (Apple typically takes 1-2 days, Google instant)' + }, + { + 'date': (launch_dt - timedelta(days=14)).strftime('%Y-%m-%d'), + 'milestone': '14 days before: Prepare launch marketing materials' + }, + { + 'date': (launch_dt - timedelta(days=7)).strftime('%Y-%m-%d'), + 'milestone': '7 days before: Set up analytics and monitoring' + }, + { + 'date': launch_dt.strftime('%Y-%m-%d'), + 'milestone': 'Launch Day: Release app and execute marketing plan' + }, + { + 'date': (launch_dt + timedelta(days=7)).strftime('%Y-%m-%d'), + 'milestone': '7 days after: Monitor metrics, respond to reviews, address critical issues' + }, + { + 'date': (launch_dt + timedelta(days=30)).strftime('%Y-%m-%d'), + 'milestone': '30 days after: Analyze launch metrics, plan first update' + } + ] + + return milestones + + def _calculate_checklist_summary(self, checklists: Dict[str, List[Dict[str, Any]]]) -> Dict[str, Any]: + """Calculate completion summary.""" + total_items = 0 + completed_items = 0 + + for platform, categories in checklists.items(): + for category in categories: + for item in category['items']: + total_items += 1 + if item['status'] == 'completed': + completed_items += 1 + + completion_percentage = (completed_items / total_items * 100) if total_items > 0 else 0 + + return { + 'total_items': total_items, + 'completed_items': completed_items, + 'pending_items': total_items - completed_items, + 'completion_percentage': round(completion_percentage, 1), + 'is_ready_to_launch': completion_percentage == 100 + } + + def _validate_apple_compliance( + self, + app_data: Dict[str, Any], + validation_results: Dict[str, Any] + ) -> None: + """Validate Apple App Store compliance.""" + # Check for required fields + if not app_data.get('privacy_policy_url'): + validation_results['errors'].append("Privacy Policy URL is required") + + if not app_data.get('app_icon'): + validation_results['errors'].append("App icon (1024x1024px) is required") + + # Check metadata character limits + title = app_data.get('title', '') + if len(title) > 30: + validation_results['errors'].append(f"Title exceeds 30 characters ({len(title)})") + + # Warnings for best practices + subtitle = app_data.get('subtitle', '') + if not subtitle: + validation_results['warnings'].append("Subtitle is empty - consider adding for better discoverability") + + keywords = app_data.get('keywords', '') + if len(keywords) < 80: + validation_results['warnings'].append( + f"Keywords field underutilized ({len(keywords)}/100 chars) - add more keywords" + ) + + def _validate_google_compliance( + self, + app_data: Dict[str, Any], + validation_results: Dict[str, Any] + ) -> None: + """Validate Google Play Store compliance.""" + # Check for required fields + if not app_data.get('privacy_policy_url'): + validation_results['errors'].append("Privacy Policy URL is required") + + if not app_data.get('feature_graphic'): + validation_results['errors'].append("Feature graphic (1024x500px) is required") + + # Check metadata character limits + title = app_data.get('title', '') + if len(title) > 50: + validation_results['errors'].append(f"Title exceeds 50 characters ({len(title)})") + + short_desc = app_data.get('short_description', '') + if len(short_desc) > 80: + validation_results['errors'].append(f"Short description exceeds 80 characters ({len(short_desc)})") + + # Warnings + if not short_desc: + validation_results['warnings'].append("Short description is empty") + + def _calculate_next_versions( + self, + current_version: str, + update_frequency: str, + feature_count: int + ) -> List[str]: + """Calculate next version numbers.""" + # Parse current version (assume semantic versioning) + parts = current_version.split('.') + major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2] if len(parts) > 2 else 0) + + versions = [] + for i in range(feature_count): + if update_frequency == 'weekly': + patch += 1 + elif update_frequency == 'biweekly': + patch += 1 + elif update_frequency == 'monthly': + minor += 1 + patch = 0 + else: # quarterly + minor += 1 + patch = 0 + + versions.append(f"{major}.{minor}.{patch}") + + return versions + + def _distribute_features( + self, + features: List[str], + versions: List[str] + ) -> List[Dict[str, Any]]: + """Distribute features across versions.""" + features_per_version = max(1, len(features) // len(versions)) + + schedule = [] + for i, version in enumerate(versions): + start_idx = i * features_per_version + end_idx = start_idx + features_per_version if i < len(versions) - 1 else len(features) + + schedule.append({ + 'version': version, + 'features': features[start_idx:end_idx], + 'release_priority': 'high' if i == 0 else ('medium' if i < len(versions) // 2 else 'low') + }) + + return schedule + + def _generate_whats_new_template(self, version_data: Dict[str, Any]) -> Dict[str, str]: + """Generate What's New template for version.""" + features_list = '\n'.join([f"โ€ข {feature}" for feature in version_data['features']]) + + template = f"""Version {version_data['version']} + +{features_list} + +We're constantly improving your experience. Thanks for using [App Name]! + +Have feedback? Contact us at support@[company].com""" + + return { + 'version': version_data['version'], + 'template': template + } + + def _generate_update_recommendations(self, update_frequency: str) -> List[str]: + """Generate recommendations for update strategy.""" + recommendations = [] + + if update_frequency == 'weekly': + recommendations.append("Weekly updates show active development but ensure quality doesn't suffer") + elif update_frequency == 'monthly': + recommendations.append("Monthly updates are optimal for most apps - balance features and stability") + + recommendations.extend([ + "Include bug fixes in every update", + "Update 'What's New' section with each release", + "Respond to reviews mentioning fixed issues" + ]) + + return recommendations + + def _recommend_day_of_week(self, app_category: str) -> Dict[str, Any]: + """Recommend best day of week to launch.""" + # General recommendations based on category + if app_category.lower() in ['games', 'entertainment']: + return { + 'recommended_day': 'Thursday', + 'rationale': 'People download entertainment apps before weekend' + } + elif app_category.lower() in ['productivity', 'business']: + return { + 'recommended_day': 'Tuesday', + 'rationale': 'Business users most active mid-week' + } + else: + return { + 'recommended_day': 'Wednesday', + 'rationale': 'Mid-week provides good balance and review potential' + } + + def _recommend_seasonal_timing(self, app_category: str, current_date: str) -> Dict[str, Any]: + """Recommend seasonal timing considerations.""" + current_dt = datetime.strptime(current_date, '%Y-%m-%d') + month = current_dt.month + + # Avoid certain periods + avoid_periods = [] + if month == 12: + avoid_periods.append("Late December - low user engagement during holidays") + if month in [7, 8]: + avoid_periods.append("Summer months - some categories see lower engagement") + + # Recommend periods + good_periods = [] + if month in [1, 9]: + good_periods.append("New Year/Back-to-school - high user engagement") + if month in [10, 11]: + good_periods.append("Pre-holiday season - good for shopping/gift apps") + + return { + 'current_month': month, + 'avoid_periods': avoid_periods, + 'good_periods': good_periods + } + + def _analyze_competitive_timing(self, app_category: str) -> Dict[str, str]: + """Analyze competitive timing considerations.""" + return { + 'recommendation': 'Research competitor launch schedules in your category', + 'strategy': 'Avoid launching same week as major competitor updates' + } + + def _calculate_optimal_dates( + self, + current_date: str, + day_rec: Dict[str, Any], + seasonal_rec: Dict[str, Any] + ) -> List[str]: + """Calculate optimal launch dates.""" + current_dt = datetime.strptime(current_date, '%Y-%m-%d') + + # Find next occurrence of recommended day + target_day = day_rec['recommended_day'] + days_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4} + target_day_num = days_map.get(target_day, 2) + + days_ahead = (target_day_num - current_dt.weekday()) % 7 + if days_ahead == 0: + days_ahead = 7 + + next_target_date = current_dt + timedelta(days=days_ahead) + + optimal_dates = [ + next_target_date.strftime('%Y-%m-%d'), + (next_target_date + timedelta(days=7)).strftime('%Y-%m-%d'), + (next_target_date + timedelta(days=14)).strftime('%Y-%m-%d') + ] + + return optimal_dates + + def _generate_timing_recommendation( + self, + optimal_dates: List[str], + seasonal_rec: Dict[str, Any] + ) -> str: + """Generate final timing recommendation.""" + if seasonal_rec['avoid_periods']: + return f"Consider launching in {optimal_dates[1]} to avoid {seasonal_rec['avoid_periods'][0]}" + elif seasonal_rec['good_periods']: + return f"Launch on {optimal_dates[0]} to capitalize on {seasonal_rec['good_periods'][0]}" + else: + return f"Recommended launch date: {optimal_dates[0]}" + + def _identify_seasonal_opportunities( + self, + app_category: str, + current_month: int + ) -> List[Dict[str, Any]]: + """Identify seasonal opportunities for category.""" + opportunities = [] + + # Universal opportunities + if current_month == 1: + opportunities.append({ + 'event': 'New Year Resolutions', + 'dates': 'January 1-31', + 'relevance': 'high' if app_category.lower() in ['health', 'fitness', 'productivity'] else 'medium' + }) + + if current_month in [11, 12]: + opportunities.append({ + 'event': 'Holiday Shopping Season', + 'dates': 'November-December', + 'relevance': 'high' if app_category.lower() in ['shopping', 'gifts'] else 'low' + }) + + # Category-specific + if app_category.lower() == 'education' and current_month in [8, 9]: + opportunities.append({ + 'event': 'Back to School', + 'dates': 'August-September', + 'relevance': 'high' + }) + + return opportunities + + def _generate_seasonal_campaign(self, opportunity: Dict[str, Any]) -> Dict[str, Any]: + """Generate campaign idea for seasonal opportunity.""" + return { + 'event': opportunity['event'], + 'campaign_idea': f"Create themed visuals and messaging for {opportunity['event']}", + 'metadata_updates': 'Update app description and screenshots with seasonal themes', + 'promotion_strategy': 'Consider limited-time features or discounts' + } + + def _create_seasonal_timeline(self, campaigns: List[Dict[str, Any]]) -> List[str]: + """Create implementation timeline for campaigns.""" + return [ + f"30 days before: Plan {campaign['event']} campaign strategy" + for campaign in campaigns + ] + + +def generate_launch_checklist( + platform: str, + app_info: Dict[str, Any], + launch_date: Optional[str] = None +) -> Dict[str, Any]: + """ + Convenience function to generate launch checklist. + + Args: + platform: Platform ('apple', 'google', or 'both') + app_info: App information + launch_date: Target launch date + + Returns: + Complete launch checklist + """ + generator = LaunchChecklistGenerator(platform) + return generator.generate_prelaunch_checklist(app_info, launch_date) diff --git a/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/localization_helper.py b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/localization_helper.py new file mode 100644 index 00000000..c47003ca --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/localization_helper.py @@ -0,0 +1,588 @@ +""" +Localization helper module for App Store Optimization. +Manages multi-language ASO optimization strategies. +""" + +from typing import Dict, List, Any, Optional, Tuple + + +class LocalizationHelper: + """Helps manage multi-language ASO optimization.""" + + # Priority markets by language (based on app store revenue and user base) + PRIORITY_MARKETS = { + 'tier_1': [ + {'language': 'en-US', 'market': 'United States', 'revenue_share': 0.25}, + {'language': 'zh-CN', 'market': 'China', 'revenue_share': 0.20}, + {'language': 'ja-JP', 'market': 'Japan', 'revenue_share': 0.10}, + {'language': 'de-DE', 'market': 'Germany', 'revenue_share': 0.08}, + {'language': 'en-GB', 'market': 'United Kingdom', 'revenue_share': 0.06} + ], + 'tier_2': [ + {'language': 'fr-FR', 'market': 'France', 'revenue_share': 0.05}, + {'language': 'ko-KR', 'market': 'South Korea', 'revenue_share': 0.05}, + {'language': 'es-ES', 'market': 'Spain', 'revenue_share': 0.03}, + {'language': 'it-IT', 'market': 'Italy', 'revenue_share': 0.03}, + {'language': 'pt-BR', 'market': 'Brazil', 'revenue_share': 0.03} + ], + 'tier_3': [ + {'language': 'ru-RU', 'market': 'Russia', 'revenue_share': 0.02}, + {'language': 'es-MX', 'market': 'Mexico', 'revenue_share': 0.02}, + {'language': 'nl-NL', 'market': 'Netherlands', 'revenue_share': 0.02}, + {'language': 'sv-SE', 'market': 'Sweden', 'revenue_share': 0.01}, + {'language': 'pl-PL', 'market': 'Poland', 'revenue_share': 0.01} + ] + } + + # Character limit multipliers by language (some languages need more/less space) + CHAR_MULTIPLIERS = { + 'en': 1.0, + 'zh': 0.6, # Chinese characters are more compact + 'ja': 0.7, # Japanese uses kanji + 'ko': 0.8, # Korean is relatively compact + 'de': 1.3, # German words are typically longer + 'fr': 1.2, # French tends to be longer + 'es': 1.1, # Spanish slightly longer + 'pt': 1.1, # Portuguese similar to Spanish + 'ru': 1.1, # Russian similar length + 'ar': 1.0, # Arabic varies + 'it': 1.1 # Italian similar to Spanish + } + + def __init__(self, app_category: str = 'general'): + """ + Initialize localization helper. + + Args: + app_category: App category to prioritize relevant markets + """ + self.app_category = app_category + self.localization_plans = [] + + def identify_target_markets( + self, + current_market: str = 'en-US', + budget_level: str = 'medium', + target_market_count: int = 5 + ) -> Dict[str, Any]: + """ + Recommend priority markets for localization. + + Args: + current_market: Current/primary market + budget_level: 'low', 'medium', or 'high' + target_market_count: Number of markets to target + + Returns: + Prioritized market recommendations + """ + # Determine tier priorities based on budget + if budget_level == 'low': + priority_tiers = ['tier_1'] + max_markets = min(target_market_count, 3) + elif budget_level == 'medium': + priority_tiers = ['tier_1', 'tier_2'] + max_markets = min(target_market_count, 8) + else: # high budget + priority_tiers = ['tier_1', 'tier_2', 'tier_3'] + max_markets = target_market_count + + # Collect markets from priority tiers + recommended_markets = [] + for tier in priority_tiers: + for market in self.PRIORITY_MARKETS[tier]: + if market['language'] != current_market: + recommended_markets.append({ + **market, + 'tier': tier, + 'estimated_translation_cost': self._estimate_translation_cost( + market['language'] + ) + }) + + # Sort by revenue share and limit + recommended_markets.sort(key=lambda x: x['revenue_share'], reverse=True) + recommended_markets = recommended_markets[:max_markets] + + # Calculate potential ROI + total_potential_revenue_share = sum(m['revenue_share'] for m in recommended_markets) + + return { + 'recommended_markets': recommended_markets, + 'total_markets': len(recommended_markets), + 'estimated_total_revenue_lift': f"{total_potential_revenue_share*100:.1f}%", + 'estimated_cost': self._estimate_total_localization_cost(recommended_markets), + 'implementation_priority': self._prioritize_implementation(recommended_markets) + } + + def translate_metadata( + self, + source_metadata: Dict[str, str], + source_language: str, + target_language: str, + platform: str = 'apple' + ) -> Dict[str, Any]: + """ + Generate localized metadata with character limit considerations. + + Args: + source_metadata: Original metadata (title, description, etc.) + source_language: Source language code (e.g., 'en') + target_language: Target language code (e.g., 'es') + platform: 'apple' or 'google' + + Returns: + Localized metadata with character limit validation + """ + # Get character multiplier + target_lang_code = target_language.split('-')[0] + char_multiplier = self.CHAR_MULTIPLIERS.get(target_lang_code, 1.0) + + # Platform-specific limits + if platform == 'apple': + limits = {'title': 30, 'subtitle': 30, 'description': 4000, 'keywords': 100} + else: + limits = {'title': 50, 'short_description': 80, 'description': 4000} + + localized_metadata = {} + warnings = [] + + for field, text in source_metadata.items(): + if field not in limits: + continue + + # Estimate target length + estimated_length = int(len(text) * char_multiplier) + limit = limits[field] + + localized_metadata[field] = { + 'original_text': text, + 'original_length': len(text), + 'estimated_target_length': estimated_length, + 'character_limit': limit, + 'fits_within_limit': estimated_length <= limit, + 'translation_notes': self._get_translation_notes( + field, + target_language, + estimated_length, + limit + ) + } + + if estimated_length > limit: + warnings.append( + f"{field}: Estimated length ({estimated_length}) may exceed limit ({limit}) - " + f"condensing may be required" + ) + + return { + 'source_language': source_language, + 'target_language': target_language, + 'platform': platform, + 'localized_fields': localized_metadata, + 'character_multiplier': char_multiplier, + 'warnings': warnings, + 'recommendations': self._generate_translation_recommendations( + target_language, + warnings + ) + } + + def adapt_keywords( + self, + source_keywords: List[str], + source_language: str, + target_language: str, + target_market: str + ) -> Dict[str, Any]: + """ + Adapt keywords for target market (not just direct translation). + + Args: + source_keywords: Original keywords + source_language: Source language code + target_language: Target language code + target_market: Target market (e.g., 'France', 'Japan') + + Returns: + Adapted keyword recommendations + """ + # Cultural adaptation considerations + cultural_notes = self._get_cultural_keyword_considerations(target_market) + + # Search behavior differences + search_patterns = self._get_search_patterns(target_market) + + adapted_keywords = [] + for keyword in source_keywords: + adapted_keywords.append({ + 'source_keyword': keyword, + 'adaptation_strategy': self._determine_adaptation_strategy( + keyword, + target_market + ), + 'cultural_considerations': cultural_notes.get(keyword, []), + 'priority': 'high' if keyword in source_keywords[:3] else 'medium' + }) + + return { + 'source_language': source_language, + 'target_language': target_language, + 'target_market': target_market, + 'adapted_keywords': adapted_keywords, + 'search_behavior_notes': search_patterns, + 'recommendations': [ + 'Use native speakers for keyword research', + 'Test keywords with local users before finalizing', + 'Consider local competitors\' keyword strategies', + 'Monitor search trends in target market' + ] + } + + def validate_translations( + self, + translated_metadata: Dict[str, str], + target_language: str, + platform: str = 'apple' + ) -> Dict[str, Any]: + """ + Validate translated metadata for character limits and quality. + + Args: + translated_metadata: Translated text fields + target_language: Target language code + platform: 'apple' or 'google' + + Returns: + Validation report + """ + # Platform limits + if platform == 'apple': + limits = {'title': 30, 'subtitle': 30, 'description': 4000, 'keywords': 100} + else: + limits = {'title': 50, 'short_description': 80, 'description': 4000} + + validation_results = { + 'is_valid': True, + 'field_validations': {}, + 'errors': [], + 'warnings': [] + } + + for field, text in translated_metadata.items(): + if field not in limits: + continue + + actual_length = len(text) + limit = limits[field] + is_within_limit = actual_length <= limit + + validation_results['field_validations'][field] = { + 'text': text, + 'length': actual_length, + 'limit': limit, + 'is_valid': is_within_limit, + 'usage_percentage': round((actual_length / limit) * 100, 1) + } + + if not is_within_limit: + validation_results['is_valid'] = False + validation_results['errors'].append( + f"{field} exceeds limit: {actual_length}/{limit} characters" + ) + + # Quality checks + quality_issues = self._check_translation_quality( + translated_metadata, + target_language + ) + + validation_results['quality_checks'] = quality_issues + + if quality_issues: + validation_results['warnings'].extend( + [f"Quality issue: {issue}" for issue in quality_issues] + ) + + return validation_results + + def calculate_localization_roi( + self, + target_markets: List[str], + current_monthly_downloads: int, + localization_cost: float, + expected_lift_percentage: float = 0.15 + ) -> Dict[str, Any]: + """ + Estimate ROI of localization investment. + + Args: + target_markets: List of market codes + current_monthly_downloads: Current monthly downloads + localization_cost: Total cost to localize + expected_lift_percentage: Expected download increase (default 15%) + + Returns: + ROI analysis + """ + # Estimate market-specific lift + market_data = [] + total_expected_lift = 0 + + for market_code in target_markets: + # Find market in priority lists + market_info = None + for tier_name, markets in self.PRIORITY_MARKETS.items(): + for m in markets: + if m['language'] == market_code: + market_info = m + break + + if not market_info: + continue + + # Estimate downloads from this market + market_downloads = int(current_monthly_downloads * market_info['revenue_share']) + expected_increase = int(market_downloads * expected_lift_percentage) + total_expected_lift += expected_increase + + market_data.append({ + 'market': market_info['market'], + 'current_monthly_downloads': market_downloads, + 'expected_increase': expected_increase, + 'revenue_potential': market_info['revenue_share'] + }) + + # Calculate payback period (assuming $2 revenue per download) + revenue_per_download = 2.0 + monthly_additional_revenue = total_expected_lift * revenue_per_download + payback_months = (localization_cost / monthly_additional_revenue) if monthly_additional_revenue > 0 else float('inf') + + return { + 'markets_analyzed': len(market_data), + 'market_breakdown': market_data, + 'total_expected_monthly_lift': total_expected_lift, + 'expected_monthly_revenue_increase': f"${monthly_additional_revenue:,.2f}", + 'localization_cost': f"${localization_cost:,.2f}", + 'payback_period_months': round(payback_months, 1) if payback_months != float('inf') else 'N/A', + 'annual_roi': f"{((monthly_additional_revenue * 12 - localization_cost) / localization_cost * 100):.1f}%" if payback_months != float('inf') else 'Negative', + 'recommendation': self._generate_roi_recommendation(payback_months) + } + + def _estimate_translation_cost(self, language: str) -> Dict[str, float]: + """Estimate translation cost for a language.""" + # Base cost per word (professional translation) + base_cost_per_word = 0.12 + + # Language-specific multipliers + multipliers = { + 'zh-CN': 1.5, # Chinese requires specialist + 'ja-JP': 1.5, # Japanese requires specialist + 'ko-KR': 1.3, + 'ar-SA': 1.4, # Arabic (right-to-left) + 'default': 1.0 + } + + multiplier = multipliers.get(language, multipliers['default']) + + # Typical word counts for app store metadata + typical_word_counts = { + 'title': 5, + 'subtitle': 5, + 'description': 300, + 'keywords': 20, + 'screenshots': 50 # Caption text + } + + total_words = sum(typical_word_counts.values()) + estimated_cost = total_words * base_cost_per_word * multiplier + + return { + 'cost_per_word': base_cost_per_word * multiplier, + 'total_words': total_words, + 'estimated_cost': round(estimated_cost, 2) + } + + def _estimate_total_localization_cost(self, markets: List[Dict[str, Any]]) -> str: + """Estimate total cost for multiple markets.""" + total = sum(m['estimated_translation_cost']['estimated_cost'] for m in markets) + return f"${total:,.2f}" + + def _prioritize_implementation(self, markets: List[Dict[str, Any]]) -> List[Dict[str, str]]: + """Create phased implementation plan.""" + phases = [] + + # Phase 1: Top revenue markets + phase_1 = [m for m in markets[:3]] + if phase_1: + phases.append({ + 'phase': 'Phase 1 (First 30 days)', + 'markets': ', '.join([m['market'] for m in phase_1]), + 'rationale': 'Highest revenue potential markets' + }) + + # Phase 2: Remaining tier 1 and top tier 2 + phase_2 = [m for m in markets[3:6]] + if phase_2: + phases.append({ + 'phase': 'Phase 2 (Days 31-60)', + 'markets': ', '.join([m['market'] for m in phase_2]), + 'rationale': 'Strong revenue markets with good ROI' + }) + + # Phase 3: Remaining markets + phase_3 = [m for m in markets[6:]] + if phase_3: + phases.append({ + 'phase': 'Phase 3 (Days 61-90)', + 'markets': ', '.join([m['market'] for m in phase_3]), + 'rationale': 'Complete global coverage' + }) + + return phases + + def _get_translation_notes( + self, + field: str, + target_language: str, + estimated_length: int, + limit: int + ) -> List[str]: + """Get translation-specific notes for field.""" + notes = [] + + if estimated_length > limit: + notes.append(f"Condensing required - aim for {limit - 10} characters to allow buffer") + + if field == 'title' and target_language.startswith('zh'): + notes.append("Chinese characters convey more meaning - may need fewer characters") + + if field == 'keywords' and target_language.startswith('de'): + notes.append("German compound words may be longer - prioritize shorter keywords") + + return notes + + def _generate_translation_recommendations( + self, + target_language: str, + warnings: List[str] + ) -> List[str]: + """Generate translation recommendations.""" + recommendations = [ + "Use professional native speakers for translation", + "Test translations with local users before finalizing" + ] + + if warnings: + recommendations.append("Work with translator to condense text while preserving meaning") + + if target_language.startswith('zh') or target_language.startswith('ja'): + recommendations.append("Consider cultural context and local idioms") + + return recommendations + + def _get_cultural_keyword_considerations(self, target_market: str) -> Dict[str, List[str]]: + """Get cultural considerations for keywords by market.""" + # Simplified example - real implementation would be more comprehensive + considerations = { + 'China': ['Avoid politically sensitive terms', 'Consider local alternatives to blocked services'], + 'Japan': ['Honorific language important', 'Technical terms often use katakana'], + 'Germany': ['Privacy and security terms resonate', 'Efficiency and quality valued'], + 'France': ['French language protection laws', 'Prefer French terms over English'], + 'default': ['Research local search behavior', 'Test with native speakers'] + } + + return considerations.get(target_market, considerations['default']) + + def _get_search_patterns(self, target_market: str) -> List[str]: + """Get search pattern notes for market.""" + patterns = { + 'China': ['Use both simplified characters and romanization', 'Brand names often romanized'], + 'Japan': ['Mix of kanji, hiragana, and katakana', 'English words common in tech'], + 'Germany': ['Compound words common', 'Specific technical terminology'], + 'default': ['Research local search trends', 'Monitor competitor keywords'] + } + + return patterns.get(target_market, patterns['default']) + + def _determine_adaptation_strategy(self, keyword: str, target_market: str) -> str: + """Determine how to adapt keyword for market.""" + # Simplified logic + if target_market in ['China', 'Japan', 'Korea']: + return 'full_localization' # Complete translation needed + elif target_market in ['Germany', 'France', 'Spain']: + return 'adapt_and_translate' # Some adaptation needed + else: + return 'direct_translation' # Direct translation usually sufficient + + def _check_translation_quality( + self, + translated_metadata: Dict[str, str], + target_language: str + ) -> List[str]: + """Basic quality checks for translations.""" + issues = [] + + # Check for untranslated placeholders + for field, text in translated_metadata.items(): + if '[' in text or '{' in text or 'TODO' in text.upper(): + issues.append(f"{field} contains placeholder text") + + # Check for excessive punctuation + for field, text in translated_metadata.items(): + if text.count('!') > 3: + issues.append(f"{field} has excessive exclamation marks") + + return issues + + def _generate_roi_recommendation(self, payback_months: float) -> str: + """Generate ROI recommendation.""" + if payback_months <= 3: + return "Excellent ROI - proceed immediately" + elif payback_months <= 6: + return "Good ROI - recommended investment" + elif payback_months <= 12: + return "Moderate ROI - consider if strategic market" + else: + return "Low ROI - reconsider or focus on higher-priority markets first" + + +def plan_localization_strategy( + current_market: str, + budget_level: str, + monthly_downloads: int +) -> Dict[str, Any]: + """ + Convenience function to plan localization strategy. + + Args: + current_market: Current market code + budget_level: Budget level + monthly_downloads: Current monthly downloads + + Returns: + Complete localization plan + """ + helper = LocalizationHelper() + + target_markets = helper.identify_target_markets( + current_market=current_market, + budget_level=budget_level + ) + + # Extract market codes + market_codes = [m['language'] for m in target_markets['recommended_markets']] + + # Calculate ROI + estimated_cost = float(target_markets['estimated_cost'].replace('$', '').replace(',', '')) + + roi_analysis = helper.calculate_localization_roi( + market_codes, + monthly_downloads, + estimated_cost + ) + + return { + 'target_markets': target_markets, + 'roi_analysis': roi_analysis + } diff --git a/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/metadata_optimizer.py b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/metadata_optimizer.py new file mode 100644 index 00000000..7b506140 --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/metadata_optimizer.py @@ -0,0 +1,581 @@ +""" +Metadata optimization module for App Store Optimization. +Optimizes titles, descriptions, and keyword fields with platform-specific character limit validation. +""" + +from typing import Dict, List, Any, Optional, Tuple +import re + + +class MetadataOptimizer: + """Optimizes app store metadata for maximum discoverability and conversion.""" + + # Platform-specific character limits + CHAR_LIMITS = { + 'apple': { + 'title': 30, + 'subtitle': 30, + 'promotional_text': 170, + 'description': 4000, + 'keywords': 100, + 'whats_new': 4000 + }, + 'google': { + 'title': 50, + 'short_description': 80, + 'full_description': 4000 + } + } + + def __init__(self, platform: str = 'apple'): + """ + Initialize metadata optimizer. + + Args: + platform: 'apple' or 'google' + """ + if platform not in ['apple', 'google']: + raise ValueError("Platform must be 'apple' or 'google'") + + self.platform = platform + self.limits = self.CHAR_LIMITS[platform] + + def optimize_title( + self, + app_name: str, + target_keywords: List[str], + include_brand: bool = True + ) -> Dict[str, Any]: + """ + Optimize app title with keyword integration. + + Args: + app_name: Your app's brand name + target_keywords: List of keywords to potentially include + include_brand: Whether to include brand name + + Returns: + Optimized title options with analysis + """ + max_length = self.limits['title'] + + title_options = [] + + # Option 1: Brand name only + if include_brand: + option1 = app_name[:max_length] + title_options.append({ + 'title': option1, + 'length': len(option1), + 'remaining_chars': max_length - len(option1), + 'keywords_included': [], + 'strategy': 'brand_only', + 'pros': ['Maximum brand recognition', 'Clean and simple'], + 'cons': ['No keyword targeting', 'Lower discoverability'] + }) + + # Option 2: Brand + Primary Keyword + if target_keywords: + primary_keyword = target_keywords[0] + option2 = self._build_title_with_keywords( + app_name, + [primary_keyword], + max_length + ) + if option2: + title_options.append({ + 'title': option2, + 'length': len(option2), + 'remaining_chars': max_length - len(option2), + 'keywords_included': [primary_keyword], + 'strategy': 'brand_plus_primary', + 'pros': ['Targets main keyword', 'Maintains brand identity'], + 'cons': ['Limited keyword coverage'] + }) + + # Option 3: Brand + Multiple Keywords (if space allows) + if len(target_keywords) > 1: + option3 = self._build_title_with_keywords( + app_name, + target_keywords[:2], + max_length + ) + if option3: + title_options.append({ + 'title': option3, + 'length': len(option3), + 'remaining_chars': max_length - len(option3), + 'keywords_included': target_keywords[:2], + 'strategy': 'brand_plus_multiple', + 'pros': ['Multiple keyword targets', 'Better discoverability'], + 'cons': ['May feel cluttered', 'Less brand focus'] + }) + + # Option 4: Keyword-first approach (for new apps) + if target_keywords and not include_brand: + option4 = " ".join(target_keywords[:2])[:max_length] + title_options.append({ + 'title': option4, + 'length': len(option4), + 'remaining_chars': max_length - len(option4), + 'keywords_included': target_keywords[:2], + 'strategy': 'keyword_first', + 'pros': ['Maximum SEO benefit', 'Clear functionality'], + 'cons': ['No brand recognition', 'Generic appearance'] + }) + + return { + 'platform': self.platform, + 'max_length': max_length, + 'options': title_options, + 'recommendation': self._recommend_title_option(title_options) + } + + def optimize_description( + self, + app_info: Dict[str, Any], + target_keywords: List[str], + description_type: str = 'full' + ) -> Dict[str, Any]: + """ + Optimize app description with keyword integration and conversion focus. + + Args: + app_info: Dict with 'name', 'key_features', 'unique_value', 'target_audience' + target_keywords: List of keywords to integrate naturally + description_type: 'full', 'short' (Google), 'subtitle' (Apple) + + Returns: + Optimized description with analysis + """ + if description_type == 'short' and self.platform == 'google': + return self._optimize_short_description(app_info, target_keywords) + elif description_type == 'subtitle' and self.platform == 'apple': + return self._optimize_subtitle(app_info, target_keywords) + else: + return self._optimize_full_description(app_info, target_keywords) + + def optimize_keyword_field( + self, + target_keywords: List[str], + app_title: str = "", + app_description: str = "" + ) -> Dict[str, Any]: + """ + Optimize Apple's 100-character keyword field. + + Rules: + - No spaces between commas + - No plural forms if singular exists + - No duplicates + - Keywords in title/subtitle are already indexed + + Args: + target_keywords: List of target keywords + app_title: Current app title (to avoid duplication) + app_description: Current description (to check coverage) + + Returns: + Optimized keyword field (comma-separated, no spaces) + """ + if self.platform != 'apple': + return {'error': 'Keyword field optimization only applies to Apple App Store'} + + max_length = self.limits['keywords'] + + # Extract words already in title (these don't need to be in keyword field) + title_words = set(app_title.lower().split()) if app_title else set() + + # Process keywords + processed_keywords = [] + for keyword in target_keywords: + keyword_lower = keyword.lower().strip() + + # Skip if already in title + if keyword_lower in title_words: + continue + + # Remove duplicates and process + words = keyword_lower.split() + for word in words: + if word not in processed_keywords and word not in title_words: + processed_keywords.append(word) + + # Remove plurals if singular exists + deduplicated = self._remove_plural_duplicates(processed_keywords) + + # Build keyword field within 100 character limit + keyword_field = self._build_keyword_field(deduplicated, max_length) + + # Calculate keyword density in description + density = self._calculate_coverage(target_keywords, app_description) + + return { + 'keyword_field': keyword_field, + 'length': len(keyword_field), + 'remaining_chars': max_length - len(keyword_field), + 'keywords_included': keyword_field.split(','), + 'keywords_count': len(keyword_field.split(',')), + 'keywords_excluded': [kw for kw in target_keywords if kw.lower() not in keyword_field], + 'description_coverage': density, + 'optimization_tips': [ + 'Keywords in title are auto-indexed - no need to repeat', + 'Use singular forms only (Apple indexes plurals automatically)', + 'No spaces between commas to maximize character usage', + 'Update keyword field with each app update to test variations' + ] + } + + def validate_character_limits( + self, + metadata: Dict[str, str] + ) -> Dict[str, Any]: + """ + Validate all metadata fields against platform character limits. + + Args: + metadata: Dictionary of field_name: value + + Returns: + Validation report with errors and warnings + """ + validation_results = { + 'is_valid': True, + 'errors': [], + 'warnings': [], + 'field_status': {} + } + + for field_name, value in metadata.items(): + if field_name not in self.limits: + validation_results['warnings'].append( + f"Unknown field '{field_name}' for {self.platform} platform" + ) + continue + + max_length = self.limits[field_name] + actual_length = len(value) + remaining = max_length - actual_length + + field_status = { + 'value': value, + 'length': actual_length, + 'limit': max_length, + 'remaining': remaining, + 'is_valid': actual_length <= max_length, + 'usage_percentage': round((actual_length / max_length) * 100, 1) + } + + validation_results['field_status'][field_name] = field_status + + if actual_length > max_length: + validation_results['is_valid'] = False + validation_results['errors'].append( + f"'{field_name}' exceeds limit: {actual_length}/{max_length} chars" + ) + elif remaining > max_length * 0.2: # More than 20% unused + validation_results['warnings'].append( + f"'{field_name}' under-utilizes space: {remaining} chars remaining" + ) + + return validation_results + + def calculate_keyword_density( + self, + text: str, + target_keywords: List[str] + ) -> Dict[str, Any]: + """ + Calculate keyword density in text. + + Args: + text: Text to analyze + target_keywords: Keywords to check + + Returns: + Density analysis + """ + text_lower = text.lower() + total_words = len(text_lower.split()) + + keyword_densities = {} + for keyword in target_keywords: + keyword_lower = keyword.lower() + count = text_lower.count(keyword_lower) + density = (count / total_words * 100) if total_words > 0 else 0 + + keyword_densities[keyword] = { + 'occurrences': count, + 'density_percentage': round(density, 2), + 'status': self._assess_density(density) + } + + # Overall assessment + total_keyword_occurrences = sum(kw['occurrences'] for kw in keyword_densities.values()) + overall_density = (total_keyword_occurrences / total_words * 100) if total_words > 0 else 0 + + return { + 'total_words': total_words, + 'keyword_densities': keyword_densities, + 'overall_keyword_density': round(overall_density, 2), + 'assessment': self._assess_overall_density(overall_density), + 'recommendations': self._generate_density_recommendations(keyword_densities) + } + + def _build_title_with_keywords( + self, + app_name: str, + keywords: List[str], + max_length: int + ) -> Optional[str]: + """Build title combining app name and keywords within limit.""" + separators = [' - ', ': ', ' | '] + + for sep in separators: + for kw in keywords: + title = f"{app_name}{sep}{kw}" + if len(title) <= max_length: + return title + + return None + + def _optimize_short_description( + self, + app_info: Dict[str, Any], + target_keywords: List[str] + ) -> Dict[str, Any]: + """Optimize Google Play short description (80 chars).""" + max_length = self.limits['short_description'] + + # Focus on unique value proposition with primary keyword + unique_value = app_info.get('unique_value', '') + primary_keyword = target_keywords[0] if target_keywords else '' + + # Template: [Primary Keyword] - [Unique Value] + short_desc = f"{primary_keyword.title()} - {unique_value}"[:max_length] + + return { + 'short_description': short_desc, + 'length': len(short_desc), + 'remaining_chars': max_length - len(short_desc), + 'keywords_included': [primary_keyword] if primary_keyword in short_desc.lower() else [], + 'strategy': 'keyword_value_proposition' + } + + def _optimize_subtitle( + self, + app_info: Dict[str, Any], + target_keywords: List[str] + ) -> Dict[str, Any]: + """Optimize Apple App Store subtitle (30 chars).""" + max_length = self.limits['subtitle'] + + # Very concise - primary keyword or key feature + primary_keyword = target_keywords[0] if target_keywords else '' + key_feature = app_info.get('key_features', [''])[0] if app_info.get('key_features') else '' + + options = [ + primary_keyword[:max_length], + key_feature[:max_length], + f"{primary_keyword} App"[:max_length] + ] + + return { + 'subtitle_options': [opt for opt in options if opt], + 'max_length': max_length, + 'recommendation': options[0] if options else '' + } + + def _optimize_full_description( + self, + app_info: Dict[str, Any], + target_keywords: List[str] + ) -> Dict[str, Any]: + """Optimize full app description (4000 chars for both platforms).""" + max_length = self.limits.get('description', self.limits.get('full_description', 4000)) + + # Structure: Hook โ†’ Features โ†’ Benefits โ†’ Social Proof โ†’ CTA + sections = [] + + # Hook (with primary keyword) + primary_keyword = target_keywords[0] if target_keywords else '' + unique_value = app_info.get('unique_value', '') + hook = f"{unique_value} {primary_keyword.title()} that helps you achieve more.\n\n" + sections.append(hook) + + # Features (with keywords naturally integrated) + features = app_info.get('key_features', []) + if features: + sections.append("KEY FEATURES:\n") + for i, feature in enumerate(features[:5], 1): + # Integrate keywords naturally + feature_text = f"โ€ข {feature}" + if i <= len(target_keywords): + keyword = target_keywords[i-1] + if keyword.lower() not in feature.lower(): + feature_text = f"โ€ข {feature} with {keyword}" + sections.append(f"{feature_text}\n") + sections.append("\n") + + # Benefits + target_audience = app_info.get('target_audience', 'users') + sections.append(f"PERFECT FOR:\n{target_audience}\n\n") + + # Social proof placeholder + sections.append("WHY USERS LOVE US:\n") + sections.append("Join thousands of satisfied users who have transformed their workflow.\n\n") + + # CTA + sections.append("Download now and start experiencing the difference!") + + # Combine and validate length + full_description = "".join(sections) + if len(full_description) > max_length: + full_description = full_description[:max_length-3] + "..." + + # Calculate keyword density + density = self.calculate_keyword_density(full_description, target_keywords) + + return { + 'full_description': full_description, + 'length': len(full_description), + 'remaining_chars': max_length - len(full_description), + 'keyword_analysis': density, + 'structure': { + 'has_hook': True, + 'has_features': len(features) > 0, + 'has_benefits': True, + 'has_cta': True + } + } + + def _remove_plural_duplicates(self, keywords: List[str]) -> List[str]: + """Remove plural forms if singular exists.""" + deduplicated = [] + singular_set = set() + + for keyword in keywords: + if keyword.endswith('s') and len(keyword) > 1: + singular = keyword[:-1] + if singular not in singular_set: + deduplicated.append(singular) + singular_set.add(singular) + else: + if keyword not in singular_set: + deduplicated.append(keyword) + singular_set.add(keyword) + + return deduplicated + + def _build_keyword_field(self, keywords: List[str], max_length: int) -> str: + """Build comma-separated keyword field within character limit.""" + keyword_field = "" + + for keyword in keywords: + test_field = f"{keyword_field},{keyword}" if keyword_field else keyword + if len(test_field) <= max_length: + keyword_field = test_field + else: + break + + return keyword_field + + def _calculate_coverage(self, keywords: List[str], text: str) -> Dict[str, int]: + """Calculate how many keywords are covered in text.""" + text_lower = text.lower() + coverage = {} + + for keyword in keywords: + coverage[keyword] = text_lower.count(keyword.lower()) + + return coverage + + def _assess_density(self, density: float) -> str: + """Assess individual keyword density.""" + if density < 0.5: + return "too_low" + elif density <= 2.5: + return "optimal" + else: + return "too_high" + + def _assess_overall_density(self, density: float) -> str: + """Assess overall keyword density.""" + if density < 2: + return "Under-optimized: Consider adding more keyword variations" + elif density <= 5: + return "Optimal: Good keyword integration without stuffing" + elif density <= 8: + return "High: Approaching keyword stuffing - reduce keyword usage" + else: + return "Too High: Keyword stuffing detected - rewrite for natural flow" + + def _generate_density_recommendations( + self, + keyword_densities: Dict[str, Dict[str, Any]] + ) -> List[str]: + """Generate recommendations based on keyword density analysis.""" + recommendations = [] + + for keyword, data in keyword_densities.items(): + if data['status'] == 'too_low': + recommendations.append( + f"Increase usage of '{keyword}' - currently only {data['occurrences']} times" + ) + elif data['status'] == 'too_high': + recommendations.append( + f"Reduce usage of '{keyword}' - appears {data['occurrences']} times (keyword stuffing risk)" + ) + + if not recommendations: + recommendations.append("Keyword density is well-balanced") + + return recommendations + + def _recommend_title_option(self, options: List[Dict[str, Any]]) -> str: + """Recommend best title option based on strategy.""" + if not options: + return "No valid options available" + + # Prefer brand_plus_primary for established apps + for option in options: + if option['strategy'] == 'brand_plus_primary': + return f"Recommended: '{option['title']}' (Balance of brand and SEO)" + + # Fallback to first option + return f"Recommended: '{options[0]['title']}' ({options[0]['strategy']})" + + +def optimize_app_metadata( + platform: str, + app_info: Dict[str, Any], + target_keywords: List[str] +) -> Dict[str, Any]: + """ + Convenience function to optimize all metadata fields. + + Args: + platform: 'apple' or 'google' + app_info: App information dictionary + target_keywords: Target keywords list + + Returns: + Complete metadata optimization package + """ + optimizer = MetadataOptimizer(platform) + + return { + 'platform': platform, + 'title': optimizer.optimize_title( + app_info['name'], + target_keywords + ), + 'description': optimizer.optimize_description( + app_info, + target_keywords, + 'full' + ), + 'keyword_field': optimizer.optimize_keyword_field( + target_keywords + ) if platform == 'apple' else None + } diff --git a/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/review_analyzer.py b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/review_analyzer.py new file mode 100644 index 00000000..4ce124d5 --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/review_analyzer.py @@ -0,0 +1,714 @@ +""" +Review analysis module for App Store Optimization. +Analyzes user reviews for sentiment, issues, and feature requests. +""" + +from typing import Dict, List, Any, Optional, Tuple +from collections import Counter +import re + + +class ReviewAnalyzer: + """Analyzes user reviews for actionable insights.""" + + # Sentiment keywords + POSITIVE_KEYWORDS = [ + 'great', 'awesome', 'excellent', 'amazing', 'love', 'best', 'perfect', + 'fantastic', 'wonderful', 'brilliant', 'outstanding', 'superb' + ] + + NEGATIVE_KEYWORDS = [ + 'bad', 'terrible', 'awful', 'horrible', 'hate', 'worst', 'useless', + 'broken', 'crash', 'bug', 'slow', 'disappointing', 'frustrating' + ] + + # Issue indicators + ISSUE_KEYWORDS = [ + 'crash', 'bug', 'error', 'broken', 'not working', 'doesnt work', + 'freezes', 'slow', 'laggy', 'glitch', 'problem', 'issue', 'fail' + ] + + # Feature request indicators + FEATURE_REQUEST_KEYWORDS = [ + 'wish', 'would be nice', 'should add', 'need', 'want', 'hope', + 'please add', 'missing', 'lacks', 'feature request' + ] + + def __init__(self, app_name: str): + """ + Initialize review analyzer. + + Args: + app_name: Name of the app + """ + self.app_name = app_name + self.reviews = [] + self.analysis_cache = {} + + def analyze_sentiment( + self, + reviews: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Analyze sentiment across reviews. + + Args: + reviews: List of review dicts with 'text', 'rating', 'date' + + Returns: + Sentiment analysis summary + """ + self.reviews = reviews + + sentiment_counts = { + 'positive': 0, + 'neutral': 0, + 'negative': 0 + } + + detailed_sentiments = [] + + for review in reviews: + text = review.get('text', '').lower() + rating = review.get('rating', 3) + + # Calculate sentiment score + sentiment_score = self._calculate_sentiment_score(text, rating) + sentiment_category = self._categorize_sentiment(sentiment_score) + + sentiment_counts[sentiment_category] += 1 + + detailed_sentiments.append({ + 'review_id': review.get('id', ''), + 'rating': rating, + 'sentiment_score': sentiment_score, + 'sentiment': sentiment_category, + 'text_preview': text[:100] + '...' if len(text) > 100 else text + }) + + # Calculate percentages + total = len(reviews) + sentiment_distribution = { + 'positive': round((sentiment_counts['positive'] / total) * 100, 1) if total > 0 else 0, + 'neutral': round((sentiment_counts['neutral'] / total) * 100, 1) if total > 0 else 0, + 'negative': round((sentiment_counts['negative'] / total) * 100, 1) if total > 0 else 0 + } + + # Calculate average rating + avg_rating = sum(r.get('rating', 0) for r in reviews) / total if total > 0 else 0 + + return { + 'total_reviews_analyzed': total, + 'average_rating': round(avg_rating, 2), + 'sentiment_distribution': sentiment_distribution, + 'sentiment_counts': sentiment_counts, + 'sentiment_trend': self._assess_sentiment_trend(sentiment_distribution), + 'detailed_sentiments': detailed_sentiments[:50] # Limit output + } + + def extract_common_themes( + self, + reviews: List[Dict[str, Any]], + min_mentions: int = 3 + ) -> Dict[str, Any]: + """ + Extract frequently mentioned themes and topics. + + Args: + reviews: List of review dicts + min_mentions: Minimum mentions to be considered common + + Returns: + Common themes analysis + """ + # Extract all words from reviews + all_words = [] + all_phrases = [] + + for review in reviews: + text = review.get('text', '').lower() + # Clean text + text = re.sub(r'[^\w\s]', ' ', text) + words = text.split() + + # Filter out common words + stop_words = { + 'the', 'and', 'for', 'with', 'this', 'that', 'from', 'have', + 'app', 'apps', 'very', 'really', 'just', 'but', 'not', 'you' + } + words = [w for w in words if w not in stop_words and len(w) > 3] + + all_words.extend(words) + + # Extract 2-3 word phrases + for i in range(len(words) - 1): + phrase = f"{words[i]} {words[i+1]}" + all_phrases.append(phrase) + + # Count frequency + word_freq = Counter(all_words) + phrase_freq = Counter(all_phrases) + + # Filter by min_mentions + common_words = [ + {'word': word, 'mentions': count} + for word, count in word_freq.most_common(30) + if count >= min_mentions + ] + + common_phrases = [ + {'phrase': phrase, 'mentions': count} + for phrase, count in phrase_freq.most_common(20) + if count >= min_mentions + ] + + # Categorize themes + themes = self._categorize_themes(common_words, common_phrases) + + return { + 'common_words': common_words, + 'common_phrases': common_phrases, + 'identified_themes': themes, + 'insights': self._generate_theme_insights(themes) + } + + def identify_issues( + self, + reviews: List[Dict[str, Any]], + rating_threshold: int = 3 + ) -> Dict[str, Any]: + """ + Identify bugs, crashes, and other issues from reviews. + + Args: + reviews: List of review dicts + rating_threshold: Only analyze reviews at or below this rating + + Returns: + Issue identification report + """ + issues = [] + + for review in reviews: + rating = review.get('rating', 5) + if rating > rating_threshold: + continue + + text = review.get('text', '').lower() + + # Check for issue keywords + mentioned_issues = [] + for keyword in self.ISSUE_KEYWORDS: + if keyword in text: + mentioned_issues.append(keyword) + + if mentioned_issues: + issues.append({ + 'review_id': review.get('id', ''), + 'rating': rating, + 'date': review.get('date', ''), + 'issue_keywords': mentioned_issues, + 'text': text[:200] + '...' if len(text) > 200 else text + }) + + # Group by issue type + issue_frequency = Counter() + for issue in issues: + for keyword in issue['issue_keywords']: + issue_frequency[keyword] += 1 + + # Categorize issues + categorized_issues = self._categorize_issues(issues) + + # Calculate issue severity + severity_scores = self._calculate_issue_severity( + categorized_issues, + len(reviews) + ) + + return { + 'total_issues_found': len(issues), + 'issue_frequency': dict(issue_frequency.most_common(15)), + 'categorized_issues': categorized_issues, + 'severity_scores': severity_scores, + 'top_issues': self._rank_issues_by_severity(severity_scores), + 'recommendations': self._generate_issue_recommendations( + categorized_issues, + severity_scores + ) + } + + def find_feature_requests( + self, + reviews: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Extract feature requests and desired improvements. + + Args: + reviews: List of review dicts + + Returns: + Feature request analysis + """ + feature_requests = [] + + for review in reviews: + text = review.get('text', '').lower() + rating = review.get('rating', 3) + + # Check for feature request indicators + is_feature_request = any( + keyword in text + for keyword in self.FEATURE_REQUEST_KEYWORDS + ) + + if is_feature_request: + # Extract the specific request + request_text = self._extract_feature_request_text(text) + + feature_requests.append({ + 'review_id': review.get('id', ''), + 'rating': rating, + 'date': review.get('date', ''), + 'request_text': request_text, + 'full_review': text[:200] + '...' if len(text) > 200 else text + }) + + # Cluster similar requests + clustered_requests = self._cluster_feature_requests(feature_requests) + + # Prioritize based on frequency and rating context + prioritized_requests = self._prioritize_feature_requests(clustered_requests) + + return { + 'total_feature_requests': len(feature_requests), + 'clustered_requests': clustered_requests, + 'prioritized_requests': prioritized_requests, + 'implementation_recommendations': self._generate_feature_recommendations( + prioritized_requests + ) + } + + def track_sentiment_trends( + self, + reviews_by_period: Dict[str, List[Dict[str, Any]]] + ) -> Dict[str, Any]: + """ + Track sentiment changes over time. + + Args: + reviews_by_period: Dict of period_name: reviews + + Returns: + Trend analysis + """ + trends = [] + + for period, reviews in reviews_by_period.items(): + sentiment = self.analyze_sentiment(reviews) + + trends.append({ + 'period': period, + 'total_reviews': len(reviews), + 'average_rating': sentiment['average_rating'], + 'positive_percentage': sentiment['sentiment_distribution']['positive'], + 'negative_percentage': sentiment['sentiment_distribution']['negative'] + }) + + # Calculate trend direction + if len(trends) >= 2: + first_period = trends[0] + last_period = trends[-1] + + rating_change = last_period['average_rating'] - first_period['average_rating'] + sentiment_change = last_period['positive_percentage'] - first_period['positive_percentage'] + + trend_direction = self._determine_trend_direction( + rating_change, + sentiment_change + ) + else: + trend_direction = 'insufficient_data' + + return { + 'periods_analyzed': len(trends), + 'trend_data': trends, + 'trend_direction': trend_direction, + 'insights': self._generate_trend_insights(trends, trend_direction) + } + + def generate_response_templates( + self, + issue_category: str + ) -> List[Dict[str, str]]: + """ + Generate response templates for common review scenarios. + + Args: + issue_category: Category of issue ('crash', 'feature_request', 'positive', etc.) + + Returns: + Response templates + """ + templates = { + 'crash': [ + { + 'scenario': 'App crash reported', + 'template': "Thank you for bringing this to our attention. We're sorry you experienced a crash. " + "Our team is investigating this issue. Could you please share more details about when " + "this occurred (device model, iOS/Android version) by contacting support@[company].com? " + "We're committed to fixing this quickly." + }, + { + 'scenario': 'Crash already fixed', + 'template': "Thank you for your feedback. We've identified and fixed this crash issue in version [X.X]. " + "Please update to the latest version. If the problem persists, please reach out to " + "support@[company].com and we'll help you directly." + } + ], + 'bug': [ + { + 'scenario': 'Bug reported', + 'template': "Thanks for reporting this bug. We take these issues seriously. Our team is looking into it " + "and we'll have a fix in an upcoming update. We appreciate your patience and will notify you " + "when it's resolved." + } + ], + 'feature_request': [ + { + 'scenario': 'Feature request received', + 'template': "Thank you for this suggestion! We're always looking to improve [app_name]. We've added your " + "request to our roadmap and will consider it for a future update. Follow us @[social] for " + "updates on new features." + }, + { + 'scenario': 'Feature already planned', + 'template': "Great news! This feature is already on our roadmap and we're working on it. Stay tuned for " + "updates in the coming months. Thanks for your feedback!" + } + ], + 'positive': [ + { + 'scenario': 'Positive review', + 'template': "Thank you so much for your kind words! We're thrilled that you're enjoying [app_name]. " + "Reviews like yours motivate our team to keep improving. If you ever have suggestions, " + "we'd love to hear them!" + } + ], + 'negative_general': [ + { + 'scenario': 'General complaint', + 'template': "We're sorry to hear you're not satisfied with your experience. We'd like to make this right. " + "Please contact us at support@[company].com so we can understand the issue better and help " + "you directly. Thank you for giving us a chance to improve." + } + ] + } + + return templates.get(issue_category, templates['negative_general']) + + def _calculate_sentiment_score(self, text: str, rating: int) -> float: + """Calculate sentiment score (-1 to 1).""" + # Start with rating-based score + rating_score = (rating - 3) / 2 # Convert 1-5 to -1 to 1 + + # Adjust based on text sentiment + positive_count = sum(1 for keyword in self.POSITIVE_KEYWORDS if keyword in text) + negative_count = sum(1 for keyword in self.NEGATIVE_KEYWORDS if keyword in text) + + text_score = (positive_count - negative_count) / 10 # Normalize + + # Weighted average (60% rating, 40% text) + final_score = (rating_score * 0.6) + (text_score * 0.4) + + return max(min(final_score, 1.0), -1.0) + + def _categorize_sentiment(self, score: float) -> str: + """Categorize sentiment score.""" + if score > 0.3: + return 'positive' + elif score < -0.3: + return 'negative' + else: + return 'neutral' + + def _assess_sentiment_trend(self, distribution: Dict[str, float]) -> str: + """Assess overall sentiment trend.""" + positive = distribution['positive'] + negative = distribution['negative'] + + if positive > 70: + return 'very_positive' + elif positive > 50: + return 'positive' + elif negative > 30: + return 'concerning' + elif negative > 50: + return 'critical' + else: + return 'mixed' + + def _categorize_themes( + self, + common_words: List[Dict[str, Any]], + common_phrases: List[Dict[str, Any]] + ) -> Dict[str, List[str]]: + """Categorize themes from words and phrases.""" + themes = { + 'features': [], + 'performance': [], + 'usability': [], + 'support': [], + 'pricing': [] + } + + # Keywords for each category + feature_keywords = {'feature', 'functionality', 'option', 'tool'} + performance_keywords = {'fast', 'slow', 'crash', 'lag', 'speed', 'performance'} + usability_keywords = {'easy', 'difficult', 'intuitive', 'confusing', 'interface', 'design'} + support_keywords = {'support', 'help', 'customer', 'service', 'response'} + pricing_keywords = {'price', 'cost', 'expensive', 'cheap', 'subscription', 'free'} + + for word_data in common_words: + word = word_data['word'] + if any(kw in word for kw in feature_keywords): + themes['features'].append(word) + elif any(kw in word for kw in performance_keywords): + themes['performance'].append(word) + elif any(kw in word for kw in usability_keywords): + themes['usability'].append(word) + elif any(kw in word for kw in support_keywords): + themes['support'].append(word) + elif any(kw in word for kw in pricing_keywords): + themes['pricing'].append(word) + + return {k: v for k, v in themes.items() if v} # Remove empty categories + + def _generate_theme_insights(self, themes: Dict[str, List[str]]) -> List[str]: + """Generate insights from themes.""" + insights = [] + + for category, keywords in themes.items(): + if keywords: + insights.append( + f"{category.title()}: Users frequently mention {', '.join(keywords[:3])}" + ) + + return insights[:5] + + def _categorize_issues(self, issues: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]: + """Categorize issues by type.""" + categories = { + 'crashes': [], + 'bugs': [], + 'performance': [], + 'compatibility': [] + } + + for issue in issues: + keywords = issue['issue_keywords'] + + if 'crash' in keywords or 'freezes' in keywords: + categories['crashes'].append(issue) + elif 'bug' in keywords or 'error' in keywords or 'broken' in keywords: + categories['bugs'].append(issue) + elif 'slow' in keywords or 'laggy' in keywords: + categories['performance'].append(issue) + else: + categories['compatibility'].append(issue) + + return {k: v for k, v in categories.items() if v} + + def _calculate_issue_severity( + self, + categorized_issues: Dict[str, List[Dict[str, Any]]], + total_reviews: int + ) -> Dict[str, Dict[str, Any]]: + """Calculate severity scores for each issue category.""" + severity_scores = {} + + for category, issues in categorized_issues.items(): + count = len(issues) + percentage = (count / total_reviews) * 100 if total_reviews > 0 else 0 + + # Calculate average rating of affected reviews + avg_rating = sum(i['rating'] for i in issues) / count if count > 0 else 0 + + # Severity score (0-100) + severity = min((percentage * 10) + ((5 - avg_rating) * 10), 100) + + severity_scores[category] = { + 'count': count, + 'percentage': round(percentage, 2), + 'average_rating': round(avg_rating, 2), + 'severity_score': round(severity, 1), + 'priority': 'critical' if severity > 70 else ('high' if severity > 40 else 'medium') + } + + return severity_scores + + def _rank_issues_by_severity( + self, + severity_scores: Dict[str, Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """Rank issues by severity score.""" + ranked = sorted( + [{'category': cat, **data} for cat, data in severity_scores.items()], + key=lambda x: x['severity_score'], + reverse=True + ) + return ranked + + def _generate_issue_recommendations( + self, + categorized_issues: Dict[str, List[Dict[str, Any]]], + severity_scores: Dict[str, Dict[str, Any]] + ) -> List[str]: + """Generate recommendations for addressing issues.""" + recommendations = [] + + for category, score_data in severity_scores.items(): + if score_data['priority'] == 'critical': + recommendations.append( + f"URGENT: Address {category} issues immediately - affecting {score_data['percentage']}% of reviews" + ) + elif score_data['priority'] == 'high': + recommendations.append( + f"HIGH PRIORITY: Focus on {category} issues in next update" + ) + + return recommendations + + def _extract_feature_request_text(self, text: str) -> str: + """Extract the specific feature request from review text.""" + # Simple extraction - find sentence with feature request keywords + sentences = text.split('.') + for sentence in sentences: + if any(keyword in sentence for keyword in self.FEATURE_REQUEST_KEYWORDS): + return sentence.strip() + return text[:100] # Fallback + + def _cluster_feature_requests( + self, + feature_requests: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """Cluster similar feature requests.""" + # Simplified clustering - group by common keywords + clusters = {} + + for request in feature_requests: + text = request['request_text'].lower() + # Extract key words + words = [w for w in text.split() if len(w) > 4] + + # Try to find matching cluster + matched = False + for cluster_key in clusters: + if any(word in cluster_key for word in words[:3]): + clusters[cluster_key].append(request) + matched = True + break + + if not matched and words: + cluster_key = ' '.join(words[:2]) + clusters[cluster_key] = [request] + + return [ + {'feature_theme': theme, 'request_count': len(requests), 'examples': requests[:3]} + for theme, requests in clusters.items() + ] + + def _prioritize_feature_requests( + self, + clustered_requests: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """Prioritize feature requests by frequency.""" + return sorted( + clustered_requests, + key=lambda x: x['request_count'], + reverse=True + )[:10] + + def _generate_feature_recommendations( + self, + prioritized_requests: List[Dict[str, Any]] + ) -> List[str]: + """Generate recommendations for feature requests.""" + recommendations = [] + + if prioritized_requests: + top_request = prioritized_requests[0] + recommendations.append( + f"Most requested feature: {top_request['feature_theme']} " + f"({top_request['request_count']} mentions) - consider for next major release" + ) + + if len(prioritized_requests) > 1: + recommendations.append( + f"Also consider: {prioritized_requests[1]['feature_theme']}" + ) + + return recommendations + + def _determine_trend_direction( + self, + rating_change: float, + sentiment_change: float + ) -> str: + """Determine overall trend direction.""" + if rating_change > 0.2 and sentiment_change > 5: + return 'improving' + elif rating_change < -0.2 and sentiment_change < -5: + return 'declining' + else: + return 'stable' + + def _generate_trend_insights( + self, + trends: List[Dict[str, Any]], + trend_direction: str + ) -> List[str]: + """Generate insights from trend analysis.""" + insights = [] + + if trend_direction == 'improving': + insights.append("Positive trend: User satisfaction is increasing over time") + elif trend_direction == 'declining': + insights.append("WARNING: User satisfaction is declining - immediate action needed") + else: + insights.append("Sentiment is stable - maintain current quality") + + # Review velocity insight + if len(trends) >= 2: + recent_reviews = trends[-1]['total_reviews'] + previous_reviews = trends[-2]['total_reviews'] + + if recent_reviews > previous_reviews * 1.5: + insights.append("Review volume increasing - growing user base or recent controversy") + + return insights + + +def analyze_reviews( + app_name: str, + reviews: List[Dict[str, Any]] +) -> Dict[str, Any]: + """ + Convenience function to perform comprehensive review analysis. + + Args: + app_name: App name + reviews: List of review dictionaries + + Returns: + Complete review analysis + """ + analyzer = ReviewAnalyzer(app_name) + + return { + 'sentiment_analysis': analyzer.analyze_sentiment(reviews), + 'common_themes': analyzer.extract_common_themes(reviews), + 'issues_identified': analyzer.identify_issues(reviews), + 'feature_requests': analyzer.find_feature_requests(reviews) + } diff --git a/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/sample_input.json b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/sample_input.json new file mode 100644 index 00000000..5435a367 --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/app-store-optimization/sample_input.json @@ -0,0 +1,30 @@ +{ + "request_type": "keyword_research", + "app_info": { + "name": "TaskFlow Pro", + "category": "Productivity", + "target_audience": "Professionals aged 25-45 working in teams", + "key_features": [ + "AI-powered task prioritization", + "Team collaboration tools", + "Calendar integration", + "Cross-platform sync" + ], + "unique_value": "AI automatically prioritizes your tasks based on deadlines and importance" + }, + "target_keywords": [ + "task manager", + "productivity app", + "todo list", + "team collaboration", + "project management" + ], + "competitors": [ + "Todoist", + "Any.do", + "Microsoft To Do", + "Things 3" + ], + "platform": "both", + "language": "en-US" +} diff --git a/plugins/antigravity-bundle-expo-react-native/skills/expo-api-routes/SKILL.md b/plugins/antigravity-bundle-expo-react-native/skills/expo-api-routes/SKILL.md new file mode 100644 index 00000000..1d72340c --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/expo-api-routes/SKILL.md @@ -0,0 +1,370 @@ +--- +name: expo-api-routes +description: Guidelines for creating API routes in Expo Router with EAS Hosting +risk: unknown +source: community +version: 1.0.0 +license: MIT +--- + +## When to Use API Routes + +Use API routes when you need: + +- **Server-side secrets** โ€” API keys, database credentials, or tokens that must never reach the client +- **Database operations** โ€” Direct database queries that shouldn't be exposed +- **Third-party API proxies** โ€” Hide API keys when calling external services (OpenAI, Stripe, etc.) +- **Server-side validation** โ€” Validate data before database writes +- **Webhook endpoints** โ€” Receive callbacks from services like Stripe or GitHub +- **Rate limiting** โ€” Control access at the server level +- **Heavy computation** โ€” Offload processing that would be slow on mobile + +## When NOT to Use API Routes + +Avoid API routes when: + +- **Data is already public** โ€” Use direct fetch to public APIs instead +- **No secrets required** โ€” Static data or client-safe operations +- **Real-time updates needed** โ€” Use WebSockets or services like Supabase Realtime +- **Simple CRUD** โ€” Consider Firebase, Supabase, or Convex for managed backends +- **File uploads** โ€” Use direct-to-storage uploads (S3 presigned URLs, Cloudflare R2) +- **Authentication only** โ€” Use Clerk, Auth0, or Firebase Auth instead + +## File Structure + +API routes live in the `app` directory with `+api.ts` suffix: + +``` +app/ + api/ + hello+api.ts โ†’ GET /api/hello + users+api.ts โ†’ /api/users + users/[id]+api.ts โ†’ /api/users/:id + (tabs)/ + index.tsx +``` + +## Basic API Route + +```ts +// app/api/hello+api.ts +export function GET(request: Request) { + return Response.json({ message: "Hello from Expo!" }); +} +``` + +## HTTP Methods + +Export named functions for each HTTP method: + +```ts +// app/api/items+api.ts +export function GET(request: Request) { + return Response.json({ items: [] }); +} + +export async function POST(request: Request) { + const body = await request.json(); + return Response.json({ created: body }, { status: 201 }); +} + +export async function PUT(request: Request) { + const body = await request.json(); + return Response.json({ updated: body }); +} + +export async function DELETE(request: Request) { + return new Response(null, { status: 204 }); +} +``` + +## Dynamic Routes + +```ts +// app/api/users/[id]+api.ts +export function GET(request: Request, { id }: { id: string }) { + return Response.json({ userId: id }); +} +``` + +## Request Handling + +### Query Parameters + +```ts +export function GET(request: Request) { + const url = new URL(request.url); + const page = url.searchParams.get("page") ?? "1"; + const limit = url.searchParams.get("limit") ?? "10"; + + return Response.json({ page, limit }); +} +``` + +### Headers + +```ts +export function GET(request: Request) { + const auth = request.headers.get("Authorization"); + + if (!auth) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + return Response.json({ authenticated: true }); +} +``` + +### JSON Body + +```ts +export async function POST(request: Request) { + const { email, password } = await request.json(); + + if (!email || !password) { + return Response.json({ error: "Missing fields" }, { status: 400 }); + } + + return Response.json({ success: true }); +} +``` + +## Environment Variables + +Use `process.env` for server-side secrets: + +```ts +// app/api/ai+api.ts +export async function POST(request: Request) { + const { prompt } = await request.json(); + + const response = await fetch("https://api.openai.com/v1/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, + }, + body: JSON.stringify({ + model: "gpt-4", + messages: [{ role: "user", content: prompt }], + }), + }); + + const data = await response.json(); + return Response.json(data); +} +``` + +Set environment variables: + +- **Local**: Create `.env` file (never commit) +- **EAS Hosting**: Use `eas env:create` or Expo dashboard + +## CORS Headers + +Add CORS for web clients: + +```ts +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", +}; + +export function OPTIONS() { + return new Response(null, { headers: corsHeaders }); +} + +export function GET() { + return Response.json({ data: "value" }, { headers: corsHeaders }); +} +``` + +## Error Handling + +```ts +export async function POST(request: Request) { + try { + const body = await request.json(); + // Process... + return Response.json({ success: true }); + } catch (error) { + console.error("API error:", error); + return Response.json({ error: "Internal server error" }, { status: 500 }); + } +} +``` + +## Testing Locally + +Start the development server with API routes: + +```bash +npx expo serve +``` + +This starts a local server at `http://localhost:8081` with full API route support. + +Test with curl: + +```bash +curl http://localhost:8081/api/hello +curl -X POST http://localhost:8081/api/users -H "Content-Type: application/json" -d '{"name":"Test"}' +``` + +## Deployment to EAS Hosting + +### Prerequisites + +```bash +npm install -g eas-cli +eas login +``` + +### Deploy + +```bash +eas deploy +``` + +This builds and deploys your API routes to EAS Hosting (Cloudflare Workers). + +### Environment Variables for Production + +```bash +# Create a secret +eas env:create --name OPENAI_API_KEY --value sk-xxx --environment production + +# Or use the Expo dashboard +``` + +### Custom Domain + +Configure in `eas.json` or Expo dashboard. + +## EAS Hosting Runtime (Cloudflare Workers) + +API routes run on Cloudflare Workers. Key limitations: + +### Missing/Limited APIs + +- **No Node.js filesystem** โ€” `fs` module unavailable +- **No native Node modules** โ€” Use Web APIs or polyfills +- **Limited execution time** โ€” 30 second timeout for CPU-intensive tasks +- **No persistent connections** โ€” WebSockets require Durable Objects +- **fetch is available** โ€” Use standard fetch for HTTP requests + +### Use Web APIs Instead + +```ts +// Use Web Crypto instead of Node crypto +const hash = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode("data") +); + +// Use fetch instead of node-fetch +const response = await fetch("https://api.example.com"); + +// Use Response/Request (already available) +return new Response(JSON.stringify(data), { + headers: { "Content-Type": "application/json" }, +}); +``` + +### Database Options + +Since filesystem is unavailable, use cloud databases: + +- **Cloudflare D1** โ€” SQLite at the edge +- **Turso** โ€” Distributed SQLite +- **PlanetScale** โ€” Serverless MySQL +- **Supabase** โ€” Postgres with REST API +- **Neon** โ€” Serverless Postgres + +Example with Turso: + +```ts +// app/api/users+api.ts +import { createClient } from "@libsql/client/web"; + +const db = createClient({ + url: process.env.TURSO_URL!, + authToken: process.env.TURSO_AUTH_TOKEN!, +}); + +export async function GET() { + const result = await db.execute("SELECT * FROM users"); + return Response.json(result.rows); +} +``` + +## Calling API Routes from Client + +```ts +// From React Native components +const response = await fetch("/api/hello"); +const data = await response.json(); + +// With body +const response = await fetch("/api/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "John" }), +}); +``` + +## Common Patterns + +### Authentication Middleware + +```ts +// utils/auth.ts +export async function requireAuth(request: Request) { + const token = request.headers.get("Authorization")?.replace("Bearer ", ""); + + if (!token) { + throw new Response(JSON.stringify({ error: "Unauthorized" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } + + // Verify token... + return { userId: "123" }; +} + +// app/api/protected+api.ts +import { requireAuth } from "../../utils/auth"; + +export async function GET(request: Request) { + const { userId } = await requireAuth(request); + return Response.json({ userId }); +} +``` + +### Proxy External API + +```ts +// app/api/weather+api.ts +export async function GET(request: Request) { + const url = new URL(request.url); + const city = url.searchParams.get("city"); + + const response = await fetch( + `https://api.weather.com/v1/current?city=${city}&key=${process.env.WEATHER_API_KEY}` + ); + + return Response.json(await response.json()); +} +``` + +## Rules + +- NEVER expose API keys or secrets in client code +- ALWAYS validate and sanitize user input +- Use proper HTTP status codes (200, 201, 400, 401, 404, 500) +- Handle errors gracefully with try/catch +- Keep API routes focused โ€” one responsibility per endpoint +- Use TypeScript for type safety +- Log errors server-side for debugging diff --git a/plugins/antigravity-bundle-expo-react-native/skills/expo-cicd-workflows/SKILL.md b/plugins/antigravity-bundle-expo-react-native/skills/expo-cicd-workflows/SKILL.md new file mode 100644 index 00000000..34e11b8d --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/expo-cicd-workflows/SKILL.md @@ -0,0 +1,94 @@ +--- +name: expo-cicd-workflows +description: Helps understand and write EAS workflow YAML files for Expo projects. Use this skill when the user asks about CI/CD or workflows in an Expo or EAS context, mentions .eas/workflows/, or wants help with EAS build pipelines or deployment automation. +allowed-tools: "Read,Write,Bash(node:*)" +risk: unknown +source: community +version: 1.0.0 +license: MIT License +--- + +# EAS Workflows Skill + +Help developers write and edit EAS CI/CD workflow YAML files. + +## Reference Documentation + +Fetch these resources before generating or validating workflow files. Use the fetch script (implemented using Node.js) in this skill's `scripts/` directory; it caches responses using ETags for efficiency: + +```bash +# Fetch resources +node {baseDir}/scripts/fetch.js +``` + +1. **JSON Schema** โ€” https://api.expo.dev/v2/workflows/schema + - It is NECESSARY to fetch this schema + - Source of truth for validation + - All job types and their required/optional parameters + - Trigger types and configurations + - Runner types, VM images, and all enums + +2. **Syntax Documentation** โ€” https://raw.githubusercontent.com/expo/expo/refs/heads/main/docs/pages/eas/workflows/syntax.mdx + - Overview of workflow YAML syntax + - Examples and English explanations + - Expression syntax and contexts + +3. **Pre-packaged Jobs** โ€” https://raw.githubusercontent.com/expo/expo/refs/heads/main/docs/pages/eas/workflows/pre-packaged-jobs.mdx + - Documentation for supported pre-packaged job types + - Job-specific parameters and outputs + +Do not rely on memorized values; these resources evolve as new features are added. + +## Workflow File Location + +Workflows live in `.eas/workflows/*.yml` (or `.yaml`). + +## Top-Level Structure + +A workflow file has these top-level keys: + +- `name` โ€” Display name for the workflow +- `on` โ€” Triggers that start the workflow (at least one required) +- `jobs` โ€” Job definitions (required) +- `defaults` โ€” Shared defaults for all jobs +- `concurrency` โ€” Control parallel workflow runs + +Consult the schema for the full specification of each section. + +## Expressions + +Use `${{ }}` syntax for dynamic values. The schema defines available contexts: + +- `github.*` โ€” GitHub repository and event information +- `inputs.*` โ€” Values from `workflow_dispatch` inputs +- `needs.*` โ€” Outputs and status from dependent jobs +- `jobs.*` โ€” Job outputs (alternative syntax) +- `steps.*` โ€” Step outputs within custom jobs +- `workflow.*` โ€” Workflow metadata + +## Generating Workflows + +When generating or editing workflows: + +1. Fetch the schema to get current job types, parameters, and allowed values +2. Validate that required fields are present for each job type +3. Verify job references in `needs` and `after` exist in the workflow +4. Check that expressions reference valid contexts and outputs +5. Ensure `if` conditions respect the schema's length constraints + +## Validation + +After generating or editing a workflow file, validate it against the schema: + +```sh +# Install dependencies if missing +[ -d "{baseDir}/scripts/node_modules" ] || npm install --prefix {baseDir}/scripts + +node {baseDir}/scripts/validate.js [workflow2.yml ...] +``` + +The validator fetches the latest schema and checks the YAML structure. Fix any reported errors before considering the workflow complete. + +## Answering Questions + +When users ask about available options (job types, triggers, runner types, etc.), fetch the schema and derive the answer from it rather than relying on potentially outdated information. diff --git a/plugins/antigravity-bundle-expo-react-native/skills/expo-deployment/SKILL.md b/plugins/antigravity-bundle-expo-react-native/skills/expo-deployment/SKILL.md new file mode 100644 index 00000000..ff0269e1 --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/expo-deployment/SKILL.md @@ -0,0 +1,73 @@ +--- +name: expo-deployment +description: "Deploy Expo apps to production" +risk: safe +source: "https://github.com/expo/skills/tree/main/plugins/expo-deployment" +date_added: "2026-02-27" +--- + +# Expo Deployment + +## Overview + +Deploy Expo applications to production environments, including app stores and over-the-air updates. + +## When to Use This Skill + +Use this skill when you need to deploy Expo apps to production. + +Use this skill when: +- Deploying Expo apps to production +- Publishing to app stores (iOS App Store, Google Play) +- Setting up over-the-air (OTA) updates +- Configuring production build settings +- Managing release channels and versions + +## Instructions + +This skill provides guidance for deploying Expo apps: + +1. **Build Configuration**: Set up production build settings +2. **App Store Submission**: Prepare and submit to app stores +3. **OTA Updates**: Configure over-the-air update channels +4. **Release Management**: Manage versions and release channels +5. **Production Optimization**: Optimize apps for production + +## Deployment Workflow + +### Pre-Deployment + +1. Ensure all tests pass +2. Update version numbers +3. Configure production environment variables +4. Review and optimize app bundle size +5. Test production builds locally + +### App Store Deployment + +1. Build production binaries (iOS/Android) +2. Configure app store metadata +3. Submit to App Store Connect / Google Play Console +4. Manage app store listings and screenshots +5. Handle app review process + +### OTA Updates + +1. Configure update channels (production, staging, etc.) +2. Build and publish updates +3. Manage rollout strategies +4. Monitor update adoption +5. Handle rollbacks if needed + +## Best Practices + +- Use EAS Build for reliable production builds +- Test production builds before submission +- Implement proper error tracking and analytics +- Use release channels for staged rollouts +- Keep app store metadata up to date +- Monitor app performance in production + +## Resources + +For more information, see the [source repository](https://github.com/expo/skills/tree/main/plugins/expo-deployment). diff --git a/plugins/antigravity-bundle-expo-react-native/skills/expo-dev-client/SKILL.md b/plugins/antigravity-bundle-expo-react-native/skills/expo-dev-client/SKILL.md new file mode 100644 index 00000000..84f77ebd --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/expo-dev-client/SKILL.md @@ -0,0 +1,166 @@ +--- +name: expo-dev-client +description: Build and distribute Expo development clients locally or via TestFlight +risk: unknown +source: community +version: 1.0.0 +license: MIT +--- + +Use EAS Build to create development clients for testing native code changes on physical devices. Use this for creating custom Expo Go clients for testing branches of your app. + +## Important: When Development Clients Are Needed + +**Only create development clients when your app requires custom native code.** Most apps work fine in Expo Go. + +You need a dev client ONLY when using: +- Local Expo modules (custom native code) +- Apple targets (widgets, app clips, extensions) +- Third-party native modules not in Expo Go + +**Try Expo Go first** with `npx expo start`. If everything works, you don't need a dev client. + +## EAS Configuration + +Ensure `eas.json` has a development profile: + +```json +{ + "cli": { + "version": ">= 16.0.1", + "appVersionSource": "remote" + }, + "build": { + "production": { + "autoIncrement": true + }, + "development": { + "autoIncrement": true, + "developmentClient": true + } + }, + "submit": { + "production": {}, + "development": {} + } +} +``` + +Key settings: +- `developmentClient: true` - Bundles expo-dev-client for development builds +- `autoIncrement: true` - Automatically increments build numbers +- `appVersionSource: "remote"` - Uses EAS as the source of truth for version numbers + +## Building for TestFlight + +Build iOS dev client and submit to TestFlight in one command: + +```bash +eas build -p ios --profile development --submit +``` + +This will: +1. Build the development client in the cloud +2. Automatically submit to App Store Connect +3. Send you an email when the build is ready in TestFlight + +After receiving the TestFlight email: +1. Download the build from TestFlight on your device +2. Launch the app to see the expo-dev-client UI +3. Connect to your local Metro bundler or scan a QR code + +## Building Locally + +Build a development client on your machine: + +```bash +# iOS (requires Xcode) +eas build -p ios --profile development --local + +# Android +eas build -p android --profile development --local +``` + +Local builds output: +- iOS: `.ipa` file +- Android: `.apk` or `.aab` file + +## Installing Local Builds + +Install iOS build on simulator: + +```bash +# Find the .app in the .tar.gz output +tar -xzf build-*.tar.gz +xcrun simctl install booted ./path/to/App.app +``` + +Install iOS build on device (requires signing): + +```bash +# Use Xcode Devices window or ideviceinstaller +ideviceinstaller -i build.ipa +``` + +Install Android build: + +```bash +adb install build.apk +``` + +## Building for Specific Platform + +```bash +# iOS only +eas build -p ios --profile development + +# Android only +eas build -p android --profile development + +# Both platforms +eas build --profile development +``` + +## Checking Build Status + +```bash +# List recent builds +eas build:list + +# View build details +eas build:view +``` + +## Using the Dev Client + +Once installed, the dev client provides: +- **Development server connection** - Enter your Metro bundler URL or scan QR +- **Build information** - View native build details +- **Launcher UI** - Switch between development servers + +Connect to local development: + +```bash +# Start Metro bundler +npx expo start --dev-client + +# Scan QR code with dev client or enter URL manually +``` + +## Troubleshooting + +**Build fails with signing errors:** +```bash +eas credentials +``` + +**Clear build cache:** +```bash +eas build -p ios --profile development --clear-cache +``` + +**Check EAS CLI version:** +```bash +eas --version +eas update +``` diff --git a/plugins/antigravity-bundle-expo-react-native/skills/expo-tailwind-setup/SKILL.md b/plugins/antigravity-bundle-expo-react-native/skills/expo-tailwind-setup/SKILL.md new file mode 100644 index 00000000..a50e10a5 --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/expo-tailwind-setup/SKILL.md @@ -0,0 +1,482 @@ +--- +name: expo-tailwind-setup +description: Set up Tailwind CSS v4 in Expo with react-native-css and NativeWind v5 for universal styling +risk: unknown +source: community +version: 1.0.0 +license: MIT +--- + +# Tailwind CSS Setup for Expo with react-native-css + +This guide covers setting up Tailwind CSS v4 in Expo using react-native-css and NativeWind v5 for universal styling across iOS, Android, and Web. + +## Overview + +This setup uses: + +- **Tailwind CSS v4** - Modern CSS-first configuration +- **react-native-css** - CSS runtime for React Native +- **NativeWind v5** - Metro transformer for Tailwind in React Native +- **@tailwindcss/postcss** - PostCSS plugin for Tailwind v4 + +## Installation + +```bash +# Install dependencies +npx expo install tailwindcss@^4 nativewind@5.0.0-preview.2 react-native-css@0.0.0-nightly.5ce6396 @tailwindcss/postcss tailwind-merge clsx +``` + +Add resolutions for lightningcss compatibility: + +```json +// package.json +{ + "resolutions": { + "lightningcss": "1.30.1" + } +} +``` + +- autoprefixer is not needed in Expo because of lightningcss +- postcss is included in expo by default + +## Configuration Files + +### Metro Config + +Create or update `metro.config.js`: + +```js +// metro.config.js +const { getDefaultConfig } = require("expo/metro-config"); +const { withNativewind } = require("nativewind/metro"); + +/** @type {import('expo/metro-config').MetroConfig} */ +const config = getDefaultConfig(__dirname); + +module.exports = withNativewind(config, { + // inline variables break PlatformColor in CSS variables + inlineVariables: false, + // We add className support manually + globalClassNamePolyfill: false, +}); +``` + +### PostCSS Config + +Create `postcss.config.mjs`: + +```js +// postcss.config.mjs +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; +``` + +### Global CSS + +Create `src/global.css`: + +```css +@import "tailwindcss/theme.css" layer(theme); +@import "tailwindcss/preflight.css" layer(base); +@import "tailwindcss/utilities.css"; + +/* Platform-specific font families */ +@media android { + :root { + --font-mono: monospace; + --font-rounded: normal; + --font-serif: serif; + --font-sans: normal; + } +} + +@media ios { + :root { + --font-mono: ui-monospace; + --font-serif: ui-serif; + --font-sans: system-ui; + --font-rounded: ui-rounded; + } +} +``` + +## IMPORTANT: No Babel Config Needed + +With Tailwind v4 and NativeWind v5, you do NOT need a babel.config.js for Tailwind. Remove any NativeWind babel presets if present: + +```js +// DELETE babel.config.js if it only contains NativeWind config +// The following is NO LONGER needed: +// module.exports = function (api) { +// api.cache(true); +// return { +// presets: [ +// ["babel-preset-expo", { jsxImportSource: "nativewind" }], +// "nativewind/babel", +// ], +// }; +// }; +``` + +## CSS Component Wrappers + +Since react-native-css requires explicit CSS element wrapping, create reusable components: + +### Main Components (`src/tw/index.tsx`) + +```tsx +import { + useCssElement, + useNativeVariable as useFunctionalVariable, +} from "react-native-css"; + +import { Link as RouterLink } from "expo-router"; +import Animated from "react-native-reanimated"; +import React from "react"; +import { + View as RNView, + Text as RNText, + Pressable as RNPressable, + ScrollView as RNScrollView, + TouchableHighlight as RNTouchableHighlight, + TextInput as RNTextInput, + StyleSheet, +} from "react-native"; + +// CSS-enabled Link +export const Link = ( + props: React.ComponentProps & { className?: string } +) => { + return useCssElement(RouterLink, props, { className: "style" }); +}; + +Link.Trigger = RouterLink.Trigger; +Link.Menu = RouterLink.Menu; +Link.MenuAction = RouterLink.MenuAction; +Link.Preview = RouterLink.Preview; + +// CSS Variable hook +export const useCSSVariable = + process.env.EXPO_OS !== "web" + ? useFunctionalVariable + : (variable: string) => `var(${variable})`; + +// View +export type ViewProps = React.ComponentProps & { + className?: string; +}; + +export const View = (props: ViewProps) => { + return useCssElement(RNView, props, { className: "style" }); +}; +View.displayName = "CSS(View)"; + +// Text +export const Text = ( + props: React.ComponentProps & { className?: string } +) => { + return useCssElement(RNText, props, { className: "style" }); +}; +Text.displayName = "CSS(Text)"; + +// ScrollView +export const ScrollView = ( + props: React.ComponentProps & { + className?: string; + contentContainerClassName?: string; + } +) => { + return useCssElement(RNScrollView, props, { + className: "style", + contentContainerClassName: "contentContainerStyle", + }); +}; +ScrollView.displayName = "CSS(ScrollView)"; + +// Pressable +export const Pressable = ( + props: React.ComponentProps & { className?: string } +) => { + return useCssElement(RNPressable, props, { className: "style" }); +}; +Pressable.displayName = "CSS(Pressable)"; + +// TextInput +export const TextInput = ( + props: React.ComponentProps & { className?: string } +) => { + return useCssElement(RNTextInput, props, { className: "style" }); +}; +TextInput.displayName = "CSS(TextInput)"; + +// AnimatedScrollView +export const AnimatedScrollView = ( + props: React.ComponentProps & { + className?: string; + contentClassName?: string; + contentContainerClassName?: string; + } +) => { + return useCssElement(Animated.ScrollView, props, { + className: "style", + contentClassName: "contentContainerStyle", + contentContainerClassName: "contentContainerStyle", + }); +}; + +// TouchableHighlight with underlayColor extraction +function XXTouchableHighlight( + props: React.ComponentProps +) { + const { underlayColor, ...style } = StyleSheet.flatten(props.style) || {}; + return ( + + ); +} + +export const TouchableHighlight = ( + props: React.ComponentProps +) => { + return useCssElement(XXTouchableHighlight, props, { className: "style" }); +}; +TouchableHighlight.displayName = "CSS(TouchableHighlight)"; +``` + +### Image Component (`src/tw/image.tsx`) + +```tsx +import { useCssElement } from "react-native-css"; +import React from "react"; +import { StyleSheet } from "react-native"; +import Animated from "react-native-reanimated"; +import { Image as RNImage } from "expo-image"; + +const AnimatedExpoImage = Animated.createAnimatedComponent(RNImage); + +export type ImageProps = React.ComponentProps; + +function CSSImage(props: React.ComponentProps) { + // @ts-expect-error: Remap objectFit style to contentFit property + const { objectFit, objectPosition, ...style } = + StyleSheet.flatten(props.style) || {}; + + return ( + + ); +} + +export const Image = ( + props: React.ComponentProps & { className?: string } +) => { + return useCssElement(CSSImage, props, { className: "style" }); +}; + +Image.displayName = "CSS(Image)"; +``` + +### Animated Components (`src/tw/animated.tsx`) + +```tsx +import * as TW from "./index"; +import RNAnimated from "react-native-reanimated"; + +export const Animated = { + ...RNAnimated, + View: RNAnimated.createAnimatedComponent(TW.View), +}; +``` + +## Usage + +Import CSS-wrapped components from your tw directory: + +```tsx +import { View, Text, ScrollView, Image } from "@/tw"; + +export default function MyScreen() { + return ( + + + Hello Tailwind! + + + + ); +} +``` + +## Custom Theme Variables + +Add custom theme variables in your global.css using `@theme`: + +```css +@layer theme { + @theme { + /* Custom fonts */ + --font-rounded: "SF Pro Rounded", sans-serif; + + /* Custom line heights */ + --text-xs--line-height: calc(1em / 0.75); + --text-sm--line-height: calc(1.25em / 0.875); + --text-base--line-height: calc(1.5em / 1); + + /* Custom leading scales */ + --leading-tight: 1.25em; + --leading-snug: 1.375em; + --leading-normal: 1.5em; + } +} +``` + +## Platform-Specific Styles + +Use platform media queries for platform-specific styling: + +```css +@media ios { + :root { + --font-sans: system-ui; + --font-rounded: ui-rounded; + } +} + +@media android { + :root { + --font-sans: normal; + --font-rounded: normal; + } +} +``` + +## Apple System Colors with CSS Variables + +Create a CSS file for Apple semantic colors: + +```css +/* src/css/sf.css */ +@layer base { + html { + color-scheme: light; + } +} + +:root { + /* Accent colors with light/dark mode */ + --sf-blue: light-dark(rgb(0 122 255), rgb(10 132 255)); + --sf-green: light-dark(rgb(52 199 89), rgb(48 209 89)); + --sf-red: light-dark(rgb(255 59 48), rgb(255 69 58)); + + /* Gray scales */ + --sf-gray: light-dark(rgb(142 142 147), rgb(142 142 147)); + --sf-gray-2: light-dark(rgb(174 174 178), rgb(99 99 102)); + + /* Text colors */ + --sf-text: light-dark(rgb(0 0 0), rgb(255 255 255)); + --sf-text-2: light-dark(rgb(60 60 67 / 0.6), rgb(235 235 245 / 0.6)); + + /* Background colors */ + --sf-bg: light-dark(rgb(255 255 255), rgb(0 0 0)); + --sf-bg-2: light-dark(rgb(242 242 247), rgb(28 28 30)); +} + +/* iOS native colors via platformColor */ +@media ios { + :root { + --sf-blue: platformColor(systemBlue); + --sf-green: platformColor(systemGreen); + --sf-red: platformColor(systemRed); + --sf-gray: platformColor(systemGray); + --sf-text: platformColor(label); + --sf-text-2: platformColor(secondaryLabel); + --sf-bg: platformColor(systemBackground); + --sf-bg-2: platformColor(secondarySystemBackground); + } +} + +/* Register as Tailwind theme colors */ +@layer theme { + @theme { + --color-sf-blue: var(--sf-blue); + --color-sf-green: var(--sf-green); + --color-sf-red: var(--sf-red); + --color-sf-gray: var(--sf-gray); + --color-sf-text: var(--sf-text); + --color-sf-text-2: var(--sf-text-2); + --color-sf-bg: var(--sf-bg); + --color-sf-bg-2: var(--sf-bg-2); + } +} +``` + +Then use in components: + +```tsx +Primary text +Secondary text +... +``` + +## Using CSS Variables in JavaScript + +Use the `useCSSVariable` hook: + +```tsx +import { useCSSVariable } from "@/tw"; + +function MyComponent() { + const blue = useCSSVariable("--sf-blue"); + + return ; +} +``` + +## Key Differences from NativeWind v4 / Tailwind v3 + +1. **No babel.config.js** - Configuration is now CSS-first +2. **PostCSS plugin** - Uses `@tailwindcss/postcss` instead of `tailwindcss` +3. **CSS imports** - Use `@import "tailwindcss/..."` instead of `@tailwind` directives +4. **Theme config** - Use `@theme` in CSS instead of `tailwind.config.js` +5. **Component wrappers** - Must wrap components with `useCssElement` for className support +6. **Metro config** - Use `withNativewind` with different options (`inlineVariables: false`) + +## Troubleshooting + +### Styles not applying + +1. Ensure you have the CSS file imported in your app entry +2. Check that components are wrapped with `useCssElement` +3. Verify Metro config has `withNativewind` applied + +### Platform colors not working + +1. Use `platformColor()` in `@media ios` blocks +2. Fall back to `light-dark()` for web/Android + +### TypeScript errors + +Add className to component props: + +```tsx +type Props = React.ComponentProps & { className?: string }; +``` diff --git a/plugins/antigravity-bundle-expo-react-native/skills/react-native-architecture/SKILL.md b/plugins/antigravity-bundle-expo-react-native/skills/react-native-architecture/SKILL.md new file mode 100644 index 00000000..b65573ce --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/react-native-architecture/SKILL.md @@ -0,0 +1,36 @@ +--- +name: react-native-architecture +description: "Production-ready patterns for React Native development with Expo, including navigation, state management, native modules, and offline-first architecture." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# React Native Architecture + +Production-ready patterns for React Native development with Expo, including navigation, state management, native modules, and offline-first architecture. + +## Use this skill when + +- Starting a new React Native or Expo project +- Implementing complex navigation patterns +- Integrating native modules and platform APIs +- Building offline-first mobile applications +- Optimizing React Native performance +- Setting up CI/CD for mobile releases + +## Do not use this skill when + +- The task is unrelated to react native architecture +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Resources + +- `resources/implementation-playbook.md` for detailed patterns and examples. diff --git a/plugins/antigravity-bundle-expo-react-native/skills/react-native-architecture/resources/implementation-playbook.md b/plugins/antigravity-bundle-expo-react-native/skills/react-native-architecture/resources/implementation-playbook.md new file mode 100644 index 00000000..51a5369d --- /dev/null +++ b/plugins/antigravity-bundle-expo-react-native/skills/react-native-architecture/resources/implementation-playbook.md @@ -0,0 +1,670 @@ +# React Native Architecture Implementation Playbook + +This file contains detailed patterns, checklists, and code samples referenced by the skill. + +# React Native Architecture + +Production-ready patterns for React Native development with Expo, including navigation, state management, native modules, and offline-first architecture. + +## When to Use This Skill + +- Starting a new React Native or Expo project +- Implementing complex navigation patterns +- Integrating native modules and platform APIs +- Building offline-first mobile applications +- Optimizing React Native performance +- Setting up CI/CD for mobile releases + +## Core Concepts + +### 1. Project Structure + +``` +src/ +โ”œโ”€โ”€ app/ # Expo Router screens +โ”‚ โ”œโ”€โ”€ (auth)/ # Auth group +โ”‚ โ”œโ”€โ”€ (tabs)/ # Tab navigation +โ”‚ โ””โ”€โ”€ _layout.tsx # Root layout +โ”œโ”€โ”€ components/ +โ”‚ โ”œโ”€โ”€ ui/ # Reusable UI components +โ”‚ โ””โ”€โ”€ features/ # Feature-specific components +โ”œโ”€โ”€ hooks/ # Custom hooks +โ”œโ”€โ”€ services/ # API and native services +โ”œโ”€โ”€ stores/ # State management +โ”œโ”€โ”€ utils/ # Utilities +โ””โ”€โ”€ types/ # TypeScript types +``` + +### 2. Expo vs Bare React Native + +| Feature | Expo | Bare RN | +|---------|------|---------| +| Setup complexity | Low | High | +| Native modules | EAS Build | Manual linking | +| OTA updates | Built-in | Manual setup | +| Build service | EAS | Custom CI | +| Custom native code | Config plugins | Direct access | + +## Quick Start + +```bash +# Create new Expo project +npx create-expo-app@latest my-app -t expo-template-blank-typescript + +# Install essential dependencies +npx expo install expo-router expo-status-bar react-native-safe-area-context +npx expo install @react-native-async-storage/async-storage +npx expo install expo-secure-store expo-haptics +``` + +```typescript +// app/_layout.tsx +import { Stack } from 'expo-router' +import { ThemeProvider } from '@/providers/ThemeProvider' +import { QueryProvider } from '@/providers/QueryProvider' + +export default function RootLayout() { + return ( + + + + + + + + + + ) +} +``` + +## Patterns + +### Pattern 1: Expo Router Navigation + +```typescript +// app/(tabs)/_layout.tsx +import { Tabs } from 'expo-router' +import { Home, Search, User, Settings } from 'lucide-react-native' +import { useTheme } from '@/hooks/useTheme' + +export default function TabLayout() { + const { colors } = useTheme() + + return ( + + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + ) +} + +// app/(tabs)/profile/[id].tsx - Dynamic route +import { useLocalSearchParams } from 'expo-router' + +export default function ProfileScreen() { + const { id } = useLocalSearchParams<{ id: string }>() + + return +} + +// Navigation from anywhere +import { router } from 'expo-router' + +// Programmatic navigation +router.push('/profile/123') +router.replace('/login') +router.back() + +// With params +router.push({ + pathname: '/product/[id]', + params: { id: '123', referrer: 'home' }, +}) +``` + +### Pattern 2: Authentication Flow + +```typescript +// providers/AuthProvider.tsx +import { createContext, useContext, useEffect, useState } from 'react' +import { useRouter, useSegments } from 'expo-router' +import * as SecureStore from 'expo-secure-store' + +interface AuthContextType { + user: User | null + isLoading: boolean + signIn: (credentials: Credentials) => Promise + signOut: () => Promise +} + +const AuthContext = createContext(null) + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const segments = useSegments() + const router = useRouter() + + // Check authentication on mount + useEffect(() => { + checkAuth() + }, []) + + // Protect routes + useEffect(() => { + if (isLoading) return + + const inAuthGroup = segments[0] === '(auth)' + + if (!user && !inAuthGroup) { + router.replace('/login') + } else if (user && inAuthGroup) { + router.replace('/(tabs)') + } + }, [user, segments, isLoading]) + + async function checkAuth() { + try { + const token = await SecureStore.getItemAsync('authToken') + if (token) { + const userData = await api.getUser(token) + setUser(userData) + } + } catch (error) { + await SecureStore.deleteItemAsync('authToken') + } finally { + setIsLoading(false) + } + } + + async function signIn(credentials: Credentials) { + const { token, user } = await api.login(credentials) + await SecureStore.setItemAsync('authToken', token) + setUser(user) + } + + async function signOut() { + await SecureStore.deleteItemAsync('authToken') + setUser(null) + } + + if (isLoading) { + return + } + + return ( + + {children} + + ) +} + +export const useAuth = () => { + const context = useContext(AuthContext) + if (!context) throw new Error('useAuth must be used within AuthProvider') + return context +} +``` + +### Pattern 3: Offline-First with React Query + +```typescript +// providers/QueryProvider.tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' +import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' +import AsyncStorage from '@react-native-async-storage/async-storage' +import NetInfo from '@react-native-community/netinfo' +import { onlineManager } from '@tanstack/react-query' + +// Sync online status +onlineManager.setEventListener((setOnline) => { + return NetInfo.addEventListener((state) => { + setOnline(!!state.isConnected) + }) +}) + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: 1000 * 60 * 60 * 24, // 24 hours + staleTime: 1000 * 60 * 5, // 5 minutes + retry: 2, + networkMode: 'offlineFirst', + }, + mutations: { + networkMode: 'offlineFirst', + }, + }, +}) + +const asyncStoragePersister = createAsyncStoragePersister({ + storage: AsyncStorage, + key: 'REACT_QUERY_OFFLINE_CACHE', +}) + +export function QueryProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} + +// hooks/useProducts.ts +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' + +export function useProducts() { + return useQuery({ + queryKey: ['products'], + queryFn: api.getProducts, + // Use stale data while revalidating + placeholderData: (previousData) => previousData, + }) +} + +export function useCreateProduct() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: api.createProduct, + // Optimistic update + onMutate: async (newProduct) => { + await queryClient.cancelQueries({ queryKey: ['products'] }) + const previous = queryClient.getQueryData(['products']) + + queryClient.setQueryData(['products'], (old: Product[]) => [ + ...old, + { ...newProduct, id: 'temp-' + Date.now() }, + ]) + + return { previous } + }, + onError: (err, newProduct, context) => { + queryClient.setQueryData(['products'], context?.previous) + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['products'] }) + }, + }) +} +``` + +### Pattern 4: Native Module Integration + +```typescript +// services/haptics.ts +import * as Haptics from 'expo-haptics' +import { Platform } from 'react-native' + +export const haptics = { + light: () => { + if (Platform.OS !== 'web') { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) + } + }, + medium: () => { + if (Platform.OS !== 'web') { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium) + } + }, + heavy: () => { + if (Platform.OS !== 'web') { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy) + } + }, + success: () => { + if (Platform.OS !== 'web') { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success) + } + }, + error: () => { + if (Platform.OS !== 'web') { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error) + } + }, +} + +// services/biometrics.ts +import * as LocalAuthentication from 'expo-local-authentication' + +export async function authenticateWithBiometrics(): Promise { + const hasHardware = await LocalAuthentication.hasHardwareAsync() + if (!hasHardware) return false + + const isEnrolled = await LocalAuthentication.isEnrolledAsync() + if (!isEnrolled) return false + + const result = await LocalAuthentication.authenticateAsync({ + promptMessage: 'Authenticate to continue', + fallbackLabel: 'Use passcode', + disableDeviceFallback: false, + }) + + return result.success +} + +// services/notifications.ts +import * as Notifications from 'expo-notifications' +import { Platform } from 'react-native' +import Constants from 'expo-constants' + +Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldPlaySound: true, + shouldSetBadge: true, + }), +}) + +export async function registerForPushNotifications() { + let token: string | undefined + + if (Platform.OS === 'android') { + await Notifications.setNotificationChannelAsync('default', { + name: 'default', + importance: Notifications.AndroidImportance.MAX, + vibrationPattern: [0, 250, 250, 250], + }) + } + + const { status: existingStatus } = await Notifications.getPermissionsAsync() + let finalStatus = existingStatus + + if (existingStatus !== 'granted') { + const { status } = await Notifications.requestPermissionsAsync() + finalStatus = status + } + + if (finalStatus !== 'granted') { + return null + } + + const projectId = Constants.expoConfig?.extra?.eas?.projectId + token = (await Notifications.getExpoPushTokenAsync({ projectId })).data + + return token +} +``` + +### Pattern 5: Platform-Specific Code + +```typescript +// components/ui/Button.tsx +import { Platform, Pressable, StyleSheet, Text, ViewStyle } from 'react-native' +import * as Haptics from 'expo-haptics' +import Animated, { + useAnimatedStyle, + useSharedValue, + withSpring, +} from 'react-native-reanimated' + +const AnimatedPressable = Animated.createAnimatedComponent(Pressable) + +interface ButtonProps { + title: string + onPress: () => void + variant?: 'primary' | 'secondary' | 'outline' + disabled?: boolean +} + +export function Button({ + title, + onPress, + variant = 'primary', + disabled = false, +}: ButtonProps) { + const scale = useSharedValue(1) + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })) + + const handlePressIn = () => { + scale.value = withSpring(0.95) + if (Platform.OS !== 'web') { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) + } + } + + const handlePressOut = () => { + scale.value = withSpring(1) + } + + return ( + + {title} + + ) +} + +// Platform-specific files +// Button.ios.tsx - iOS-specific implementation +// Button.android.tsx - Android-specific implementation +// Button.web.tsx - Web-specific implementation + +// Or use Platform.select +const styles = StyleSheet.create({ + button: { + paddingVertical: 12, + paddingHorizontal: 24, + borderRadius: 8, + alignItems: 'center', + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + }, + android: { + elevation: 4, + }, + }), + }, + primary: { + backgroundColor: '#007AFF', + }, + secondary: { + backgroundColor: '#5856D6', + }, + outline: { + backgroundColor: 'transparent', + borderWidth: 1, + borderColor: '#007AFF', + }, + disabled: { + opacity: 0.5, + }, + text: { + fontSize: 16, + fontWeight: '600', + }, + primaryText: { + color: '#FFFFFF', + }, + secondaryText: { + color: '#FFFFFF', + }, + outlineText: { + color: '#007AFF', + }, +}) +``` + +### Pattern 6: Performance Optimization + +```typescript +// components/ProductList.tsx +import { FlashList } from '@shopify/flash-list' +import { memo, useCallback } from 'react' + +interface ProductListProps { + products: Product[] + onProductPress: (id: string) => void +} + +// Memoize list item +const ProductItem = memo(function ProductItem({ + item, + onPress, +}: { + item: Product + onPress: (id: string) => void +}) { + const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]) + + return ( + + + {item.name} + ${item.price} + + ) +}) + +export function ProductList({ products, onProductPress }: ProductListProps) { + const renderItem = useCallback( + ({ item }: { item: Product }) => ( + + ), + [onProductPress] + ) + + const keyExtractor = useCallback((item: Product) => item.id, []) + + return ( + + ) +} +``` + +## EAS Build & Submit + +```json +// eas.json +{ + "cli": { "version": ">= 5.0.0" }, + "build": { + "development": { + "developmentClient": true, + "distribution": "internal", + "ios": { "simulator": true } + }, + "preview": { + "distribution": "internal", + "android": { "buildType": "apk" } + }, + "production": { + "autoIncrement": true + } + }, + "submit": { + "production": { + "ios": { "appleId": "your@email.com", "ascAppId": "123456789" }, + "android": { "serviceAccountKeyPath": "./google-services.json" } + } + } +} +``` + +```bash +# Build commands +eas build --platform ios --profile development +eas build --platform android --profile preview +eas build --platform all --profile production + +# Submit to stores +eas submit --platform ios +eas submit --platform android + +# OTA updates +eas update --branch production --message "Bug fixes" +``` + +## Best Practices + +### Do's +- **Use Expo** - Faster development, OTA updates, managed native code +- **FlashList over FlatList** - Better performance for long lists +- **Memoize components** - Prevent unnecessary re-renders +- **Use Reanimated** - 60fps animations on native thread +- **Test on real devices** - Simulators miss real-world issues + +### Don'ts +- **Don't inline styles** - Use StyleSheet.create for performance +- **Don't fetch in render** - Use useEffect or React Query +- **Don't ignore platform differences** - Test on both iOS and Android +- **Don't store secrets in code** - Use environment variables +- **Don't skip error boundaries** - Mobile crashes are unforgiving + +## Resources + +- [Expo Documentation](https://docs.expo.dev/) +- [Expo Router](https://docs.expo.dev/router/introduction/) +- [React Native Performance](https://reactnative.dev/docs/performance) +- [FlashList](https://shopify.github.io/flash-list/) diff --git a/plugins/antigravity-bundle-full-stack-developer/.codex-plugin/plugin.json b/plugins/antigravity-bundle-full-stack-developer/.codex-plugin/plugin.json new file mode 100644 index 00000000..812074b4 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-full-stack-developer", + "version": "8.10.0", + "description": "Install the \"Full-Stack Developer\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "full-stack-developer", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Full-Stack Developer", + "shortDescription": "Web Development ยท 6 curated skills", + "longDescription": "For end-to-end web application development. Covers Senior Fullstack, Frontend Developer, and 4 more skills.", + "developerName": "sickn33 and contributors", + "category": "Web Development", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/SKILL.md b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/SKILL.md new file mode 100644 index 00000000..75fc5d27 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/SKILL.md @@ -0,0 +1,85 @@ +--- +name: api-patterns +description: "API design principles and decision-making. REST vs GraphQL vs tRPC selection, response formats, versioning, pagination." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# API Patterns + +> API design principles and decision-making for 2025. +> **Learn to THINK, not copy fixed patterns.** + +## ๐ŸŽฏ Selective Reading Rule + +**Read ONLY files relevant to the request!** Check the content map, find what you need. + +--- + +## ๐Ÿ“‘ Content Map + +| File | Description | When to Read | +|------|-------------|--------------| +| `api-style.md` | REST vs GraphQL vs tRPC decision tree | Choosing API type | +| `rest.md` | Resource naming, HTTP methods, status codes | Designing REST API | +| `response.md` | Envelope pattern, error format, pagination | Response structure | +| `graphql.md` | Schema design, when to use, security | Considering GraphQL | +| `trpc.md` | TypeScript monorepo, type safety | TS fullstack projects | +| `versioning.md` | URI/Header/Query versioning | API evolution planning | +| `auth.md` | JWT, OAuth, Passkey, API Keys | Auth pattern selection | +| `rate-limiting.md` | Token bucket, sliding window | API protection | +| `documentation.md` | OpenAPI/Swagger best practices | Documentation | +| `security-testing.md` | OWASP API Top 10, auth/authz testing | Security audits | + +--- + +## ๐Ÿ”— Related Skills + +| Need | Skill | +|------|-------| +| API implementation | `@[skills/backend-development]` | +| Data structure | `@[skills/database-design]` | +| Security details | `@[skills/security-hardening]` | + +--- + +## โœ… Decision Checklist + +Before designing an API: + +- [ ] **Asked user about API consumers?** +- [ ] **Chosen API style for THIS context?** (REST/GraphQL/tRPC) +- [ ] **Defined consistent response format?** +- [ ] **Planned versioning strategy?** +- [ ] **Considered authentication needs?** +- [ ] **Planned rate limiting?** +- [ ] **Documentation approach defined?** + +--- + +## โŒ Anti-Patterns + +**DON'T:** +- Default to REST for everything +- Use verbs in REST endpoints (/getUsers) +- Return inconsistent response formats +- Expose internal errors to clients +- Skip rate limiting + +**DO:** +- Choose API style based on context +- Ask about client requirements +- Document thoroughly +- Use appropriate status codes + +--- + +## Script + +| Script | Purpose | Command | +|--------|---------|---------| +| `scripts/api_validator.py` | API endpoint validation | `python scripts/api_validator.py ` | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/api-style.md b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/api-style.md new file mode 100644 index 00000000..c94cb8a4 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/api-style.md @@ -0,0 +1,42 @@ +# API Style Selection (2025) + +> REST vs GraphQL vs tRPC - Hangi durumda hangisi? + +## Decision Tree + +``` +Who are the API consumers? +โ”‚ +โ”œโ”€โ”€ Public API / Multiple platforms +โ”‚ โ””โ”€โ”€ REST + OpenAPI (widest compatibility) +โ”‚ +โ”œโ”€โ”€ Complex data needs / Multiple frontends +โ”‚ โ””โ”€โ”€ GraphQL (flexible queries) +โ”‚ +โ”œโ”€โ”€ TypeScript frontend + backend (monorepo) +โ”‚ โ””โ”€โ”€ tRPC (end-to-end type safety) +โ”‚ +โ”œโ”€โ”€ Real-time / Event-driven +โ”‚ โ””โ”€โ”€ WebSocket + AsyncAPI +โ”‚ +โ””โ”€โ”€ Internal microservices + โ””โ”€โ”€ gRPC (performance) or REST (simplicity) +``` + +## Comparison + +| Factor | REST | GraphQL | tRPC | +|--------|------|---------|------| +| **Best for** | Public APIs | Complex apps | TS monorepos | +| **Learning curve** | Low | Medium | Low (if TS) | +| **Over/under fetching** | Common | Solved | Solved | +| **Type safety** | Manual (OpenAPI) | Schema-based | Automatic | +| **Caching** | HTTP native | Complex | Client-based | + +## Selection Questions + +1. Who are the API consumers? +2. Is the frontend TypeScript? +3. How complex are the data relationships? +4. Is caching critical? +5. Public or internal API? diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/auth.md b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/auth.md new file mode 100644 index 00000000..c04030d3 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/auth.md @@ -0,0 +1,24 @@ +# Authentication Patterns + +> Choose auth pattern based on use case. + +## Selection Guide + +| Pattern | Best For | +|---------|----------| +| **JWT** | Stateless, microservices | +| **Session** | Traditional web, simple | +| **OAuth 2.0** | Third-party integration | +| **API Keys** | Server-to-server, public APIs | +| **Passkey** | Modern passwordless (2025+) | + +## JWT Principles + +``` +Important: +โ”œโ”€โ”€ Always verify signature +โ”œโ”€โ”€ Check expiration +โ”œโ”€โ”€ Include minimal claims +โ”œโ”€โ”€ Use short expiry + refresh tokens +โ””โ”€โ”€ Never store sensitive data in JWT +``` diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/documentation.md b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/documentation.md new file mode 100644 index 00000000..5e199da0 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/documentation.md @@ -0,0 +1,26 @@ +# API Documentation Principles + +> Good docs = happy developers = API adoption. + +## OpenAPI/Swagger Essentials + +``` +Include: +โ”œโ”€โ”€ All endpoints with examples +โ”œโ”€โ”€ Request/response schemas +โ”œโ”€โ”€ Authentication requirements +โ”œโ”€โ”€ Error response formats +โ””โ”€โ”€ Rate limiting info +``` + +## Good Documentation Has + +``` +Essentials: +โ”œโ”€โ”€ Quick start / Getting started +โ”œโ”€โ”€ Authentication guide +โ”œโ”€โ”€ Complete API reference +โ”œโ”€โ”€ Error handling guide +โ”œโ”€โ”€ Code examples (multiple languages) +โ””โ”€โ”€ Changelog +``` diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/graphql.md b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/graphql.md new file mode 100644 index 00000000..1e5632ce --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/graphql.md @@ -0,0 +1,41 @@ +# GraphQL Principles + +> Flexible queries for complex, interconnected data. + +## When to Use + +``` +โœ… Good fit: +โ”œโ”€โ”€ Complex, interconnected data +โ”œโ”€โ”€ Multiple frontend platforms +โ”œโ”€โ”€ Clients need flexible queries +โ”œโ”€โ”€ Evolving data requirements +โ””โ”€โ”€ Reducing over-fetching matters + +โŒ Poor fit: +โ”œโ”€โ”€ Simple CRUD operations +โ”œโ”€โ”€ File upload heavy +โ”œโ”€โ”€ HTTP caching important +โ””โ”€โ”€ Team unfamiliar with GraphQL +``` + +## Schema Design Principles + +``` +Principles: +โ”œโ”€โ”€ Think in graphs, not endpoints +โ”œโ”€โ”€ Design for evolvability (no versions) +โ”œโ”€โ”€ Use connections for pagination +โ”œโ”€โ”€ Be specific with types (not generic "data") +โ””โ”€โ”€ Handle nullability thoughtfully +``` + +## Security Considerations + +``` +Protect against: +โ”œโ”€โ”€ Query depth attacks โ†’ Set max depth +โ”œโ”€โ”€ Query complexity โ†’ Calculate cost +โ”œโ”€โ”€ Batching abuse โ†’ Limit batch size +โ”œโ”€โ”€ Introspection โ†’ Disable in production +``` diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/rate-limiting.md b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/rate-limiting.md new file mode 100644 index 00000000..cffaa290 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/rate-limiting.md @@ -0,0 +1,31 @@ +# Rate Limiting Principles + +> Protect your API from abuse and overload. + +## Why Rate Limit + +``` +Protect against: +โ”œโ”€โ”€ Brute force attacks +โ”œโ”€โ”€ Resource exhaustion +โ”œโ”€โ”€ Cost overruns (if pay-per-use) +โ””โ”€โ”€ Unfair usage +``` + +## Strategy Selection + +| Type | How | When | +|------|-----|------| +| **Token bucket** | Burst allowed, refills over time | Most APIs | +| **Sliding window** | Smooth distribution | Strict limits | +| **Fixed window** | Simple counters per window | Basic needs | + +## Response Headers + +``` +Include in headers: +โ”œโ”€โ”€ X-RateLimit-Limit (max requests) +โ”œโ”€โ”€ X-RateLimit-Remaining (requests left) +โ”œโ”€โ”€ X-RateLimit-Reset (when limit resets) +โ””โ”€โ”€ Return 429 when exceeded +``` diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/response.md b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/response.md new file mode 100644 index 00000000..3c6ab141 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/response.md @@ -0,0 +1,37 @@ +# Response Format Principles + +> Consistency is key - choose a format and stick to it. + +## Common Patterns + +``` +Choose one: +โ”œโ”€โ”€ Envelope pattern ({ success, data, error }) +โ”œโ”€โ”€ Direct data (just return the resource) +โ””โ”€โ”€ HAL/JSON:API (hypermedia) +``` + +## Error Response + +``` +Include: +โ”œโ”€โ”€ Error code (for programmatic handling) +โ”œโ”€โ”€ User message (for display) +โ”œโ”€โ”€ Details (for debugging, field-level errors) +โ”œโ”€โ”€ Request ID (for support) +โ””โ”€โ”€ NOT internal details (security!) +``` + +## Pagination Types + +| Type | Best For | Trade-offs | +|------|----------|------------| +| **Offset** | Simple, jumpable | Performance on large datasets | +| **Cursor** | Large datasets | Can't jump to page | +| **Keyset** | Performance critical | Requires sortable key | + +### Selection Questions + +1. How large is the dataset? +2. Do users need to jump to specific pages? +3. Is data frequently changing? diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/rest.md b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/rest.md new file mode 100644 index 00000000..c04aa7ca --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/rest.md @@ -0,0 +1,40 @@ +# REST Principles + +> Resource-based API design - nouns not verbs. + +## Resource Naming Rules + +``` +Principles: +โ”œโ”€โ”€ Use NOUNS, not verbs (resources, not actions) +โ”œโ”€โ”€ Use PLURAL forms (/users not /user) +โ”œโ”€โ”€ Use lowercase with hyphens (/user-profiles) +โ”œโ”€โ”€ Nest for relationships (/users/123/posts) +โ””โ”€โ”€ Keep shallow (max 3 levels deep) +``` + +## HTTP Method Selection + +| Method | Purpose | Idempotent? | Body? | +|--------|---------|-------------|-------| +| **GET** | Read resource(s) | Yes | No | +| **POST** | Create new resource | No | Yes | +| **PUT** | Replace entire resource | Yes | Yes | +| **PATCH** | Partial update | No | Yes | +| **DELETE** | Remove resource | Yes | No | + +## Status Code Selection + +| Situation | Code | Why | +|-----------|------|-----| +| Success (read) | 200 | Standard success | +| Created | 201 | New resource created | +| No content | 204 | Success, nothing to return | +| Bad request | 400 | Malformed request | +| Unauthorized | 401 | Missing/invalid auth | +| Forbidden | 403 | Valid auth, no permission | +| Not found | 404 | Resource doesn't exist | +| Conflict | 409 | State conflict (duplicate) | +| Validation error | 422 | Valid syntax, invalid data | +| Rate limited | 429 | Too many requests | +| Server error | 500 | Our fault | diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/scripts/api_validator.py b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/scripts/api_validator.py new file mode 100644 index 00000000..930db829 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/scripts/api_validator.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +""" +API Validator - Checks API endpoints for best practices. +Validates OpenAPI specs, response formats, and common issues. +""" +import sys +import json +import re +from pathlib import Path + +# Fix Windows console encoding for Unicode output +try: + sys.stdout.reconfigure(encoding='utf-8', errors='replace') + sys.stderr.reconfigure(encoding='utf-8', errors='replace') +except AttributeError: + pass # Python < 3.7 + +def find_api_files(project_path: Path) -> list: + """Find API-related files.""" + patterns = [ + "**/*api*.ts", "**/*api*.js", "**/*api*.py", + "**/routes/*.ts", "**/routes/*.js", "**/routes/*.py", + "**/controllers/*.ts", "**/controllers/*.js", + "**/endpoints/*.ts", "**/endpoints/*.py", + "**/*.openapi.json", "**/*.openapi.yaml", + "**/swagger.json", "**/swagger.yaml", + "**/openapi.json", "**/openapi.yaml" + ] + + files = [] + for pattern in patterns: + files.extend(project_path.glob(pattern)) + + # Exclude node_modules, etc. + return [f for f in files if not any(x in str(f) for x in ['node_modules', '.git', 'dist', 'build', '__pycache__'])] + +def check_openapi_spec(file_path: Path) -> dict: + """Check OpenAPI/Swagger specification.""" + issues = [] + passed = [] + + try: + content = file_path.read_text(encoding='utf-8') + + if file_path.suffix == '.json': + spec = json.loads(content) + else: + # Basic YAML check + if 'openapi:' in content or 'swagger:' in content: + passed.append("[OK] OpenAPI/Swagger version defined") + else: + issues.append("[X] No OpenAPI version found") + + if 'paths:' in content: + passed.append("[OK] Paths section exists") + else: + issues.append("[X] No paths defined") + + if 'components:' in content or 'definitions:' in content: + passed.append("[OK] Schema components defined") + + return {'file': str(file_path), 'passed': passed, 'issues': issues, 'type': 'openapi'} + + # JSON OpenAPI checks + if 'openapi' in spec or 'swagger' in spec: + passed.append("[OK] OpenAPI version defined") + + if 'info' in spec: + if 'title' in spec['info']: + passed.append("[OK] API title defined") + if 'version' in spec['info']: + passed.append("[OK] API version defined") + if 'description' not in spec['info']: + issues.append("[!] API description missing") + + if 'paths' in spec: + path_count = len(spec['paths']) + passed.append(f"[OK] {path_count} endpoints defined") + + # Check each path + for path, methods in spec['paths'].items(): + for method, details in methods.items(): + if method in ['get', 'post', 'put', 'patch', 'delete']: + if 'responses' not in details: + issues.append(f"[X] {method.upper()} {path}: No responses defined") + if 'summary' not in details and 'description' not in details: + issues.append(f"[!] {method.upper()} {path}: No description") + + except Exception as e: + issues.append(f"[X] Parse error: {e}") + + return {'file': str(file_path), 'passed': passed, 'issues': issues, 'type': 'openapi'} + +def check_api_code(file_path: Path) -> dict: + """Check API code for common issues.""" + issues = [] + passed = [] + + try: + content = file_path.read_text(encoding='utf-8') + + # Check for error handling + error_patterns = [ + r'try\s*{', r'try:', r'\.catch\(', + r'except\s+', r'catch\s*\(' + ] + has_error_handling = any(re.search(p, content) for p in error_patterns) + if has_error_handling: + passed.append("[OK] Error handling present") + else: + issues.append("[X] No error handling found") + + # Check for status codes + status_patterns = [ + r'status\s*\(\s*\d{3}\s*\)', r'statusCode\s*[=:]\s*\d{3}', + r'HttpStatus\.', r'status_code\s*=\s*\d{3}', + r'\.status\(\d{3}\)', r'res\.status\(' + ] + has_status = any(re.search(p, content) for p in status_patterns) + if has_status: + passed.append("[OK] HTTP status codes used") + else: + issues.append("[!] No explicit HTTP status codes") + + # Check for validation + validation_patterns = [ + r'validate', r'schema', r'zod', r'joi', r'yup', + r'pydantic', r'@Body\(', r'@Query\(' + ] + has_validation = any(re.search(p, content, re.I) for p in validation_patterns) + if has_validation: + passed.append("[OK] Input validation present") + else: + issues.append("[!] No input validation detected") + + # Check for auth middleware + auth_patterns = [ + r'auth', r'jwt', r'bearer', r'token', + r'middleware', r'guard', r'@Authenticated' + ] + has_auth = any(re.search(p, content, re.I) for p in auth_patterns) + if has_auth: + passed.append("[OK] Authentication/authorization detected") + + # Check for rate limiting + rate_patterns = [r'rateLimit', r'throttle', r'rate.?limit'] + has_rate = any(re.search(p, content, re.I) for p in rate_patterns) + if has_rate: + passed.append("[OK] Rate limiting present") + + # Check for logging + log_patterns = [r'console\.log', r'logger\.', r'logging\.', r'log\.'] + has_logging = any(re.search(p, content) for p in log_patterns) + if has_logging: + passed.append("[OK] Logging present") + + except Exception as e: + issues.append(f"[X] Read error: {e}") + + return {'file': str(file_path), 'passed': passed, 'issues': issues, 'type': 'code'} + +def main(): + target = sys.argv[1] if len(sys.argv) > 1 else "." + project_path = Path(target) + + print("\n" + "=" * 60) + print(" API VALIDATOR - Endpoint Best Practices Check") + print("=" * 60 + "\n") + + api_files = find_api_files(project_path) + + if not api_files: + print("[!] No API files found.") + print(" Looking for: routes/, controllers/, api/, openapi.json/yaml") + sys.exit(0) + + results = [] + for file_path in api_files[:15]: # Limit + if 'openapi' in file_path.name.lower() or 'swagger' in file_path.name.lower(): + result = check_openapi_spec(file_path) + else: + result = check_api_code(file_path) + results.append(result) + + # Print results + total_issues = 0 + total_passed = 0 + + for result in results: + print(f"\n[FILE] {result['file']} [{result['type']}]") + for item in result['passed']: + print(f" {item}") + total_passed += 1 + for item in result['issues']: + print(f" {item}") + if item.startswith("[X]"): + total_issues += 1 + + print("\n" + "=" * 60) + print(f"[RESULTS] {total_passed} passed, {total_issues} critical issues") + print("=" * 60) + + if total_issues == 0: + print("[OK] API validation passed") + sys.exit(0) + else: + print("[X] Fix critical issues before deployment") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/security-testing.md b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/security-testing.md new file mode 100644 index 00000000..265023fa --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/security-testing.md @@ -0,0 +1,122 @@ +# API Security Testing + +> Principles for testing API security. OWASP API Top 10, authentication, authorization testing. + +--- + +## OWASP API Security Top 10 + +| Vulnerability | Test Focus | +|---------------|------------| +| **API1: BOLA** | Access other users' resources | +| **API2: Broken Auth** | JWT, session, credentials | +| **API3: Property Auth** | Mass assignment, data exposure | +| **API4: Resource Consumption** | Rate limiting, DoS | +| **API5: Function Auth** | Admin endpoints, role bypass | +| **API6: Business Flow** | Logic abuse, automation | +| **API7: SSRF** | Internal network access | +| **API8: Misconfiguration** | Debug endpoints, CORS | +| **API9: Inventory** | Shadow APIs, old versions | +| **API10: Unsafe Consumption** | Third-party API trust | + +--- + +## Authentication Testing + +### JWT Testing + +| Check | What to Test | +|-------|--------------| +| Algorithm | None, algorithm confusion | +| Secret | Weak secrets, brute force | +| Claims | Expiration, issuer, audience | +| Signature | Manipulation, key injection | + +### Session Testing + +| Check | What to Test | +|-------|--------------| +| Generation | Predictability | +| Storage | Client-side security | +| Expiration | Timeout enforcement | +| Invalidation | Logout effectiveness | + +--- + +## Authorization Testing + +| Test Type | Approach | +|-----------|----------| +| **Horizontal** | Access peer users' data | +| **Vertical** | Access higher privilege functions | +| **Context** | Access outside allowed scope | + +### BOLA/IDOR Testing + +1. Identify resource IDs in requests +2. Capture request with user A's session +3. Replay with user B's session +4. Check for unauthorized access + +--- + +## Input Validation Testing + +| Injection Type | Test Focus | +|----------------|------------| +| SQL | Query manipulation | +| NoSQL | Document queries | +| Command | System commands | +| LDAP | Directory queries | + +**Approach:** Test all parameters, try type coercion, test boundaries, check error messages. + +--- + +## Rate Limiting Testing + +| Aspect | Check | +|--------|-------| +| Existence | Is there any limit? | +| Bypass | Headers, IP rotation | +| Scope | Per-user, per-IP, global | + +**Bypass techniques:** X-Forwarded-For, different HTTP methods, case variations, API versioning. + +--- + +## GraphQL Security + +| Test | Focus | +|------|-------| +| Introspection | Schema disclosure | +| Batching | Query DoS | +| Nesting | Depth-based DoS | +| Authorization | Field-level access | + +--- + +## Security Testing Checklist + +**Authentication:** +- [ ] Test for bypass +- [ ] Check credential strength +- [ ] Verify token security + +**Authorization:** +- [ ] Test BOLA/IDOR +- [ ] Check privilege escalation +- [ ] Verify function access + +**Input:** +- [ ] Test all parameters +- [ ] Check for injection + +**Config:** +- [ ] Check CORS +- [ ] Verify headers +- [ ] Test error handling + +--- + +> **Remember:** APIs are the backbone of modern apps. Test them like attackers will. diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/trpc.md b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/trpc.md new file mode 100644 index 00000000..10976866 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/trpc.md @@ -0,0 +1,41 @@ +# tRPC Principles + +> End-to-end type safety for TypeScript monorepos. + +## When to Use + +``` +โœ… Perfect fit: +โ”œโ”€โ”€ TypeScript on both ends +โ”œโ”€โ”€ Monorepo structure +โ”œโ”€โ”€ Internal tools +โ”œโ”€โ”€ Rapid development +โ””โ”€โ”€ Type safety critical + +โŒ Poor fit: +โ”œโ”€โ”€ Non-TypeScript clients +โ”œโ”€โ”€ Public API +โ”œโ”€โ”€ Need REST conventions +โ””โ”€โ”€ Multiple language backends +``` + +## Key Benefits + +``` +Why tRPC: +โ”œโ”€โ”€ Zero schema maintenance +โ”œโ”€โ”€ End-to-end type inference +โ”œโ”€โ”€ IDE autocomplete across stack +โ”œโ”€โ”€ Instant API changes reflected +โ””โ”€โ”€ No code generation step +``` + +## Integration Patterns + +``` +Common setups: +โ”œโ”€โ”€ Next.js + tRPC (most common) +โ”œโ”€โ”€ Monorepo with shared types +โ”œโ”€โ”€ Remix + tRPC +โ””โ”€โ”€ Any TS frontend + backend +``` diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/versioning.md b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/versioning.md new file mode 100644 index 00000000..5ead01b2 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/api-patterns/versioning.md @@ -0,0 +1,22 @@ +# Versioning Strategies + +> Plan for API evolution from day one. + +## Decision Factors + +| Strategy | Implementation | Trade-offs | +|----------|---------------|------------| +| **URI** | /v1/users | Clear, easy caching | +| **Header** | Accept-Version: 1 | Cleaner URLs, harder discovery | +| **Query** | ?version=1 | Easy to add, messy | +| **None** | Evolve carefully | Best for internal, risky for public | + +## Versioning Philosophy + +``` +Consider: +โ”œโ”€โ”€ Public API? โ†’ Version in URI +โ”œโ”€โ”€ Internal only? โ†’ May not need versioning +โ”œโ”€โ”€ GraphQL? โ†’ Typically no versions (evolve schema) +โ”œโ”€โ”€ tRPC? โ†’ Types enforce compatibility +``` diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/SKILL.md b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/SKILL.md new file mode 100644 index 00000000..e42cfb26 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/SKILL.md @@ -0,0 +1,347 @@ +--- +name: backend-dev-guidelines +description: "You are a senior backend engineer operating production-grade services under strict architectural and reliability constraints. Use when routes, controllers, services, repositories, express middleware, or prisma database access." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Backend Development Guidelines + +**(Node.js ยท Express ยท TypeScript ยท Microservices)** + +You are a **senior backend engineer** operating production-grade services under strict architectural and reliability constraints. + +Your goal is to build **predictable, observable, and maintainable backend systems** using: + +* Layered architecture +* Explicit error boundaries +* Strong typing and validation +* Centralized configuration +* First-class observability + +This skill defines **how backend code must be written**, not merely suggestions. + +--- + +## 1. Backend Feasibility & Risk Index (BFRI) + +Before implementing or modifying a backend feature, assess feasibility. + +### BFRI Dimensions (1โ€“5) + +| Dimension | Question | +| ----------------------------- | ---------------------------------------------------------------- | +| **Architectural Fit** | Does this follow routes โ†’ controllers โ†’ services โ†’ repositories? | +| **Business Logic Complexity** | How complex is the domain logic? | +| **Data Risk** | Does this affect critical data paths or transactions? | +| **Operational Risk** | Does this impact auth, billing, messaging, or infra? | +| **Testability** | Can this be reliably unit + integration tested? | + +### Score Formula + +``` +BFRI = (Architectural Fit + Testability) โˆ’ (Complexity + Data Risk + Operational Risk) +``` + +**Range:** `-10 โ†’ +10` + +### Interpretation + +| BFRI | Meaning | Action | +| -------- | --------- | ---------------------- | +| **6โ€“10** | Safe | Proceed | +| **3โ€“5** | Moderate | Add tests + monitoring | +| **0โ€“2** | Risky | Refactor or isolate | +| **< 0** | Dangerous | Redesign before coding | + +--- + +## When to Use +Automatically applies when working on: + +* Routes, controllers, services, repositories +* Express middleware +* Prisma database access +* Zod validation +* Sentry error tracking +* Configuration management +* Backend refactors or migrations + +--- + +## 3. Core Architecture Doctrine (Non-Negotiable) + +### 1. Layered Architecture Is Mandatory + +``` +Routes โ†’ Controllers โ†’ Services โ†’ Repositories โ†’ Database +``` + +* No layer skipping +* No cross-layer leakage +* Each layer has **one responsibility** + +--- + +### 2. Routes Only Route + +```ts +// โŒ NEVER +router.post('/create', async (req, res) => { + await prisma.user.create(...); +}); + +// โœ… ALWAYS +router.post('/create', (req, res) => + userController.create(req, res) +); +``` + +Routes must contain **zero business logic**. + +--- + +### 3. Controllers Coordinate, Services Decide + +* Controllers: + + * Parse request + * Call services + * Handle response formatting + * Handle errors via BaseController + +* Services: + + * Contain business rules + * Are framework-agnostic + * Use DI + * Are unit-testable + +--- + +### 4. All Controllers Extend `BaseController` + +```ts +export class UserController extends BaseController { + async getUser(req: Request, res: Response): Promise { + try { + const user = await this.userService.getById(req.params.id); + this.handleSuccess(res, user); + } catch (error) { + this.handleError(error, res, 'getUser'); + } + } +} +``` + +No raw `res.json` calls outside BaseController helpers. + +--- + +### 5. All Errors Go to Sentry + +```ts +catch (error) { + Sentry.captureException(error); + throw error; +} +``` + +โŒ `console.log` +โŒ silent failures +โŒ swallowed errors + +--- + +### 6. unifiedConfig Is the Only Config Source + +```ts +// โŒ NEVER +process.env.JWT_SECRET; + +// โœ… ALWAYS +import { config } from '@/config/unifiedConfig'; +config.auth.jwtSecret; +``` + +--- + +### 7. Validate All External Input with Zod + +* Request bodies +* Query params +* Route params +* Webhook payloads + +```ts +const schema = z.object({ + email: z.string().email(), +}); + +const input = schema.parse(req.body); +``` + +No validation = bug. + +--- + +## 4. Directory Structure (Canonical) + +``` +src/ +โ”œโ”€โ”€ config/ # unifiedConfig +โ”œโ”€โ”€ controllers/ # BaseController + controllers +โ”œโ”€โ”€ services/ # Business logic +โ”œโ”€โ”€ repositories/ # Prisma access +โ”œโ”€โ”€ routes/ # Express routes +โ”œโ”€โ”€ middleware/ # Auth, validation, errors +โ”œโ”€โ”€ validators/ # Zod schemas +โ”œโ”€โ”€ types/ # Shared types +โ”œโ”€โ”€ utils/ # Helpers +โ”œโ”€โ”€ tests/ # Unit + integration tests +โ”œโ”€โ”€ instrument.ts # Sentry (FIRST IMPORT) +โ”œโ”€โ”€ app.ts # Express app +โ””โ”€โ”€ server.ts # HTTP server +``` + +--- + +## 5. Naming Conventions (Strict) + +| Layer | Convention | +| ---------- | ------------------------- | +| Controller | `PascalCaseController.ts` | +| Service | `camelCaseService.ts` | +| Repository | `PascalCaseRepository.ts` | +| Routes | `camelCaseRoutes.ts` | +| Validators | `camelCase.schema.ts` | + +--- + +## 6. Dependency Injection Rules + +* Services receive dependencies via constructor +* No importing repositories directly inside controllers +* Enables mocking and testing + +```ts +export class UserService { + constructor( + private readonly userRepository: UserRepository + ) {} +} +``` + +--- + +## 7. Prisma & Repository Rules + +* Prisma client **never used directly in controllers** +* Repositories: + + * Encapsulate queries + * Handle transactions + * Expose intent-based methods + +```ts +await userRepository.findActiveUsers(); +``` + +--- + +## 8. Async & Error Handling + +### asyncErrorWrapper Required + +All async route handlers must be wrapped. + +```ts +router.get( + '/users', + asyncErrorWrapper((req, res) => + controller.list(req, res) + ) +); +``` + +No unhandled promise rejections. + +--- + +## 9. Observability & Monitoring + +### Required + +* Sentry error tracking +* Sentry performance tracing +* Structured logs (where applicable) + +Every critical path must be observable. + +--- + +## 10. Testing Discipline + +### Required Tests + +* **Unit tests** for services +* **Integration tests** for routes +* **Repository tests** for complex queries + +```ts +describe('UserService', () => { + it('creates a user', async () => { + expect(user).toBeDefined(); + }); +}); +``` + +No tests โ†’ no merge. + +--- + +## 11. Anti-Patterns (Immediate Rejection) + +โŒ Business logic in routes +โŒ Skipping service layer +โŒ Direct Prisma in controllers +โŒ Missing validation +โŒ process.env usage +โŒ console.log instead of Sentry +โŒ Untested business logic + +--- + +## 12. Integration With Other Skills + +* **frontend-dev-guidelines** โ†’ API contract alignment +* **error-tracking** โ†’ Sentry standards +* **database-verification** โ†’ Schema correctness +* **analytics-tracking** โ†’ Event pipelines +* **skill-developer** โ†’ Skill governance + +--- + +## 13. Operator Validation Checklist + +Before finalizing backend work: + +* [ ] BFRI โ‰ฅ 3 +* [ ] Layered architecture respected +* [ ] Input validated +* [ ] Errors captured in Sentry +* [ ] unifiedConfig used +* [ ] Tests written +* [ ] No anti-patterns present + +--- + +## 14. Skill Status + +**Status:** Stable ยท Enforceable ยท Production-grade +**Intended Use:** Long-lived Node.js microservices with real traffic and real risk +--- + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/architecture-overview.md b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/architecture-overview.md new file mode 100644 index 00000000..d472845f --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/architecture-overview.md @@ -0,0 +1,451 @@ +# Architecture Overview - Backend Services + +Complete guide to the layered architecture pattern used in backend microservices. + +## Table of Contents + +- [Layered Architecture Pattern](#layered-architecture-pattern) +- [Request Lifecycle](#request-lifecycle) +- [Service Comparison](#service-comparison) +- [Directory Structure Rationale](#directory-structure-rationale) +- [Module Organization](#module-organization) +- [Separation of Concerns](#separation-of-concerns) + +--- + +## Layered Architecture Pattern + +### The Four Layers + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ HTTP Request โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Layer 1: ROUTES โ”‚ +โ”‚ - Route definitions only โ”‚ +โ”‚ - Middleware registration โ”‚ +โ”‚ - Delegate to controllers โ”‚ +โ”‚ - NO business logic โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Layer 2: CONTROLLERS โ”‚ +โ”‚ - Request/response handling โ”‚ +โ”‚ - Input validation โ”‚ +โ”‚ - Call services โ”‚ +โ”‚ - Format responses โ”‚ +โ”‚ - Error handling โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Layer 3: SERVICES โ”‚ +โ”‚ - Business logic โ”‚ +โ”‚ - Orchestration โ”‚ +โ”‚ - Call repositories โ”‚ +โ”‚ - No HTTP knowledge โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Layer 4: REPOSITORIES โ”‚ +โ”‚ - Data access abstraction โ”‚ +โ”‚ - Prisma operations โ”‚ +โ”‚ - Query optimization โ”‚ +โ”‚ - Caching โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Database (MySQL) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Why This Architecture? + +**Testability:** +- Each layer can be tested independently +- Easy to mock dependencies +- Clear test boundaries + +**Maintainability:** +- Changes isolated to specific layers +- Business logic separate from HTTP concerns +- Easy to locate bugs + +**Reusability:** +- Services can be used by routes, cron jobs, scripts +- Repositories hide database implementation +- Business logic not tied to HTTP + +**Scalability:** +- Easy to add new endpoints +- Clear patterns to follow +- Consistent structure + +--- + +## Request Lifecycle + +### Complete Flow Example + +```typescript +1. HTTP POST /api/users + โ†“ +2. Express matches route in userRoutes.ts + โ†“ +3. Middleware chain executes: + - SSOMiddleware.verifyLoginStatus (authentication) + - auditMiddleware (context tracking) + โ†“ +4. Route handler delegates to controller: + router.post('/users', (req, res) => userController.create(req, res)) + โ†“ +5. Controller validates and calls service: + - Validate input with Zod + - Call userService.create(data) + - Handle success/error + โ†“ +6. Service executes business logic: + - Check business rules + - Call userRepository.create(data) + - Return result + โ†“ +7. Repository performs database operation: + - PrismaService.main.user.create({ data }) + - Handle database errors + - Return created user + โ†“ +8. Response flows back: + Repository โ†’ Service โ†’ Controller โ†’ Express โ†’ Client +``` + +### Middleware Execution Order + +**Critical:** Middleware executes in registration order + +```typescript +app.use(Sentry.Handlers.requestHandler()); // 1. Sentry tracing (FIRST) +app.use(express.json()); // 2. Body parsing +app.use(express.urlencoded({ extended: true })); // 3. URL encoding +app.use(cookieParser()); // 4. Cookie parsing +app.use(SSOMiddleware.initialize()); // 5. Auth initialization +// ... routes registered here +app.use(auditMiddleware); // 6. Audit (if global) +app.use(errorBoundary); // 7. Error handler (LAST) +app.use(Sentry.Handlers.errorHandler()); // 8. Sentry errors (LAST) +``` + +**Rule:** Error handlers must be registered AFTER routes! + +--- + +## Service Comparison + +### Email Service (Mature Pattern โœ…) + +**Strengths:** +- Comprehensive BaseController with Sentry integration +- Clean route delegation (no business logic in routes) +- Consistent dependency injection pattern +- Good middleware organization +- Type-safe throughout +- Excellent error handling + +**Example Structure:** +``` +email/src/ +โ”œโ”€โ”€ controllers/ +โ”‚ โ”œโ”€โ”€ BaseController.ts โœ… Excellent template +โ”‚ โ”œโ”€โ”€ NotificationController.ts โœ… Extends BaseController +โ”‚ โ””โ”€โ”€ EmailController.ts โœ… Clean patterns +โ”œโ”€โ”€ routes/ +โ”‚ โ”œโ”€โ”€ notificationRoutes.ts โœ… Clean delegation +โ”‚ โ””โ”€โ”€ emailRoutes.ts โœ… No business logic +โ”œโ”€โ”€ services/ +โ”‚ โ”œโ”€โ”€ NotificationService.ts โœ… Dependency injection +โ”‚ โ””โ”€โ”€ BatchingService.ts โœ… Clear responsibility +โ””โ”€โ”€ middleware/ + โ”œโ”€โ”€ errorBoundary.ts โœ… Comprehensive + โ””โ”€โ”€ DevImpersonationSSOMiddleware.ts +``` + +**Use as template** for new services! + +### Form Service (Transitioning โš ๏ธ) + +**Strengths:** +- Excellent workflow architecture (event sourcing) +- Good Sentry integration +- Innovative audit middleware (AsyncLocalStorage) +- Comprehensive permission system + +**Weaknesses:** +- Some routes have 200+ lines of business logic +- Inconsistent controller naming +- Direct process.env usage (60+ occurrences) +- Minimal repository pattern usage + +**Example:** +``` +form/src/ +โ”œโ”€โ”€ routes/ +โ”‚ โ”œโ”€โ”€ responseRoutes.ts โŒ Business logic in routes +โ”‚ โ””โ”€โ”€ proxyRoutes.ts โœ… Good validation pattern +โ”œโ”€โ”€ controllers/ +โ”‚ โ”œโ”€โ”€ formController.ts โš ๏ธ Lowercase naming +โ”‚ โ””โ”€โ”€ UserProfileController.ts โœ… PascalCase naming +โ”œโ”€โ”€ workflow/ โœ… Excellent architecture! +โ”‚ โ”œโ”€โ”€ core/ +โ”‚ โ”‚ โ”œโ”€โ”€ WorkflowEngineV3.ts โœ… Event sourcing +โ”‚ โ”‚ โ””โ”€โ”€ DryRunWrapper.ts โœ… Innovative +โ”‚ โ””โ”€โ”€ services/ +โ””โ”€โ”€ middleware/ + โ””โ”€โ”€ auditMiddleware.ts โœ… AsyncLocalStorage pattern +``` + +**Learn from:** workflow/, middleware/auditMiddleware.ts +**Avoid:** responseRoutes.ts, direct process.env + +--- + +## Directory Structure Rationale + +### Controllers Directory + +**Purpose:** Handle HTTP request/response concerns + +**Contents:** +- `BaseController.ts` - Base class with common methods +- `{Feature}Controller.ts` - Feature-specific controllers + +**Naming:** PascalCase + Controller + +**Responsibilities:** +- Parse request parameters +- Validate input (Zod) +- Call appropriate service methods +- Format responses +- Handle errors (via BaseController) +- Set HTTP status codes + +### Services Directory + +**Purpose:** Business logic and orchestration + +**Contents:** +- `{feature}Service.ts` - Feature business logic + +**Naming:** camelCase + Service (or PascalCase + Service) + +**Responsibilities:** +- Implement business rules +- Orchestrate multiple repositories +- Transaction management +- Business validations +- No HTTP knowledge (Request/Response types) + +### Repositories Directory + +**Purpose:** Data access abstraction + +**Contents:** +- `{Entity}Repository.ts` - Database operations for entity + +**Naming:** PascalCase + Repository + +**Responsibilities:** +- Prisma query operations +- Query optimization +- Database error handling +- Caching layer +- Hide Prisma implementation details + +**Current Gap:** Only 1 repository exists (WorkflowRepository) + +### Routes Directory + +**Purpose:** Route registration ONLY + +**Contents:** +- `{feature}Routes.ts` - Express router for feature + +**Naming:** camelCase + Routes + +**Responsibilities:** +- Register routes with Express +- Apply middleware +- Delegate to controllers +- **NO business logic!** + +### Middleware Directory + +**Purpose:** Cross-cutting concerns + +**Contents:** +- Authentication middleware +- Audit middleware +- Error boundaries +- Validation middleware +- Custom middleware + +**Naming:** camelCase + +**Types:** +- Request processing (before handler) +- Response processing (after handler) +- Error handling (error boundary) + +### Config Directory + +**Purpose:** Configuration management + +**Contents:** +- `unifiedConfig.ts` - Type-safe configuration +- Environment-specific configs + +**Pattern:** Single source of truth + +### Types Directory + +**Purpose:** TypeScript type definitions + +**Contents:** +- `{feature}.types.ts` - Feature-specific types +- DTOs (Data Transfer Objects) +- Request/Response types +- Domain models + +--- + +## Module Organization + +### Feature-Based Organization + +For large features, use subdirectories: + +``` +src/workflow/ +โ”œโ”€โ”€ core/ # Core engine +โ”œโ”€โ”€ services/ # Workflow-specific services +โ”œโ”€โ”€ actions/ # System actions +โ”œโ”€โ”€ models/ # Domain models +โ”œโ”€โ”€ validators/ # Workflow validation +โ””โ”€โ”€ utils/ # Workflow utilities +``` + +**When to use:** +- Feature has 5+ files +- Clear sub-domains exist +- Logical grouping improves clarity + +### Flat Organization + +For simple features: + +``` +src/ +โ”œโ”€โ”€ controllers/UserController.ts +โ”œโ”€โ”€ services/userService.ts +โ”œโ”€โ”€ routes/userRoutes.ts +โ””โ”€โ”€ repositories/UserRepository.ts +``` + +**When to use:** +- Simple features (< 5 files) +- No clear sub-domains +- Flat structure is clearer + +--- + +## Separation of Concerns + +### What Goes Where + +**Routes Layer:** +- โœ… Route definitions +- โœ… Middleware registration +- โœ… Controller delegation +- โŒ Business logic +- โŒ Database operations +- โŒ Validation logic (should be in validator or controller) + +**Controllers Layer:** +- โœ… Request parsing (params, body, query) +- โœ… Input validation (Zod) +- โœ… Service calls +- โœ… Response formatting +- โœ… Error handling +- โŒ Business logic +- โŒ Database operations + +**Services Layer:** +- โœ… Business logic +- โœ… Business rules enforcement +- โœ… Orchestration (multiple repos) +- โœ… Transaction management +- โŒ HTTP concerns (Request/Response) +- โŒ Direct Prisma calls (use repositories) + +**Repositories Layer:** +- โœ… Prisma operations +- โœ… Query construction +- โœ… Database error handling +- โœ… Caching +- โŒ Business logic +- โŒ HTTP concerns + +### Example: User Creation + +**Route:** +```typescript +router.post('/users', + SSOMiddleware.verifyLoginStatus, + auditMiddleware, + (req, res) => userController.create(req, res) +); +``` + +**Controller:** +```typescript +async create(req: Request, res: Response): Promise { + try { + const validated = createUserSchema.parse(req.body); + const user = await this.userService.create(validated); + this.handleSuccess(res, user, 'User created'); + } catch (error) { + this.handleError(error, res, 'create'); + } +} +``` + +**Service:** +```typescript +async create(data: CreateUserDTO): Promise { + // Business rule: check if email already exists + const existing = await this.userRepository.findByEmail(data.email); + if (existing) throw new ConflictError('Email already exists'); + + // Create user + return await this.userRepository.create(data); +} +``` + +**Repository:** +```typescript +async create(data: CreateUserDTO): Promise { + return PrismaService.main.user.create({ data }); +} + +async findByEmail(email: string): Promise { + return PrismaService.main.user.findUnique({ where: { email } }); +} +``` + +**Notice:** Each layer has clear, distinct responsibilities! + +--- + +**Related Files:** +- SKILL.md - Main guide +- [routing-and-controllers.md](routing-and-controllers.md) - Routes and controllers details +- [services-and-repositories.md](services-and-repositories.md) - Service and repository patterns diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/async-and-errors.md b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/async-and-errors.md new file mode 100644 index 00000000..2a1ca99f --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/async-and-errors.md @@ -0,0 +1,307 @@ +# Async Patterns and Error Handling + +Complete guide to async/await patterns and custom error handling. + +## Table of Contents + +- [Async/Await Best Practices](#asyncawait-best-practices) +- [Promise Error Handling](#promise-error-handling) +- [Custom Error Types](#custom-error-types) +- [asyncErrorWrapper Utility](#asyncerrorwrapper-utility) +- [Error Propagation](#error-propagation) +- [Common Async Pitfalls](#common-async-pitfalls) + +--- + +## Async/Await Best Practices + +### Always Use Try-Catch + +```typescript +// โŒ NEVER: Unhandled async errors +async function fetchData() { + const data = await database.query(); // If throws, unhandled! + return data; +} + +// โœ… ALWAYS: Wrap in try-catch +async function fetchData() { + try { + const data = await database.query(); + return data; + } catch (error) { + Sentry.captureException(error); + throw error; + } +} +``` + +### Avoid .then() Chains + +```typescript +// โŒ AVOID: Promise chains +function processData() { + return fetchData() + .then(data => transform(data)) + .then(transformed => save(transformed)) + .catch(error => { + console.error(error); + }); +} + +// โœ… PREFER: Async/await +async function processData() { + try { + const data = await fetchData(); + const transformed = await transform(data); + return await save(transformed); + } catch (error) { + Sentry.captureException(error); + throw error; + } +} +``` + +--- + +## Promise Error Handling + +### Parallel Operations + +```typescript +// โœ… Handle errors in Promise.all +try { + const [users, profiles, settings] = await Promise.all([ + userService.getAll(), + profileService.getAll(), + settingsService.getAll(), + ]); +} catch (error) { + // One failure fails all + Sentry.captureException(error); + throw error; +} + +// โœ… Handle errors individually with Promise.allSettled +const results = await Promise.allSettled([ + userService.getAll(), + profileService.getAll(), + settingsService.getAll(), +]); + +results.forEach((result, index) => { + if (result.status === 'rejected') { + Sentry.captureException(result.reason, { + tags: { operation: ['users', 'profiles', 'settings'][index] } + }); + } +}); +``` + +--- + +## Custom Error Types + +### Define Custom Errors + +```typescript +// Base error class +export class AppError extends Error { + constructor( + message: string, + public code: string, + public statusCode: number, + public isOperational: boolean = true + ) { + super(message); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} + +// Specific error types +export class ValidationError extends AppError { + constructor(message: string) { + super(message, 'VALIDATION_ERROR', 400); + } +} + +export class NotFoundError extends AppError { + constructor(message: string) { + super(message, 'NOT_FOUND', 404); + } +} + +export class ForbiddenError extends AppError { + constructor(message: string) { + super(message, 'FORBIDDEN', 403); + } +} + +export class ConflictError extends AppError { + constructor(message: string) { + super(message, 'CONFLICT', 409); + } +} +``` + +### Usage + +```typescript +// Throw specific errors +if (!user) { + throw new NotFoundError('User not found'); +} + +if (user.age < 18) { + throw new ValidationError('User must be 18+'); +} + +// Error boundary handles them +function errorBoundary(error, req, res, next) { + if (error instanceof AppError) { + return res.status(error.statusCode).json({ + error: { + message: error.message, + code: error.code + } + }); + } + + // Unknown error + Sentry.captureException(error); + res.status(500).json({ error: { message: 'Internal server error' } }); +} +``` + +--- + +## asyncErrorWrapper Utility + +### Pattern + +```typescript +export function asyncErrorWrapper( + handler: (req: Request, res: Response, next: NextFunction) => Promise +) { + return async (req: Request, res: Response, next: NextFunction) => { + try { + await handler(req, res, next); + } catch (error) { + next(error); + } + }; +} +``` + +### Usage + +```typescript +// Without wrapper - error can be unhandled +router.get('/users', async (req, res) => { + const users = await userService.getAll(); // If throws, unhandled! + res.json(users); +}); + +// With wrapper - errors caught +router.get('/users', asyncErrorWrapper(async (req, res) => { + const users = await userService.getAll(); + res.json(users); +})); +``` + +--- + +## Error Propagation + +### Proper Error Chains + +```typescript +// โœ… Propagate errors up the stack +async function repositoryMethod() { + try { + return await PrismaService.main.user.findMany(); + } catch (error) { + Sentry.captureException(error, { tags: { layer: 'repository' } }); + throw error; // Propagate to service + } +} + +async function serviceMethod() { + try { + return await repositoryMethod(); + } catch (error) { + Sentry.captureException(error, { tags: { layer: 'service' } }); + throw error; // Propagate to controller + } +} + +async function controllerMethod(req, res) { + try { + const result = await serviceMethod(); + res.json(result); + } catch (error) { + this.handleError(error, res, 'controllerMethod'); // Final handler + } +} +``` + +--- + +## Common Async Pitfalls + +### Fire and Forget (Bad) + +```typescript +// โŒ NEVER: Fire and forget +async function processRequest(req, res) { + sendEmail(user.email); // Fires async, errors unhandled! + res.json({ success: true }); +} + +// โœ… ALWAYS: Await or handle +async function processRequest(req, res) { + try { + await sendEmail(user.email); + res.json({ success: true }); + } catch (error) { + Sentry.captureException(error); + res.status(500).json({ error: 'Failed to send email' }); + } +} + +// โœ… OR: Intentional background task +async function processRequest(req, res) { + sendEmail(user.email).catch(error => { + Sentry.captureException(error); + }); + res.json({ success: true }); +} +``` + +### Unhandled Rejections + +```typescript +// โœ… Global handler for unhandled rejections +process.on('unhandledRejection', (reason, promise) => { + Sentry.captureException(reason, { + tags: { type: 'unhandled_rejection' } + }); + console.error('Unhandled Rejection:', reason); +}); + +process.on('uncaughtException', (error) => { + Sentry.captureException(error, { + tags: { type: 'uncaught_exception' } + }); + console.error('Uncaught Exception:', error); + process.exit(1); +}); +``` + +--- + +**Related Files:** +- SKILL.md +- [sentry-and-monitoring.md](sentry-and-monitoring.md) +- [complete-examples.md](complete-examples.md) diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/complete-examples.md b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/complete-examples.md new file mode 100644 index 00000000..ab62578d --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/complete-examples.md @@ -0,0 +1,638 @@ +# Complete Examples - Full Working Code + +Real-world examples showing complete implementation patterns. + +## Table of Contents + +- [Complete Controller Example](#complete-controller-example) +- [Complete Service with DI](#complete-service-with-di) +- [Complete Route File](#complete-route-file) +- [Complete Repository](#complete-repository) +- [Refactoring Example: Bad to Good](#refactoring-example-bad-to-good) +- [End-to-End Feature Example](#end-to-end-feature-example) + +--- + +## Complete Controller Example + +### UserController (Following All Best Practices) + +```typescript +// controllers/UserController.ts +import { Request, Response } from 'express'; +import { BaseController } from './BaseController'; +import { UserService } from '../services/userService'; +import { createUserSchema, updateUserSchema } from '../validators/userSchemas'; +import { z } from 'zod'; + +export class UserController extends BaseController { + private userService: UserService; + + constructor() { + super(); + this.userService = new UserService(); + } + + async getUser(req: Request, res: Response): Promise { + try { + this.addBreadcrumb('Fetching user', 'user_controller', { + userId: req.params.id, + }); + + const user = await this.withTransaction( + 'user.get', + 'db.query', + () => this.userService.findById(req.params.id) + ); + + if (!user) { + return this.handleError( + new Error('User not found'), + res, + 'getUser', + 404 + ); + } + + this.handleSuccess(res, user); + } catch (error) { + this.handleError(error, res, 'getUser'); + } + } + + async listUsers(req: Request, res: Response): Promise { + try { + const users = await this.userService.getAll(); + this.handleSuccess(res, users); + } catch (error) { + this.handleError(error, res, 'listUsers'); + } + } + + async createUser(req: Request, res: Response): Promise { + try { + // Validate input with Zod + const validated = createUserSchema.parse(req.body); + + // Track performance + const user = await this.withTransaction( + 'user.create', + 'db.mutation', + () => this.userService.create(validated) + ); + + this.handleSuccess(res, user, 'User created successfully', 201); + } catch (error) { + if (error instanceof z.ZodError) { + return this.handleError(error, res, 'createUser', 400); + } + this.handleError(error, res, 'createUser'); + } + } + + async updateUser(req: Request, res: Response): Promise { + try { + const validated = updateUserSchema.parse(req.body); + + const user = await this.userService.update( + req.params.id, + validated + ); + + this.handleSuccess(res, user, 'User updated'); + } catch (error) { + if (error instanceof z.ZodError) { + return this.handleError(error, res, 'updateUser', 400); + } + this.handleError(error, res, 'updateUser'); + } + } + + async deleteUser(req: Request, res: Response): Promise { + try { + await this.userService.delete(req.params.id); + this.handleSuccess(res, null, 'User deleted', 204); + } catch (error) { + this.handleError(error, res, 'deleteUser'); + } + } +} +``` + +--- + +## Complete Service with DI + +### UserService + +```typescript +// services/userService.ts +import { UserRepository } from '../repositories/UserRepository'; +import { ConflictError, NotFoundError, ValidationError } from '../types/errors'; +import type { CreateUserDTO, UpdateUserDTO, User } from '../types/user.types'; + +export class UserService { + private userRepository: UserRepository; + + constructor(userRepository?: UserRepository) { + this.userRepository = userRepository || new UserRepository(); + } + + async findById(id: string): Promise { + return await this.userRepository.findById(id); + } + + async getAll(): Promise { + return await this.userRepository.findActive(); + } + + async create(data: CreateUserDTO): Promise { + // Business rule: validate age + if (data.age < 18) { + throw new ValidationError('User must be 18 or older'); + } + + // Business rule: check email uniqueness + const existing = await this.userRepository.findByEmail(data.email); + if (existing) { + throw new ConflictError('Email already in use'); + } + + // Create user with profile + return await this.userRepository.create({ + email: data.email, + profile: { + create: { + firstName: data.firstName, + lastName: data.lastName, + age: data.age, + }, + }, + }); + } + + async update(id: string, data: UpdateUserDTO): Promise { + // Check exists + const existing = await this.userRepository.findById(id); + if (!existing) { + throw new NotFoundError('User not found'); + } + + // Business rule: email uniqueness if changing + if (data.email && data.email !== existing.email) { + const emailTaken = await this.userRepository.findByEmail(data.email); + if (emailTaken) { + throw new ConflictError('Email already in use'); + } + } + + return await this.userRepository.update(id, data); + } + + async delete(id: string): Promise { + const existing = await this.userRepository.findById(id); + if (!existing) { + throw new NotFoundError('User not found'); + } + + await this.userRepository.delete(id); + } +} +``` + +--- + +## Complete Route File + +### userRoutes.ts + +```typescript +// routes/userRoutes.ts +import { Router } from 'express'; +import { UserController } from '../controllers/UserController'; +import { SSOMiddlewareClient } from '../middleware/SSOMiddleware'; +import { auditMiddleware } from '../middleware/auditMiddleware'; + +const router = Router(); +const controller = new UserController(); + +// GET /users - List all users +router.get('/', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.listUsers(req, res) +); + +// GET /users/:id - Get single user +router.get('/:id', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.getUser(req, res) +); + +// POST /users - Create user +router.post('/', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.createUser(req, res) +); + +// PUT /users/:id - Update user +router.put('/:id', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.updateUser(req, res) +); + +// DELETE /users/:id - Delete user +router.delete('/:id', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.deleteUser(req, res) +); + +export default router; +``` + +--- + +## Complete Repository + +### UserRepository + +```typescript +// repositories/UserRepository.ts +import { PrismaService } from '@project-lifecycle-portal/database'; +import type { User, Prisma } from '@prisma/client'; + +export class UserRepository { + async findById(id: string): Promise { + return PrismaService.main.user.findUnique({ + where: { id }, + include: { profile: true }, + }); + } + + async findByEmail(email: string): Promise { + return PrismaService.main.user.findUnique({ + where: { email }, + include: { profile: true }, + }); + } + + async findActive(): Promise { + return PrismaService.main.user.findMany({ + where: { isActive: true }, + include: { profile: true }, + orderBy: { createdAt: 'desc' }, + }); + } + + async create(data: Prisma.UserCreateInput): Promise { + return PrismaService.main.user.create({ + data, + include: { profile: true }, + }); + } + + async update(id: string, data: Prisma.UserUpdateInput): Promise { + return PrismaService.main.user.update({ + where: { id }, + data, + include: { profile: true }, + }); + } + + async delete(id: string): Promise { + // Soft delete + return PrismaService.main.user.update({ + where: { id }, + data: { + isActive: false, + deletedAt: new Date(), + }, + }); + } +} +``` + +--- + +## Refactoring Example: Bad to Good + +### BEFORE: Business Logic in Routes โŒ + +```typescript +// routes/postRoutes.ts (BAD - 200+ lines) +router.post('/posts', async (req, res) => { + try { + const username = res.locals.claims.preferred_username; + const responses = req.body.responses; + const stepInstanceId = req.body.stepInstanceId; + + // โŒ Permission check in route + const userId = await userProfileService.getProfileByEmail(username).then(p => p.id); + const canComplete = await permissionService.canCompleteStep(userId, stepInstanceId); + if (!canComplete) { + return res.status(403).json({ error: 'No permission' }); + } + + // โŒ Business logic in route + const post = await postRepository.create({ + title: req.body.title, + content: req.body.content, + authorId: userId + }); + + // โŒ More business logic... + if (res.locals.isImpersonating) { + impersonationContextStore.storeContext(...); + } + + // ... 100+ more lines + + res.json({ success: true, data: result }); + } catch (e) { + handler.handleException(res, e); + } +}); +``` + +### AFTER: Clean Separation โœ… + +**1. Clean Route:** +```typescript +// routes/postRoutes.ts +import { PostController } from '../controllers/PostController'; + +const router = Router(); +const controller = new PostController(); + +// โœ… CLEAN: 8 lines total! +router.post('/', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.createPost(req, res) +); + +export default router; +``` + +**2. Controller:** +```typescript +// controllers/PostController.ts +export class PostController extends BaseController { + private postService: PostService; + + constructor() { + super(); + this.postService = new PostService(); + } + + async createPost(req: Request, res: Response): Promise { + try { + const validated = createPostSchema.parse({ + ...req.body, + }); + + const result = await this.postService.createPost( + validated, + res.locals.userId + ); + + this.handleSuccess(res, result, 'Post created successfully'); + } catch (error) { + this.handleError(error, res, 'createPost'); + } + } +} +``` + +**3. Service:** +```typescript +// services/postService.ts +export class PostService { + async createPost( + data: CreatePostDTO, + userId: string + ): Promise { + // Permission check + const canComplete = await permissionService.canCompleteStep( + userId, + data.stepInstanceId + ); + + if (!canComplete) { + throw new ForbiddenError('No permission to complete step'); + } + + // Execute workflow + const engine = await createWorkflowEngine(); + const command = new CompleteStepCommand( + data.stepInstanceId, + userId, + data.responses + ); + const events = await engine.executeCommand(command); + + // Handle impersonation + if (context.isImpersonating) { + await this.handleImpersonation(data.stepInstanceId, context); + } + + return { events, success: true }; + } + + private async handleImpersonation(stepInstanceId: number, context: any) { + impersonationContextStore.storeContext(stepInstanceId, { + originalUserId: context.originalUserId, + effectiveUserId: context.effectiveUserId, + }); + } +} +``` + +**Result:** +- Route: 8 lines (was 200+) +- Controller: 25 lines +- Service: 40 lines +- **Testable, maintainable, reusable!** + +--- + +## End-to-End Feature Example + +### Complete User Management Feature + +**1. Types:** +```typescript +// types/user.types.ts +export interface User { + id: string; + email: string; + isActive: boolean; + profile?: UserProfile; +} + +export interface CreateUserDTO { + email: string; + firstName: string; + lastName: string; + age: number; +} + +export interface UpdateUserDTO { + email?: string; + firstName?: string; + lastName?: string; +} +``` + +**2. Validators:** +```typescript +// validators/userSchemas.ts +import { z } from 'zod'; + +export const createUserSchema = z.object({ + email: z.string().email(), + firstName: z.string().min(1).max(100), + lastName: z.string().min(1).max(100), + age: z.number().int().min(18).max(120), +}); + +export const updateUserSchema = z.object({ + email: z.string().email().optional(), + firstName: z.string().min(1).max(100).optional(), + lastName: z.string().min(1).max(100).optional(), +}); +``` + +**3. Repository:** +```typescript +// repositories/UserRepository.ts +export class UserRepository { + async findById(id: string): Promise { + return PrismaService.main.user.findUnique({ + where: { id }, + include: { profile: true }, + }); + } + + async create(data: Prisma.UserCreateInput): Promise { + return PrismaService.main.user.create({ + data, + include: { profile: true }, + }); + } +} +``` + +**4. Service:** +```typescript +// services/userService.ts +export class UserService { + private userRepository: UserRepository; + + constructor() { + this.userRepository = new UserRepository(); + } + + async create(data: CreateUserDTO): Promise { + const existing = await this.userRepository.findByEmail(data.email); + if (existing) { + throw new ConflictError('Email already exists'); + } + + return await this.userRepository.create({ + email: data.email, + profile: { + create: { + firstName: data.firstName, + lastName: data.lastName, + age: data.age, + }, + }, + }); + } +} +``` + +**5. Controller:** +```typescript +// controllers/UserController.ts +export class UserController extends BaseController { + private userService: UserService; + + constructor() { + super(); + this.userService = new UserService(); + } + + async createUser(req: Request, res: Response): Promise { + try { + const validated = createUserSchema.parse(req.body); + const user = await this.userService.create(validated); + this.handleSuccess(res, user, 'User created', 201); + } catch (error) { + this.handleError(error, res, 'createUser'); + } + } +} +``` + +**6. Routes:** +```typescript +// routes/userRoutes.ts +const router = Router(); +const controller = new UserController(); + +router.post('/', + SSOMiddlewareClient.verifyLoginStatus, + async (req, res) => controller.createUser(req, res) +); + +export default router; +``` + +**7. Register in app.ts:** +```typescript +// app.ts +import userRoutes from './routes/userRoutes'; + +app.use('/api/users', userRoutes); +``` + +**Complete Request Flow:** +``` +POST /api/users + โ†“ +userRoutes matches / + โ†“ +SSOMiddleware authenticates + โ†“ +controller.createUser called + โ†“ +Validates with Zod + โ†“ +userService.create called + โ†“ +Checks business rules + โ†“ +userRepository.create called + โ†“ +Prisma creates user + โ†“ +Returns up the chain + โ†“ +Controller formats response + โ†“ +200/201 sent to client +``` + +--- + +**Related Files:** +- SKILL.md +- [routing-and-controllers.md](routing-and-controllers.md) +- [services-and-repositories.md](services-and-repositories.md) +- [validation-patterns.md](validation-patterns.md) diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/configuration.md b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/configuration.md new file mode 100644 index 00000000..bba45d64 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/configuration.md @@ -0,0 +1,275 @@ +# Configuration Management - UnifiedConfig Pattern + +Complete guide to managing configuration in backend microservices. + +## Table of Contents + +- [UnifiedConfig Overview](#unifiedconfig-overview) +- [NEVER Use process.env Directly](#never-use-processenv-directly) +- [Configuration Structure](#configuration-structure) +- [Environment-Specific Configs](#environment-specific-configs) +- [Secrets Management](#secrets-management) +- [Migration Guide](#migration-guide) + +--- + +## UnifiedConfig Overview + +### Why UnifiedConfig? + +**Problems with process.env:** +- โŒ No type safety +- โŒ No validation +- โŒ Hard to test +- โŒ Scattered throughout code +- โŒ No default values +- โŒ Runtime errors for typos + +**Benefits of unifiedConfig:** +- โœ… Type-safe configuration +- โœ… Single source of truth +- โœ… Validated at startup +- โœ… Easy to test with mocks +- โœ… Clear structure +- โœ… Fallback to environment variables + +--- + +## NEVER Use process.env Directly + +### The Rule + +```typescript +// โŒ NEVER DO THIS +const timeout = parseInt(process.env.TIMEOUT_MS || '5000'); +const dbHost = process.env.DB_HOST || 'localhost'; + +// โœ… ALWAYS DO THIS +import { config } from './config/unifiedConfig'; +const timeout = config.timeouts.default; +const dbHost = config.database.host; +``` + +### Why This Matters + +**Example of problems:** +```typescript +// Typo in environment variable name +const host = process.env.DB_HSOT; // undefined! No error! + +// Type safety +const port = process.env.PORT; // string! Need parseInt +const timeout = parseInt(process.env.TIMEOUT); // NaN if not set! +``` + +**With unifiedConfig:** +```typescript +const port = config.server.port; // number, guaranteed +const timeout = config.timeouts.default; // number, with fallback +``` + +--- + +## Configuration Structure + +### UnifiedConfig Interface + +```typescript +export interface UnifiedConfig { + database: { + host: string; + port: number; + username: string; + password: string; + database: string; + }; + server: { + port: number; + sessionSecret: string; + }; + tokens: { + jwt: string; + inactivity: string; + internal: string; + }; + keycloak: { + realm: string; + client: string; + baseUrl: string; + secret: string; + }; + aws: { + region: string; + emailQueueUrl: string; + accessKeyId: string; + secretAccessKey: string; + }; + sentry: { + dsn: string; + environment: string; + tracesSampleRate: number; + }; + // ... more sections +} +``` + +### Implementation Pattern + +**File:** `/blog-api/src/config/unifiedConfig.ts` + +```typescript +import * as fs from 'fs'; +import * as path from 'path'; +import * as ini from 'ini'; + +const configPath = path.join(__dirname, '../../config.ini'); +const iniConfig = ini.parse(fs.readFileSync(configPath, 'utf-8')); + +export const config: UnifiedConfig = { + database: { + host: iniConfig.database?.host || process.env.DB_HOST || 'localhost', + port: parseInt(iniConfig.database?.port || process.env.DB_PORT || '3306'), + username: iniConfig.database?.username || process.env.DB_USER || 'root', + password: iniConfig.database?.password || process.env.DB_PASSWORD || '', + database: iniConfig.database?.database || process.env.DB_NAME || 'blog_dev', + }, + server: { + port: parseInt(iniConfig.server?.port || process.env.PORT || '3002'), + sessionSecret: iniConfig.server?.sessionSecret || process.env.SESSION_SECRET || 'dev-secret', + }, + // ... more configuration +}; + +// Validate critical config +if (!config.tokens.jwt) { + throw new Error('JWT secret not configured!'); +} +``` + +**Key Points:** +- Read from config.ini first +- Fallback to process.env +- Default values for development +- Validation at startup +- Type-safe access + +--- + +## Environment-Specific Configs + +### config.ini Structure + +```ini +[database] +host = localhost +port = 3306 +username = root +password = password1 +database = blog_dev + +[server] +port = 3002 +sessionSecret = your-secret-here + +[tokens] +jwt = your-jwt-secret +inactivity = 30m +internal = internal-api-token + +[keycloak] +realm = myapp +client = myapp-client +baseUrl = http://localhost:8080 +secret = keycloak-client-secret + +[sentry] +dsn = https://your-sentry-dsn +environment = development +tracesSampleRate = 0.1 +``` + +### Environment Overrides + +```bash +# .env file (optional overrides) +DB_HOST=production-db.example.com +DB_PASSWORD=secure-password +PORT=80 +``` + +**Precedence:** +1. config.ini (highest priority) +2. process.env variables +3. Hard-coded defaults (lowest priority) + +--- + +## Secrets Management + +### DO NOT Commit Secrets + +```gitignore +# .gitignore +config.ini +.env +sentry.ini +*.pem +*.key +``` + +### Use Environment Variables in Production + +```typescript +// Development: config.ini +// Production: Environment variables + +export const config: UnifiedConfig = { + database: { + password: process.env.DB_PASSWORD || iniConfig.database?.password || '', + }, + tokens: { + jwt: process.env.JWT_SECRET || iniConfig.tokens?.jwt || '', + }, +}; +``` + +--- + +## Migration Guide + +### Find All process.env Usage + +```bash +grep -r "process.env" blog-api/src/ --include="*.ts" | wc -l +``` + +### Migration Example + +**Before:** +```typescript +// Scattered throughout code +const timeout = parseInt(process.env.OPENID_HTTP_TIMEOUT_MS || '15000'); +const keycloakUrl = process.env.KEYCLOAK_BASE_URL; +const jwtSecret = process.env.JWT_SECRET; +``` + +**After:** +```typescript +import { config } from './config/unifiedConfig'; + +const timeout = config.keycloak.timeout; +const keycloakUrl = config.keycloak.baseUrl; +const jwtSecret = config.tokens.jwt; +``` + +**Benefits:** +- Type-safe +- Centralized +- Easy to test +- Validated at startup + +--- + +**Related Files:** +- SKILL.md +- [testing-guide.md](testing-guide.md) diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/database-patterns.md b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/database-patterns.md new file mode 100644 index 00000000..413445dc --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/database-patterns.md @@ -0,0 +1,224 @@ +# Database Patterns - Prisma Best Practices + +Complete guide to database access patterns using Prisma in backend microservices. + +## Table of Contents + +- [PrismaService Usage](#prismaservice-usage) +- [Repository Pattern](#repository-pattern) +- [Transaction Patterns](#transaction-patterns) +- [Query Optimization](#query-optimization) +- [N+1 Query Prevention](#n1-query-prevention) +- [Error Handling](#error-handling) + +--- + +## PrismaService Usage + +### Basic Pattern + +```typescript +import { PrismaService } from '@project-lifecycle-portal/database'; + +// Always use PrismaService.main +const users = await PrismaService.main.user.findMany(); +``` + +### Check Availability + +```typescript +if (!PrismaService.isAvailable) { + throw new Error('Prisma client not initialized'); +} + +const user = await PrismaService.main.user.findUnique({ where: { id } }); +``` + +--- + +## Repository Pattern + +### Why Use Repositories + +โœ… **Use repositories when:** +- Complex queries with joins/includes +- Query used in multiple places +- Need caching layer +- Want to mock for testing + +โŒ **Skip repositories for:** +- Simple one-off queries +- Prototyping (can refactor later) + +### Repository Template + +```typescript +export class UserRepository { + async findById(id: string): Promise { + return PrismaService.main.user.findUnique({ + where: { id }, + include: { profile: true }, + }); + } + + async findActive(): Promise { + return PrismaService.main.user.findMany({ + where: { isActive: true }, + orderBy: { createdAt: 'desc' }, + }); + } + + async create(data: Prisma.UserCreateInput): Promise { + return PrismaService.main.user.create({ data }); + } +} +``` + +--- + +## Transaction Patterns + +### Simple Transaction + +```typescript +const result = await PrismaService.main.$transaction(async (tx) => { + const user = await tx.user.create({ data: userData }); + const profile = await tx.userProfile.create({ data: { userId: user.id } }); + return { user, profile }; +}); +``` + +### Interactive Transaction + +```typescript +const result = await PrismaService.main.$transaction( + async (tx) => { + const user = await tx.user.findUnique({ where: { id } }); + if (!user) throw new Error('User not found'); + + return await tx.user.update({ + where: { id }, + data: { lastLogin: new Date() }, + }); + }, + { + maxWait: 5000, + timeout: 10000, + } +); +``` + +--- + +## Query Optimization + +### Use select to Limit Fields + +```typescript +// โŒ Fetches all fields +const users = await PrismaService.main.user.findMany(); + +// โœ… Only fetch needed fields +const users = await PrismaService.main.user.findMany({ + select: { + id: true, + email: true, + profile: { select: { firstName: true, lastName: true } }, + }, +}); +``` + +### Use include Carefully + +```typescript +// โŒ Excessive includes +const user = await PrismaService.main.user.findUnique({ + where: { id }, + include: { + profile: true, + posts: { include: { comments: true } }, + workflows: { include: { steps: { include: { actions: true } } } }, + }, +}); + +// โœ… Only include what you need +const user = await PrismaService.main.user.findUnique({ + where: { id }, + include: { profile: true }, +}); +``` + +--- + +## N+1 Query Prevention + +### Problem: N+1 Queries + +```typescript +// โŒ N+1 Query Problem +const users = await PrismaService.main.user.findMany(); // 1 query + +for (const user of users) { + // N queries (one per user) + const profile = await PrismaService.main.userProfile.findUnique({ + where: { userId: user.id }, + }); +} +``` + +### Solution: Use include or Batching + +```typescript +// โœ… Single query with include +const users = await PrismaService.main.user.findMany({ + include: { profile: true }, +}); + +// โœ… Or batch query +const userIds = users.map(u => u.id); +const profiles = await PrismaService.main.userProfile.findMany({ + where: { userId: { in: userIds } }, +}); +``` + +--- + +## Error Handling + +### Prisma Error Types + +```typescript +import { Prisma } from '@prisma/client'; + +try { + await PrismaService.main.user.create({ data }); +} catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + // Unique constraint violation + if (error.code === 'P2002') { + throw new ConflictError('Email already exists'); + } + + // Foreign key constraint + if (error.code === 'P2003') { + throw new ValidationError('Invalid reference'); + } + + // Record not found + if (error.code === 'P2025') { + throw new NotFoundError('Record not found'); + } + } + + // Unknown error + Sentry.captureException(error); + throw error; +} +``` + +--- + +**Related Files:** +- SKILL.md +- [services-and-repositories.md](services-and-repositories.md) +- [async-and-errors.md](async-and-errors.md) diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/middleware-guide.md b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/middleware-guide.md new file mode 100644 index 00000000..ae7b0a2d --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/middleware-guide.md @@ -0,0 +1,213 @@ +# Middleware Guide - Express Middleware Patterns + +Complete guide to creating and using middleware in backend microservices. + +## Table of Contents + +- [Authentication Middleware](#authentication-middleware) +- [Audit Middleware with AsyncLocalStorage](#audit-middleware-with-asynclocalstorage) +- [Error Boundary Middleware](#error-boundary-middleware) +- [Validation Middleware](#validation-middleware) +- [Composable Middleware](#composable-middleware) +- [Middleware Ordering](#middleware-ordering) + +--- + +## Authentication Middleware + +### SSOMiddleware Pattern + +**File:** `/form/src/middleware/SSOMiddleware.ts` + +```typescript +export class SSOMiddlewareClient { + static verifyLoginStatus(req: Request, res: Response, next: NextFunction): void { + const token = req.cookies.refresh_token; + + if (!token) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + try { + const decoded = jwt.verify(token, config.tokens.jwt); + res.locals.claims = decoded; + res.locals.effectiveUserId = decoded.sub; + next(); + } catch (error) { + res.status(401).json({ error: 'Invalid token' }); + } + } +} +``` + +--- + +## Audit Middleware with AsyncLocalStorage + +### Excellent Pattern from Blog API + +**File:** `/form/src/middleware/auditMiddleware.ts` + +```typescript +import { AsyncLocalStorage } from 'async_hooks'; + +export interface AuditContext { + userId: string; + userName?: string; + impersonatedBy?: string; + sessionId?: string; + timestamp: Date; + requestId: string; +} + +export const auditContextStorage = new AsyncLocalStorage(); + +export function auditMiddleware(req: Request, res: Response, next: NextFunction): void { + const context: AuditContext = { + userId: res.locals.effectiveUserId || 'anonymous', + userName: res.locals.claims?.preferred_username, + impersonatedBy: res.locals.isImpersonating ? res.locals.originalUserId : undefined, + timestamp: new Date(), + requestId: req.id || uuidv4(), + }; + + auditContextStorage.run(context, () => { + next(); + }); +} + +// Getter for current context +export function getAuditContext(): AuditContext | null { + return auditContextStorage.getStore() || null; +} +``` + +**Benefits:** +- Context propagates through entire request +- No need to pass context through every function +- Automatically available in services, repositories +- Type-safe context access + +**Usage in Services:** +```typescript +import { getAuditContext } from '../middleware/auditMiddleware'; + +async function someOperation() { + const context = getAuditContext(); + console.log('Operation by:', context?.userId); +} +``` + +--- + +## Error Boundary Middleware + +### Comprehensive Error Handler + +**File:** `/form/src/middleware/errorBoundary.ts` + +```typescript +export function errorBoundary( + error: Error, + req: Request, + res: Response, + next: NextFunction +): void { + // Determine status code + const statusCode = getStatusCodeForError(error); + + // Capture to Sentry + Sentry.withScope((scope) => { + scope.setLevel(statusCode >= 500 ? 'error' : 'warning'); + scope.setTag('error_type', error.name); + scope.setContext('error_details', { + message: error.message, + stack: error.stack, + }); + Sentry.captureException(error); + }); + + // User-friendly response + res.status(statusCode).json({ + success: false, + error: { + message: getUserFriendlyMessage(error), + code: error.name, + }, + requestId: Sentry.getCurrentScope().getPropagationContext().traceId, + }); +} + +// Async wrapper +export function asyncErrorWrapper( + handler: (req: Request, res: Response, next: NextFunction) => Promise +) { + return async (req: Request, res: Response, next: NextFunction) => { + try { + await handler(req, res, next); + } catch (error) { + next(error); + } + }; +} +``` + +--- + +## Composable Middleware + +### withAuthAndAudit Pattern + +```typescript +export function withAuthAndAudit(...authMiddleware: any[]) { + return [ + ...authMiddleware, + auditMiddleware, + ]; +} + +// Usage +router.post('/:formID/submit', + ...withAuthAndAudit(SSOMiddlewareClient.verifyLoginStatus), + async (req, res) => controller.submit(req, res) +); +``` + +--- + +## Middleware Ordering + +### Critical Order (Must Follow) + +```typescript +// 1. Sentry request handler (FIRST) +app.use(Sentry.Handlers.requestHandler()); + +// 2. Body parsing +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// 3. Cookie parsing +app.use(cookieParser()); + +// 4. Auth initialization +app.use(SSOMiddleware.initialize()); + +// 5. Routes registered here +app.use('/api/users', userRoutes); + +// 6. Error handler (AFTER routes) +app.use(errorBoundary); + +// 7. Sentry error handler (LAST) +app.use(Sentry.Handlers.errorHandler()); +``` + +**Rule:** Error handlers MUST be registered AFTER all routes! + +--- + +**Related Files:** +- SKILL.md +- [routing-and-controllers.md](routing-and-controllers.md) +- [async-and-errors.md](async-and-errors.md) diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/routing-and-controllers.md b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/routing-and-controllers.md new file mode 100644 index 00000000..ec39a468 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/routing-and-controllers.md @@ -0,0 +1,756 @@ +# Routing and Controllers - Best Practices + +Complete guide to clean route definitions and controller patterns. + +## Table of Contents + +- [Routes: Routing Only](#routes-routing-only) +- [BaseController Pattern](#basecontroller-pattern) +- [Good Examples](#good-examples) +- [Anti-Patterns](#anti-patterns) +- [Refactoring Guide](#refactoring-guide) +- [Error Handling](#error-handling) +- [HTTP Status Codes](#http-status-codes) + +--- + +## Routes: Routing Only + +### The Golden Rule + +**Routes should ONLY:** +- โœ… Define route paths +- โœ… Register middleware +- โœ… Delegate to controllers + +**Routes should NEVER:** +- โŒ Contain business logic +- โŒ Access database directly +- โŒ Implement validation logic (use Zod + controller) +- โŒ Format complex responses +- โŒ Handle complex error scenarios + +### Clean Route Pattern + +```typescript +// routes/userRoutes.ts +import { Router } from 'express'; +import { UserController } from '../controllers/UserController'; +import { SSOMiddlewareClient } from '../middleware/SSOMiddleware'; +import { auditMiddleware } from '../middleware/auditMiddleware'; + +const router = Router(); +const controller = new UserController(); + +// โœ… CLEAN: Route definition only +router.get('/:id', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.getUser(req, res) +); + +router.post('/', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.createUser(req, res) +); + +router.put('/:id', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.updateUser(req, res) +); + +export default router; +``` + +**Key Points:** +- Each route: method, path, middleware chain, controller delegation +- No try-catch needed (controller handles errors) +- Clean, readable, maintainable +- Easy to see all endpoints at a glance + +--- + +## BaseController Pattern + +### Why BaseController? + +**Benefits:** +- Consistent error handling across all controllers +- Automatic Sentry integration +- Standardized response formats +- Reusable helper methods +- Performance tracking utilities +- Logging and breadcrumb helpers + +### BaseController Pattern (Template) + +**File:** `/email/src/controllers/BaseController.ts` + +```typescript +import * as Sentry from '@sentry/node'; +import { Response } from 'express'; + +export abstract class BaseController { + /** + * Handle errors with Sentry integration + */ + protected handleError( + error: unknown, + res: Response, + context: string, + statusCode = 500 + ): void { + Sentry.withScope((scope) => { + scope.setTag('controller', this.constructor.name); + scope.setTag('operation', context); + scope.setUser({ id: res.locals?.claims?.userId }); + + if (error instanceof Error) { + scope.setContext('error_details', { + message: error.message, + stack: error.stack, + }); + } + + Sentry.captureException(error); + }); + + res.status(statusCode).json({ + success: false, + error: { + message: error instanceof Error ? error.message : 'An error occurred', + code: statusCode, + }, + }); + } + + /** + * Handle success responses + */ + protected handleSuccess( + res: Response, + data: T, + message?: string, + statusCode = 200 + ): void { + res.status(statusCode).json({ + success: true, + message, + data, + }); + } + + /** + * Performance tracking wrapper + */ + protected async withTransaction( + name: string, + operation: string, + callback: () => Promise + ): Promise { + return await Sentry.startSpan( + { name, op: operation }, + callback + ); + } + + /** + * Validate required fields + */ + protected validateRequest( + required: string[], + actual: Record, + res: Response + ): boolean { + const missing = required.filter((field) => !actual[field]); + + if (missing.length > 0) { + Sentry.captureMessage( + `Missing required fields: ${missing.join(', ')}`, + 'warning' + ); + + res.status(400).json({ + success: false, + error: { + message: 'Missing required fields', + code: 'VALIDATION_ERROR', + details: { missing }, + }, + }); + return false; + } + return true; + } + + /** + * Logging helpers + */ + protected logInfo(message: string, context?: Record): void { + Sentry.addBreadcrumb({ + category: this.constructor.name, + message, + level: 'info', + data: context, + }); + } + + protected logWarning(message: string, context?: Record): void { + Sentry.captureMessage(message, { + level: 'warning', + tags: { controller: this.constructor.name }, + extra: context, + }); + } + + /** + * Add Sentry breadcrumb + */ + protected addBreadcrumb( + message: string, + category: string, + data?: Record + ): void { + Sentry.addBreadcrumb({ message, category, level: 'info', data }); + } + + /** + * Capture custom metric + */ + protected captureMetric(name: string, value: number, unit: string): void { + Sentry.metrics.gauge(name, value, { unit }); + } +} +``` + +### Using BaseController + +```typescript +// controllers/UserController.ts +import { Request, Response } from 'express'; +import { BaseController } from './BaseController'; +import { UserService } from '../services/userService'; +import { createUserSchema } from '../validators/userSchemas'; + +export class UserController extends BaseController { + private userService: UserService; + + constructor() { + super(); + this.userService = new UserService(); + } + + async getUser(req: Request, res: Response): Promise { + try { + this.addBreadcrumb('Fetching user', 'user_controller', { userId: req.params.id }); + + const user = await this.userService.findById(req.params.id); + + if (!user) { + return this.handleError( + new Error('User not found'), + res, + 'getUser', + 404 + ); + } + + this.handleSuccess(res, user); + } catch (error) { + this.handleError(error, res, 'getUser'); + } + } + + async createUser(req: Request, res: Response): Promise { + try { + // Validate input + const validated = createUserSchema.parse(req.body); + + // Track performance + const user = await this.withTransaction( + 'user.create', + 'db.query', + () => this.userService.create(validated) + ); + + this.handleSuccess(res, user, 'User created successfully', 201); + } catch (error) { + this.handleError(error, res, 'createUser'); + } + } + + async updateUser(req: Request, res: Response): Promise { + try { + const validated = updateUserSchema.parse(req.body); + const user = await this.userService.update(req.params.id, validated); + this.handleSuccess(res, user, 'User updated'); + } catch (error) { + this.handleError(error, res, 'updateUser'); + } + } +} +``` + +**Benefits:** +- Consistent error handling +- Automatic Sentry integration +- Performance tracking +- Clean, readable code +- Easy to test + +--- + +## Good Examples + +### Example 1: Email Notification Routes (Excellent โœ…) + +**File:** `/email/src/routes/notificationRoutes.ts` + +```typescript +import { Router } from 'express'; +import { NotificationController } from '../controllers/NotificationController'; +import { SSOMiddlewareClient } from '../middleware/SSOMiddleware'; + +const router = Router(); +const controller = new NotificationController(); + +// โœ… EXCELLENT: Clean delegation +router.get('/', + SSOMiddlewareClient.verifyLoginStatus, + async (req, res) => controller.getNotifications(req, res) +); + +router.post('/', + SSOMiddlewareClient.verifyLoginStatus, + async (req, res) => controller.createNotification(req, res) +); + +router.put('/:id/read', + SSOMiddlewareClient.verifyLoginStatus, + async (req, res) => controller.markAsRead(req, res) +); + +export default router; +``` + +**What Makes This Excellent:** +- Zero business logic in routes +- Clear middleware chain +- Consistent pattern +- Easy to understand + +### Example 2: Proxy Routes with Validation (Good โœ…) + +**File:** `/form/src/routes/proxyRoutes.ts` + +```typescript +import { z } from 'zod'; + +const createProxySchema = z.object({ + originalUserID: z.string().min(1), + proxyUserID: z.string().min(1), + startsAt: z.string().datetime(), + expiresAt: z.string().datetime(), +}); + +router.post('/', + SSOMiddlewareClient.verifyLoginStatus, + async (req, res) => { + try { + const validated = createProxySchema.parse(req.body); + const proxy = await proxyService.createProxyRelationship(validated); + res.status(201).json({ success: true, data: proxy }); + } catch (error) { + handler.handleException(res, error); + } + } +); +``` + +**What Makes This Good:** +- Zod validation +- Delegates to service +- Proper HTTP status codes +- Error handling + +**Could Be Better:** +- Move validation to controller +- Use BaseController + +--- + +## Anti-Patterns + +### Anti-Pattern 1: Business Logic in Routes (Bad โŒ) + +**File:** `/form/src/routes/responseRoutes.ts` (actual production code) + +```typescript +// โŒ ANTI-PATTERN: 200+ lines of business logic in route +router.post('/:formID/submit', async (req: Request, res: Response) => { + try { + const username = res.locals.claims.preferred_username; + const responses = req.body.responses; + const stepInstanceId = req.body.stepInstanceId; + + // โŒ Permission checking in route + const userId = await userProfileService.getProfileByEmail(username).then(p => p.id); + const canComplete = await permissionService.canCompleteStep(userId, stepInstanceId); + if (!canComplete) { + return res.status(403).json({ error: 'No permission' }); + } + + // โŒ Workflow logic in route + const { createWorkflowEngine, CompleteStepCommand } = require('../workflow/core/WorkflowEngineV3'); + const engine = await createWorkflowEngine(); + const command = new CompleteStepCommand( + stepInstanceId, + userId, + responses, + additionalContext + ); + const events = await engine.executeCommand(command); + + // โŒ Impersonation handling in route + if (res.locals.isImpersonating) { + impersonationContextStore.storeContext(stepInstanceId, { + originalUserId: res.locals.originalUserId, + effectiveUserId: userId, + }); + } + + // โŒ Response processing in route + const post = await PrismaService.main.post.findUnique({ + where: { id: postData.id }, + include: { comments: true }, + }); + + // โŒ Permission check in route + await checkPostPermissions(post, userId); + + // ... 100+ more lines of business logic + + res.json({ success: true, data: result }); + } catch (e) { + handler.handleException(res, e); + } +}); +``` + +**Why This Is Terrible:** +- 200+ lines of business logic +- Hard to test (requires HTTP mocking) +- Hard to reuse (tied to route) +- Mixed responsibilities +- Difficult to debug +- Performance tracking difficult + +### How to Refactor (Step-by-Step) + +**Step 1: Create Controller** + +```typescript +// controllers/PostController.ts +export class PostController extends BaseController { + private postService: PostService; + + constructor() { + super(); + this.postService = new PostService(); + } + + async createPost(req: Request, res: Response): Promise { + try { + const validated = createPostSchema.parse({ + ...req.body, + }); + + const result = await this.postService.createPost( + validated, + res.locals.userId + ); + + this.handleSuccess(res, result, 'Post created successfully'); + } catch (error) { + this.handleError(error, res, 'createPost'); + } + } +} +``` + +**Step 2: Create Service** + +```typescript +// services/postService.ts +export class PostService { + async createPost( + data: CreatePostDTO, + userId: string + ): Promise { + // Permission check + const canCreate = await permissionService.canCreatePost(userId); + if (!canCreate) { + throw new ForbiddenError('No permission to create post'); + } + + // Execute workflow + const engine = await createWorkflowEngine(); + const command = new CompleteStepCommand(/* ... */); + const events = await engine.executeCommand(command); + + // Handle impersonation if needed + if (context.isImpersonating) { + await this.handleImpersonation(data.stepInstanceId, context); + } + + // Synchronize roles + await this.synchronizeRoles(events, userId); + + return { events, success: true }; + } + + private async handleImpersonation(stepInstanceId: number, context: any) { + impersonationContextStore.storeContext(stepInstanceId, { + originalUserId: context.originalUserId, + effectiveUserId: context.effectiveUserId, + }); + } + + private async synchronizeRoles(events: WorkflowEvent[], userId: string) { + // Role synchronization logic + } +} +``` + +**Step 3: Update Route** + +```typescript +// routes/postRoutes.ts +import { PostController } from '../controllers/PostController'; + +const router = Router(); +const controller = new PostController(); + +// โœ… CLEAN: Just routing +router.post('/', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.createPost(req, res) +); +``` + +**Result:** +- Route: 8 lines (was 200+) +- Controller: 25 lines (request handling) +- Service: 50 lines (business logic) +- Testable, reusable, maintainable! + +--- + +## Error Handling + +### Controller Error Handling + +```typescript +async createUser(req: Request, res: Response): Promise { + try { + const result = await this.userService.create(req.body); + this.handleSuccess(res, result, 'User created', 201); + } catch (error) { + // BaseController.handleError automatically: + // - Captures to Sentry with context + // - Sets appropriate status code + // - Returns formatted error response + this.handleError(error, res, 'createUser'); + } +} +``` + +### Custom Error Status Codes + +```typescript +async getUser(req: Request, res: Response): Promise { + try { + const user = await this.userService.findById(req.params.id); + + if (!user) { + // Custom 404 status + return this.handleError( + new Error('User not found'), + res, + 'getUser', + 404 // Custom status code + ); + } + + this.handleSuccess(res, user); + } catch (error) { + this.handleError(error, res, 'getUser'); + } +} +``` + +### Validation Errors + +```typescript +async createUser(req: Request, res: Response): Promise { + try { + const validated = createUserSchema.parse(req.body); + const user = await this.userService.create(validated); + this.handleSuccess(res, user, 'User created', 201); + } catch (error) { + // Zod errors get 400 status + if (error instanceof z.ZodError) { + return this.handleError(error, res, 'createUser', 400); + } + this.handleError(error, res, 'createUser'); + } +} +``` + +--- + +## HTTP Status Codes + +### Standard Codes + +| Code | Use Case | Example | +|------|----------|---------| +| 200 | Success (GET, PUT) | User retrieved, Updated | +| 201 | Created (POST) | User created | +| 204 | No Content (DELETE) | User deleted | +| 400 | Bad Request | Invalid input data | +| 401 | Unauthorized | Not authenticated | +| 403 | Forbidden | No permission | +| 404 | Not Found | Resource doesn't exist | +| 409 | Conflict | Duplicate resource | +| 422 | Unprocessable Entity | Validation failed | +| 500 | Internal Server Error | Unexpected error | + +### Usage Examples + +```typescript +// 200 - Success (default) +this.handleSuccess(res, user); + +// 201 - Created +this.handleSuccess(res, user, 'Created', 201); + +// 400 - Bad Request +this.handleError(error, res, 'operation', 400); + +// 404 - Not Found +this.handleError(new Error('Not found'), res, 'operation', 404); + +// 403 - Forbidden +this.handleError(new ForbiddenError('No permission'), res, 'operation', 403); +``` + +--- + +## Refactoring Guide + +### Identify Routes Needing Refactoring + +**Red Flags:** +- Route file > 100 lines +- Multiple try-catch blocks in one route +- Direct database access (Prisma calls) +- Complex business logic (if statements, loops) +- Permission checks in routes + +**Check your routes:** +```bash +# Find large route files +wc -l form/src/routes/*.ts | sort -n + +# Find routes with Prisma usage +grep -r "PrismaService" form/src/routes/ +``` + +### Refactoring Process + +**1. Extract to Controller:** +```typescript +// Before: Route with logic +router.post('/action', async (req, res) => { + try { + // 50 lines of logic + } catch (e) { + handler.handleException(res, e); + } +}); + +// After: Clean route +router.post('/action', (req, res) => controller.performAction(req, res)); + +// New controller method +async performAction(req: Request, res: Response): Promise { + try { + const result = await this.service.performAction(req.body); + this.handleSuccess(res, result); + } catch (error) { + this.handleError(error, res, 'performAction'); + } +} +``` + +**2. Extract to Service:** +```typescript +// Controller stays thin +async performAction(req: Request, res: Response): Promise { + try { + const validated = actionSchema.parse(req.body); + const result = await this.actionService.execute(validated); + this.handleSuccess(res, result); + } catch (error) { + this.handleError(error, res, 'performAction'); + } +} + +// Service contains business logic +export class ActionService { + async execute(data: ActionDTO): Promise { + // All business logic here + // Permission checks + // Database operations + // Complex transformations + return result; + } +} +``` + +**3. Add Repository (if needed):** +```typescript +// Service calls repository +export class ActionService { + constructor(private actionRepository: ActionRepository) {} + + async execute(data: ActionDTO): Promise { + // Business logic + const entity = await this.actionRepository.findById(data.id); + // More logic + return await this.actionRepository.update(data.id, changes); + } +} + +// Repository handles data access +export class ActionRepository { + async findById(id: number): Promise { + return PrismaService.main.entity.findUnique({ where: { id } }); + } + + async update(id: number, data: Partial): Promise { + return PrismaService.main.entity.update({ where: { id }, data }); + } +} +``` + +--- + +**Related Files:** +- SKILL.md - Main guide +- [services-and-repositories.md](services-and-repositories.md) - Service layer details +- [complete-examples.md](complete-examples.md) - Full refactoring examples diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/sentry-and-monitoring.md b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/sentry-and-monitoring.md new file mode 100644 index 00000000..7888cdd0 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/sentry-and-monitoring.md @@ -0,0 +1,336 @@ +# Sentry Integration and Monitoring + +Complete guide to error tracking and performance monitoring with Sentry v8. + +## Table of Contents + +- [Core Principles](#core-principles) +- [Sentry Initialization](#sentry-initialization) +- [Error Capture Patterns](#error-capture-patterns) +- [Performance Monitoring](#performance-monitoring) +- [Cron Job Monitoring](#cron-job-monitoring) +- [Error Context Best Practices](#error-context-best-practices) +- [Common Mistakes](#common-mistakes) + +--- + +## Core Principles + +**MANDATORY**: All errors MUST be captured to Sentry. No exceptions. + +**ALL ERRORS MUST BE CAPTURED** - Use Sentry v8 with comprehensive error tracking across all services. + +--- + +## Sentry Initialization + +### instrument.ts Pattern + +**Location:** `src/instrument.ts` (MUST be first import in server.ts and all cron jobs) + +**Template for Microservices:** + +```typescript +import * as Sentry from '@sentry/node'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ini from 'ini'; + +const sentryConfigPath = path.join(__dirname, '../sentry.ini'); +const sentryConfig = ini.parse(fs.readFileSync(sentryConfigPath, 'utf-8')); + +Sentry.init({ + dsn: sentryConfig.sentry?.dsn, + environment: process.env.NODE_ENV || 'development', + tracesSampleRate: parseFloat(sentryConfig.sentry?.tracesSampleRate || '0.1'), + profilesSampleRate: parseFloat(sentryConfig.sentry?.profilesSampleRate || '0.1'), + + integrations: [ + ...Sentry.getDefaultIntegrations({}), + Sentry.extraErrorDataIntegration({ depth: 5 }), + Sentry.localVariablesIntegration(), + Sentry.requestDataIntegration({ + include: { + cookies: false, + data: true, + headers: true, + ip: true, + query_string: true, + url: true, + user: { id: true, email: true, username: true }, + }, + }), + Sentry.consoleIntegration(), + Sentry.contextLinesIntegration(), + Sentry.prismaIntegration(), + ], + + beforeSend(event, hint) { + // Filter health checks + if (event.request?.url?.includes('/healthcheck')) { + return null; + } + + // Scrub sensitive headers + if (event.request?.headers) { + delete event.request.headers['authorization']; + delete event.request.headers['cookie']; + } + + // Mask emails for PII + if (event.user?.email) { + event.user.email = event.user.email.replace(/^(.{2}).*(@.*)$/, '$1***$2'); + } + + return event; + }, + + ignoreErrors: [ + /^Invalid JWT/, + /^JWT expired/, + 'NetworkError', + ], +}); + +// Set service context +Sentry.setTags({ + service: 'form', + version: '1.0.1', +}); + +Sentry.setContext('runtime', { + node_version: process.version, + platform: process.platform, +}); +``` + +**Critical Points:** +- PII protection built-in (beforeSend) +- Filter non-critical errors +- Comprehensive integrations +- Prisma instrumentation +- Service-specific tagging + +--- + +## Error Capture Patterns + +### 1. BaseController Pattern + +```typescript +// Use BaseController.handleError +protected handleError(error: unknown, res: Response, context: string, statusCode = 500): void { + Sentry.withScope((scope) => { + scope.setTag('controller', this.constructor.name); + scope.setTag('operation', context); + scope.setUser({ id: res.locals?.claims?.userId }); + Sentry.captureException(error); + }); + + res.status(statusCode).json({ + success: false, + error: { message: error instanceof Error ? error.message : 'Error occurred' } + }); +} +``` + +### 2. Workflow Error Handling + +```typescript +import { SentryHelper } from '../utils/sentryHelper'; + +try { + await businessOperation(); +} catch (error) { + SentryHelper.captureOperationError(error, { + operationType: 'POST_CREATION', + entityId: 123, + userId: 'user-123', + operation: 'createPost', + }); + throw error; +} +``` + +### 3. Service Layer Error Handling + +```typescript +try { + await someOperation(); +} catch (error) { + Sentry.captureException(error, { + tags: { + service: 'form', + operation: 'someOperation' + }, + extra: { + userId: currentUser.id, + entityId: 123 + } + }); + throw error; +} +``` + +--- + +## Performance Monitoring + +### Database Performance Tracking + +```typescript +import { DatabasePerformanceMonitor } from '../utils/databasePerformance'; + +const result = await DatabasePerformanceMonitor.withPerformanceTracking( + 'findMany', + 'UserProfile', + async () => { + return await PrismaService.main.userProfile.findMany({ take: 5 }); + } +); +``` + +### API Endpoint Spans + +```typescript +router.post('/operation', async (req, res) => { + return await Sentry.startSpan({ + name: 'operation.execute', + op: 'http.server', + attributes: { + 'http.method': 'POST', + 'http.route': '/operation' + } + }, async () => { + const result = await performOperation(); + res.json(result); + }); +}); +``` + +--- + +## Cron Job Monitoring + +### Mandatory Pattern + +```typescript +#!/usr/bin/env node +import '../instrument'; // FIRST LINE after shebang +import * as Sentry from '@sentry/node'; + +async function main() { + return await Sentry.startSpan({ + name: 'cron.job-name', + op: 'cron', + attributes: { + 'cron.job': 'job-name', + 'cron.startTime': new Date().toISOString(), + } + }, async () => { + try { + // Cron job logic here + } catch (error) { + Sentry.captureException(error, { + tags: { + 'cron.job': 'job-name', + 'error.type': 'execution_error' + } + }); + console.error('[Cron] Error:', error); + process.exit(1); + } + }); +} + +main().then(() => { + console.log('[Cron] Completed successfully'); + process.exit(0); +}).catch((error) => { + console.error('[Cron] Fatal error:', error); + process.exit(1); +}); +``` + +--- + +## Error Context Best Practices + +### Rich Context Example + +```typescript +Sentry.withScope((scope) => { + // User context + scope.setUser({ + id: user.id, + email: user.email, + username: user.username + }); + + // Tags for filtering + scope.setTag('service', 'form'); + scope.setTag('endpoint', req.path); + scope.setTag('method', req.method); + + // Structured context + scope.setContext('operation', { + type: 'workflow.complete', + workflowId: 123, + stepId: 456 + }); + + // Breadcrumbs for timeline + scope.addBreadcrumb({ + category: 'workflow', + message: 'Starting step completion', + level: 'info', + data: { stepId: 456 } + }); + + Sentry.captureException(error); +}); +``` + +--- + +## Common Mistakes + +```typescript +// โŒ Swallowing errors +try { + await riskyOperation(); +} catch (error) { + // Silent failure +} + +// โŒ Generic error messages +throw new Error('Error occurred'); + +// โŒ Exposing sensitive data +Sentry.captureException(error, { + extra: { password: user.password } // NEVER +}); + +// โŒ Missing async error handling +async function bad() { + fetchData().then(data => processResult(data)); // Unhandled +} + +// โœ… Proper async handling +async function good() { + try { + const data = await fetchData(); + processResult(data); + } catch (error) { + Sentry.captureException(error); + throw error; + } +} +``` + +--- + +**Related Files:** +- SKILL.md +- [routing-and-controllers.md](routing-and-controllers.md) +- [async-and-errors.md](async-and-errors.md) diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/services-and-repositories.md b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/services-and-repositories.md new file mode 100644 index 00000000..3bee4bf7 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/services-and-repositories.md @@ -0,0 +1,789 @@ +# Services and Repositories - Business Logic Layer + +Complete guide to organizing business logic with services and data access with repositories. + +## Table of Contents + +- [Service Layer Overview](#service-layer-overview) +- [Dependency Injection Pattern](#dependency-injection-pattern) +- [Singleton Pattern](#singleton-pattern) +- [Repository Pattern](#repository-pattern) +- [Service Design Principles](#service-design-principles) +- [Caching Strategies](#caching-strategies) +- [Testing Services](#testing-services) + +--- + +## Service Layer Overview + +### Purpose of Services + +**Services contain business logic** - the 'what' and 'why' of your application: + +``` +Controller asks: "Should I do this?" +Service answers: "Yes/No, here's why, and here's what happens" +Repository executes: "Here's the data you requested" +``` + +**Services are responsible for:** +- โœ… Business rules enforcement +- โœ… Orchestrating multiple repositories +- โœ… Transaction management +- โœ… Complex calculations +- โœ… External service integration +- โœ… Business validations + +**Services should NOT:** +- โŒ Know about HTTP (Request/Response) +- โŒ Direct Prisma access (use repositories) +- โŒ Handle route-specific logic +- โŒ Format HTTP responses + +--- + +## Dependency Injection Pattern + +### Why Dependency Injection? + +**Benefits:** +- Easy to test (inject mocks) +- Clear dependencies +- Flexible configuration +- Promotes loose coupling + +### Excellent Example: NotificationService + +**File:** `/blog-api/src/services/NotificationService.ts` + +```typescript +// Define dependencies interface for clarity +export interface NotificationServiceDependencies { + prisma: PrismaClient; + batchingService: BatchingService; + emailComposer: EmailComposer; +} + +// Service with dependency injection +export class NotificationService { + private prisma: PrismaClient; + private batchingService: BatchingService; + private emailComposer: EmailComposer; + private preferencesCache: Map = new Map(); + private CACHE_TTL = (notificationConfig.preferenceCacheTTLMinutes || 5) * 60 * 1000; + + // Dependencies injected via constructor + constructor(dependencies: NotificationServiceDependencies) { + this.prisma = dependencies.prisma; + this.batchingService = dependencies.batchingService; + this.emailComposer = dependencies.emailComposer; + } + + /** + * Create a notification and route it appropriately + */ + async createNotification(params: CreateNotificationParams) { + const { recipientID, type, title, message, link, context = {}, channel = 'both', priority = NotificationPriority.NORMAL } = params; + + try { + // Get template and render content + const template = getNotificationTemplate(type); + const rendered = renderNotificationContent(template, context); + + // Create in-app notification record + const notificationId = await createNotificationRecord({ + instanceId: parseInt(context.instanceId || '0', 10), + template: type, + recipientUserId: recipientID, + channel: channel === 'email' ? 'email' : 'inApp', + contextData: context, + title: finalTitle, + message: finalMessage, + link: finalLink, + }); + + // Route notification based on channel + if (channel === 'email' || channel === 'both') { + await this.routeNotification({ + notificationId, + userId: recipientID, + type, + priority, + title: finalTitle, + message: finalMessage, + link: finalLink, + context, + }); + } + + return notification; + } catch (error) { + ErrorLogger.log(error, { + context: { + '[NotificationService] createNotification': { + type: params.type, + recipientID: params.recipientID, + }, + }, + }); + throw error; + } + } + + /** + * Route notification based on user preferences + */ + private async routeNotification(params: { notificationId: number; userId: string; type: string; priority: NotificationPriority; title: string; message: string; link?: string; context?: Record }) { + // Get user preferences with caching + const preferences = await this.getUserPreferences(params.userId); + + // Check if we should batch or send immediately + if (this.shouldBatchEmail(preferences, params.type, params.priority)) { + await this.batchingService.queueNotificationForBatch({ + notificationId: params.notificationId, + userId: params.userId, + userPreference: preferences, + priority: params.priority, + }); + } else { + // Send immediately via EmailComposer + await this.sendImmediateEmail({ + userId: params.userId, + title: params.title, + message: params.message, + link: params.link, + context: params.context, + type: params.type, + }); + } + } + + /** + * Determine if email should be batched + */ + shouldBatchEmail(preferences: UserPreference, notificationType: string, priority: NotificationPriority): boolean { + // HIGH priority always immediate + if (priority === NotificationPriority.HIGH) { + return false; + } + + // Check batch mode + const batchMode = preferences.emailBatchMode || BatchMode.IMMEDIATE; + return batchMode !== BatchMode.IMMEDIATE; + } + + /** + * Get user preferences with caching + */ + async getUserPreferences(userId: string): Promise { + // Check cache first + const cached = this.preferencesCache.get(userId); + if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) { + return cached.preferences; + } + + const preference = await this.prisma.userPreference.findUnique({ + where: { userID: userId }, + }); + + const finalPreferences = preference || DEFAULT_PREFERENCES; + + // Update cache + this.preferencesCache.set(userId, { + preferences: finalPreferences, + timestamp: Date.now(), + }); + + return finalPreferences; + } +} +``` + +**Usage in Controller:** + +```typescript +// Instantiate with dependencies +const notificationService = new NotificationService({ + prisma: PrismaService.main, + batchingService: new BatchingService(PrismaService.main), + emailComposer: new EmailComposer(), +}); + +// Use in controller +const notification = await notificationService.createNotification({ + recipientID: 'user-123', + type: 'AFRLWorkflowNotification', + context: { workflowName: 'AFRL Monthly Report' }, +}); +``` + +**Key Takeaways:** +- Dependencies passed via constructor +- Clear interface defines required dependencies +- Easy to test (inject mocks) +- Encapsulated caching logic +- Business rules isolated from HTTP + +--- + +## Singleton Pattern + +### When to Use Singletons + +**Use for:** +- Services with expensive initialization +- Services with shared state (caching) +- Services accessed from many places +- Permission services +- Configuration services + +### Example: PermissionService (Singleton) + +**File:** `/blog-api/src/services/permissionService.ts` + +```typescript +import { PrismaClient } from '@prisma/client'; + +class PermissionService { + private static instance: PermissionService; + private prisma: PrismaClient; + private permissionCache: Map = new Map(); + private CACHE_TTL = 5 * 60 * 1000; // 5 minutes + + // Private constructor prevents direct instantiation + private constructor() { + this.prisma = PrismaService.main; + } + + // Get singleton instance + public static getInstance(): PermissionService { + if (!PermissionService.instance) { + PermissionService.instance = new PermissionService(); + } + return PermissionService.instance; + } + + /** + * Check if user can complete a workflow step + */ + async canCompleteStep(userId: string, stepInstanceId: number): Promise { + const cacheKey = `${userId}:${stepInstanceId}`; + + // Check cache + const cached = this.permissionCache.get(cacheKey); + if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) { + return cached.canAccess; + } + + try { + const post = await this.prisma.post.findUnique({ + where: { id: postId }, + include: { + author: true, + comments: { + include: { + user: true, + }, + }, + }, + }); + + if (!post) { + return false; + } + + // Check if user has permission + const canEdit = post.authorId === userId || + await this.isUserAdmin(userId); + + // Cache result + this.permissionCache.set(cacheKey, { + canAccess: isAssigned, + timestamp: Date.now(), + }); + + return isAssigned; + } catch (error) { + console.error('[PermissionService] Error checking step permission:', error); + return false; + } + } + + /** + * Clear cache for user + */ + clearUserCache(userId: string): void { + for (const [key] of this.permissionCache) { + if (key.startsWith(`${userId}:`)) { + this.permissionCache.delete(key); + } + } + } + + /** + * Clear all cache + */ + clearCache(): void { + this.permissionCache.clear(); + } +} + +// Export singleton instance +export const permissionService = PermissionService.getInstance(); +``` + +**Usage:** + +```typescript +import { permissionService } from '../services/permissionService'; + +// Use anywhere in the codebase +const canComplete = await permissionService.canCompleteStep(userId, stepId); + +if (!canComplete) { + throw new ForbiddenError('You do not have permission to complete this step'); +} +``` + +--- + +## Repository Pattern + +### Purpose of Repositories + +**Repositories abstract data access** - the 'how' of data operations: + +``` +Service: "Get me all active users sorted by name" +Repository: "Here's the Prisma query that does that" +``` + +**Repositories are responsible for:** +- โœ… All Prisma operations +- โœ… Query construction +- โœ… Query optimization (select, include) +- โœ… Database error handling +- โœ… Caching database results + +**Repositories should NOT:** +- โŒ Contain business logic +- โŒ Know about HTTP +- โŒ Make decisions (that's service layer) + +### Repository Template + +```typescript +// repositories/UserRepository.ts +import { PrismaService } from '@project-lifecycle-portal/database'; +import type { User, Prisma } from '@project-lifecycle-portal/database'; + +export class UserRepository { + /** + * Find user by ID with optimized query + */ + async findById(userId: string): Promise { + try { + return await PrismaService.main.user.findUnique({ + where: { userID: userId }, + select: { + userID: true, + email: true, + name: true, + isActive: true, + roles: true, + createdAt: true, + updatedAt: true, + }, + }); + } catch (error) { + console.error('[UserRepository] Error finding user by ID:', error); + throw new Error(`Failed to find user: ${userId}`); + } + } + + /** + * Find all active users + */ + async findActive(options?: { orderBy?: Prisma.UserOrderByWithRelationInput }): Promise { + try { + return await PrismaService.main.user.findMany({ + where: { isActive: true }, + orderBy: options?.orderBy || { name: 'asc' }, + select: { + userID: true, + email: true, + name: true, + roles: true, + }, + }); + } catch (error) { + console.error('[UserRepository] Error finding active users:', error); + throw new Error('Failed to find active users'); + } + } + + /** + * Find user by email + */ + async findByEmail(email: string): Promise { + try { + return await PrismaService.main.user.findUnique({ + where: { email }, + }); + } catch (error) { + console.error('[UserRepository] Error finding user by email:', error); + throw new Error(`Failed to find user with email: ${email}`); + } + } + + /** + * Create new user + */ + async create(data: Prisma.UserCreateInput): Promise { + try { + return await PrismaService.main.user.create({ data }); + } catch (error) { + console.error('[UserRepository] Error creating user:', error); + throw new Error('Failed to create user'); + } + } + + /** + * Update user + */ + async update(userId: string, data: Prisma.UserUpdateInput): Promise { + try { + return await PrismaService.main.user.update({ + where: { userID: userId }, + data, + }); + } catch (error) { + console.error('[UserRepository] Error updating user:', error); + throw new Error(`Failed to update user: ${userId}`); + } + } + + /** + * Delete user (soft delete by setting isActive = false) + */ + async delete(userId: string): Promise { + try { + return await PrismaService.main.user.update({ + where: { userID: userId }, + data: { isActive: false }, + }); + } catch (error) { + console.error('[UserRepository] Error deleting user:', error); + throw new Error(`Failed to delete user: ${userId}`); + } + } + + /** + * Check if email exists + */ + async emailExists(email: string): Promise { + try { + const count = await PrismaService.main.user.count({ + where: { email }, + }); + return count > 0; + } catch (error) { + console.error('[UserRepository] Error checking email exists:', error); + throw new Error('Failed to check if email exists'); + } + } +} + +// Export singleton instance +export const userRepository = new UserRepository(); +``` + +**Using Repository in Service:** + +```typescript +// services/userService.ts +import { userRepository } from '../repositories/UserRepository'; +import { ConflictError, NotFoundError } from '../utils/errors'; + +export class UserService { + /** + * Create new user with business rules + */ + async createUser(data: { email: string; name: string; roles: string[] }): Promise { + // Business rule: Check if email already exists + const emailExists = await userRepository.emailExists(data.email); + if (emailExists) { + throw new ConflictError('Email already exists'); + } + + // Business rule: Validate roles + const validRoles = ['admin', 'operations', 'user']; + const invalidRoles = data.roles.filter((role) => !validRoles.includes(role)); + if (invalidRoles.length > 0) { + throw new ValidationError(`Invalid roles: ${invalidRoles.join(', ')}`); + } + + // Create user via repository + return await userRepository.create({ + email: data.email, + name: data.name, + roles: data.roles, + isActive: true, + }); + } + + /** + * Get user by ID + */ + async getUser(userId: string): Promise { + const user = await userRepository.findById(userId); + + if (!user) { + throw new NotFoundError(`User not found: ${userId}`); + } + + return user; + } +} +``` + +--- + +## Service Design Principles + +### 1. Single Responsibility + +Each service should have ONE clear purpose: + +```typescript +// โœ… GOOD - Single responsibility +class UserService { + async createUser() {} + async updateUser() {} + async deleteUser() {} +} + +class EmailService { + async sendEmail() {} + async sendBulkEmails() {} +} + +// โŒ BAD - Too many responsibilities +class UserService { + async createUser() {} + async sendWelcomeEmail() {} // Should be EmailService + async logUserActivity() {} // Should be AuditService + async processPayment() {} // Should be PaymentService +} +``` + +### 2. Clear Method Names + +Method names should describe WHAT they do: + +```typescript +// โœ… GOOD - Clear intent +async createNotification() +async getUserPreferences() +async shouldBatchEmail() +async routeNotification() + +// โŒ BAD - Vague or misleading +async process() +async handle() +async doIt() +async execute() +``` + +### 3. Return Types + +Always use explicit return types: + +```typescript +// โœ… GOOD - Explicit types +async createUser(data: CreateUserDTO): Promise {} +async findUsers(): Promise {} +async deleteUser(id: string): Promise {} + +// โŒ BAD - Implicit any +async createUser(data) {} // No types! +``` + +### 4. Error Handling + +Services should throw meaningful errors: + +```typescript +// โœ… GOOD - Meaningful errors +if (!user) { + throw new NotFoundError(`User not found: ${userId}`); +} + +if (emailExists) { + throw new ConflictError('Email already exists'); +} + +// โŒ BAD - Generic errors +if (!user) { + throw new Error('Error'); // What error? +} +``` + +### 5. Avoid God Services + +Don't create services that do everything: + +```typescript +// โŒ BAD - God service +class WorkflowService { + async startWorkflow() {} + async completeStep() {} + async assignRoles() {} + async sendNotifications() {} // Should be NotificationService + async validatePermissions() {} // Should be PermissionService + async logAuditTrail() {} // Should be AuditService + // ... 50 more methods +} + +// โœ… GOOD - Focused services +class WorkflowService { + constructor( + private notificationService: NotificationService, + private permissionService: PermissionService, + private auditService: AuditService + ) {} + + async startWorkflow() { + // Orchestrate other services + await this.permissionService.checkPermission(); + await this.workflowRepository.create(); + await this.notificationService.notify(); + await this.auditService.log(); + } +} +``` + +--- + +## Caching Strategies + +### 1. In-Memory Caching + +```typescript +class UserService { + private cache: Map = new Map(); + private CACHE_TTL = 5 * 60 * 1000; // 5 minutes + + async getUser(userId: string): Promise { + // Check cache + const cached = this.cache.get(userId); + if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) { + return cached.user; + } + + // Fetch from database + const user = await userRepository.findById(userId); + + // Update cache + if (user) { + this.cache.set(userId, { user, timestamp: Date.now() }); + } + + return user; + } + + clearUserCache(userId: string): void { + this.cache.delete(userId); + } +} +``` + +### 2. Cache Invalidation + +```typescript +class UserService { + async updateUser(userId: string, data: UpdateUserDTO): Promise { + // Update in database + const user = await userRepository.update(userId, data); + + // Invalidate cache + this.clearUserCache(userId); + + return user; + } +} +``` + +--- + +## Testing Services + +### Unit Tests + +```typescript +// tests/userService.test.ts +import { UserService } from '../services/userService'; +import { userRepository } from '../repositories/UserRepository'; +import { ConflictError } from '../utils/errors'; + +// Mock repository +jest.mock('../repositories/UserRepository'); + +describe('UserService', () => { + let userService: UserService; + + beforeEach(() => { + userService = new UserService(); + jest.clearAllMocks(); + }); + + describe('createUser', () => { + it('should create user when email does not exist', async () => { + // Arrange + const userData = { + email: 'test@example.com', + name: 'Test User', + roles: ['user'], + }; + + (userRepository.emailExists as jest.Mock).mockResolvedValue(false); + (userRepository.create as jest.Mock).mockResolvedValue({ + userID: '123', + ...userData, + }); + + // Act + const user = await userService.createUser(userData); + + // Assert + expect(user).toBeDefined(); + expect(user.email).toBe(userData.email); + expect(userRepository.emailExists).toHaveBeenCalledWith(userData.email); + expect(userRepository.create).toHaveBeenCalled(); + }); + + it('should throw ConflictError when email exists', async () => { + // Arrange + const userData = { + email: 'existing@example.com', + name: 'Test User', + roles: ['user'], + }; + + (userRepository.emailExists as jest.Mock).mockResolvedValue(true); + + // Act & Assert + await expect(userService.createUser(userData)).rejects.toThrow(ConflictError); + expect(userRepository.create).not.toHaveBeenCalled(); + }); + }); +}); +``` + +--- + +**Related Files:** +- SKILL.md - Main guide +- [routing-and-controllers.md](routing-and-controllers.md) - Controllers that use services +- [database-patterns.md](database-patterns.md) - Prisma and repository patterns +- [complete-examples.md](complete-examples.md) - Full service/repository examples diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/testing-guide.md b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/testing-guide.md new file mode 100644 index 00000000..5e468bfe --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/testing-guide.md @@ -0,0 +1,235 @@ +# Testing Guide - Backend Testing Strategies + +Complete guide to testing backend services with Jest and best practices. + +## Table of Contents + +- [Unit Testing](#unit-testing) +- [Integration Testing](#integration-testing) +- [Mocking Strategies](#mocking-strategies) +- [Test Data Management](#test-data-management) +- [Testing Authenticated Routes](#testing-authenticated-routes) +- [Coverage Targets](#coverage-targets) + +--- + +## Unit Testing + +### Test Structure + +```typescript +// services/userService.test.ts +import { UserService } from './userService'; +import { UserRepository } from '../repositories/UserRepository'; + +jest.mock('../repositories/UserRepository'); + +describe('UserService', () => { + let service: UserService; + let mockRepository: jest.Mocked; + + beforeEach(() => { + mockRepository = { + findByEmail: jest.fn(), + create: jest.fn(), + } as any; + + service = new UserService(); + (service as any).userRepository = mockRepository; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('create', () => { + it('should throw error if email exists', async () => { + mockRepository.findByEmail.mockResolvedValue({ id: '123' } as any); + + await expect( + service.create({ email: 'test@test.com' }) + ).rejects.toThrow('Email already in use'); + }); + + it('should create user if email is unique', async () => { + mockRepository.findByEmail.mockResolvedValue(null); + mockRepository.create.mockResolvedValue({ id: '123' } as any); + + const user = await service.create({ + email: 'test@test.com', + firstName: 'John', + lastName: 'Doe', + }); + + expect(user).toBeDefined(); + expect(mockRepository.create).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'test@test.com' + }) + ); + }); + }); +}); +``` + +--- + +## Integration Testing + +### Test with Real Database + +```typescript +import { PrismaService } from '@project-lifecycle-portal/database'; + +describe('UserService Integration', () => { + let testUser: any; + + beforeAll(async () => { + // Create test data + testUser = await PrismaService.main.user.create({ + data: { + email: 'test@test.com', + profile: { create: { firstName: 'Test', lastName: 'User' } }, + }, + }); + }); + + afterAll(async () => { + // Cleanup + await PrismaService.main.user.delete({ where: { id: testUser.id } }); + }); + + it('should find user by email', async () => { + const user = await userService.findByEmail('test@test.com'); + expect(user).toBeDefined(); + expect(user?.email).toBe('test@test.com'); + }); +}); +``` + +--- + +## Mocking Strategies + +### Mock PrismaService + +```typescript +jest.mock('@project-lifecycle-portal/database', () => ({ + PrismaService: { + main: { + user: { + findMany: jest.fn(), + findUnique: jest.fn(), + create: jest.fn(), + update: jest.fn(), + }, + }, + isAvailable: true, + }, +})); +``` + +### Mock Services + +```typescript +const mockUserService = { + findById: jest.fn(), + create: jest.fn(), + update: jest.fn(), +} as jest.Mocked; +``` + +--- + +## Test Data Management + +### Setup and Teardown + +```typescript +describe('PermissionService', () => { + let instanceId: number; + + beforeAll(async () => { + // Create test post + const post = await PrismaService.main.post.create({ + data: { title: 'Test Post', content: 'Test', authorId: 'test-user' }, + }); + instanceId = post.id; + }); + + afterAll(async () => { + // Cleanup + await PrismaService.main.post.delete({ + where: { id: instanceId }, + }); + }); + + beforeEach(() => { + // Clear caches + permissionService.clearCache(); + }); + + it('should check permissions', async () => { + const hasPermission = await permissionService.checkPermission( + 'user-id', + instanceId, + 'VIEW_WORKFLOW' + ); + expect(hasPermission).toBeDefined(); + }); +}); +``` + +--- + +## Testing Authenticated Routes + +### Using test-auth-route.js + +```bash +# Test authenticated endpoint +node scripts/test-auth-route.js http://localhost:3002/form/api/users + +# Test with POST data +node scripts/test-auth-route.js http://localhost:3002/form/api/users POST '{"email":"test@test.com"}' +``` + +### Mock Authentication in Tests + +```typescript +// Mock auth middleware +jest.mock('../middleware/SSOMiddleware', () => ({ + SSOMiddlewareClient: { + verifyLoginStatus: (req, res, next) => { + res.locals.claims = { + sub: 'test-user-id', + preferred_username: 'testuser', + }; + next(); + }, + }, +})); +``` + +--- + +## Coverage Targets + +### Recommended Coverage + +- **Unit Tests**: 70%+ coverage +- **Integration Tests**: Critical paths covered +- **E2E Tests**: Happy paths covered + +### Run Coverage + +```bash +npm test -- --coverage +``` + +--- + +**Related Files:** +- SKILL.md +- [services-and-repositories.md](services-and-repositories.md) +- [complete-examples.md](complete-examples.md) diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/validation-patterns.md b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/validation-patterns.md new file mode 100644 index 00000000..8d941d14 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/backend-dev-guidelines/resources/validation-patterns.md @@ -0,0 +1,754 @@ +# Validation Patterns - Input Validation with Zod + +Complete guide to input validation using Zod schemas for type-safe validation. + +## Table of Contents + +- [Why Zod?](#why-zod) +- [Basic Zod Patterns](#basic-zod-patterns) +- [Schema Examples from Codebase](#schema-examples-from-codebase) +- [Route-Level Validation](#route-level-validation) +- [Controller Validation](#controller-validation) +- [DTO Pattern](#dto-pattern) +- [Error Handling](#error-handling) +- [Advanced Patterns](#advanced-patterns) + +--- + +## Why Zod? + +### Benefits Over Joi/Other Libraries + +**Type Safety:** +- โœ… Full TypeScript inference +- โœ… Runtime + compile-time validation +- โœ… Automatic type generation + +**Developer Experience:** +- โœ… Intuitive API +- โœ… Composable schemas +- โœ… Excellent error messages + +**Performance:** +- โœ… Fast validation +- โœ… Small bundle size +- โœ… Tree-shakeable + +### Migration from Joi + +Modern validation uses Zod instead of Joi: + +```typescript +// โŒ OLD - Joi (being phased out) +const schema = Joi.object({ + email: Joi.string().email().required(), + name: Joi.string().min(3).required(), +}); + +// โœ… NEW - Zod (preferred) +const schema = z.object({ + email: z.string().email(), + name: z.string().min(3), +}); +``` + +--- + +## Basic Zod Patterns + +### Primitive Types + +```typescript +import { z } from 'zod'; + +// Strings +const nameSchema = z.string(); +const emailSchema = z.string().email(); +const urlSchema = z.string().url(); +const uuidSchema = z.string().uuid(); +const minLengthSchema = z.string().min(3); +const maxLengthSchema = z.string().max(100); + +// Numbers +const ageSchema = z.number().int().positive(); +const priceSchema = z.number().positive(); +const rangeSchema = z.number().min(0).max(100); + +// Booleans +const activeSchema = z.boolean(); + +// Dates +const dateSchema = z.string().datetime(); // ISO 8601 string +const nativeDateSchema = z.date(); // Native Date object + +// Enums +const roleSchema = z.enum(['admin', 'operations', 'user']); +const statusSchema = z.enum(['PENDING', 'APPROVED', 'REJECTED']); +``` + +### Objects + +```typescript +// Simple object +const userSchema = z.object({ + email: z.string().email(), + name: z.string(), + age: z.number().int().positive(), +}); + +// Nested objects +const addressSchema = z.object({ + street: z.string(), + city: z.string(), + zipCode: z.string().regex(/^\d{5}$/), +}); + +const userWithAddressSchema = z.object({ + name: z.string(), + address: addressSchema, +}); + +// Optional fields +const userSchema = z.object({ + name: z.string(), + email: z.string().email().optional(), + phone: z.string().optional(), +}); + +// Nullable fields +const userSchema = z.object({ + name: z.string(), + middleName: z.string().nullable(), +}); +``` + +### Arrays + +```typescript +// Array of primitives +const rolesSchema = z.array(z.string()); +const numbersSchema = z.array(z.number()); + +// Array of objects +const usersSchema = z.array( + z.object({ + id: z.string(), + name: z.string(), + }) +); + +// Array with constraints +const tagsSchema = z.array(z.string()).min(1).max(10); +const nonEmptyArray = z.array(z.string()).nonempty(); +``` + +--- + +## Schema Examples from Codebase + +### Form Validation Schemas + +**File:** `/form/src/helpers/zodSchemas.ts` + +```typescript +import { z } from 'zod'; + +// Question types enum +export const questionTypeSchema = z.enum([ + 'input', + 'textbox', + 'editor', + 'dropdown', + 'autocomplete', + 'checkbox', + 'radio', + 'upload', +]); + +// Upload types +export const uploadTypeSchema = z.array( + z.enum(['pdf', 'image', 'excel', 'video', 'powerpoint', 'word']).nullable() +); + +// Input types +export const inputTypeSchema = z + .enum(['date', 'number', 'input', 'currency']) + .nullable(); + +// Question option +export const questionOptionSchema = z.object({ + id: z.number().int().positive().optional(), + controlTag: z.string().max(150).nullable().optional(), + label: z.string().max(100).nullable().optional(), + order: z.number().int().min(0).default(0), +}); + +// Question schema +export const questionSchema = z.object({ + id: z.number().int().positive().optional(), + formID: z.number().int().positive(), + sectionID: z.number().int().positive().optional(), + options: z.array(questionOptionSchema).optional(), + label: z.string().max(500), + description: z.string().max(5000).optional(), + type: questionTypeSchema, + uploadTypes: uploadTypeSchema.optional(), + inputType: inputTypeSchema.optional(), + tags: z.array(z.string().max(150)).optional(), + required: z.boolean(), + isStandard: z.boolean().optional(), + deprecatedKey: z.string().nullable().optional(), + maxLength: z.number().int().positive().nullable().optional(), + isOptionsSorted: z.boolean().optional(), +}); + +// Form section schema +export const formSectionSchema = z.object({ + id: z.number().int().positive(), + formID: z.number().int().positive(), + questions: z.array(questionSchema).optional(), + label: z.string().max(500), + description: z.string().max(5000).optional(), + isStandard: z.boolean(), +}); + +// Create form schema +export const createFormSchema = z.object({ + id: z.number().int().positive(), + label: z.string().max(150), + description: z.string().max(6000).nullable().optional(), + isPhase: z.boolean().optional(), + username: z.string(), +}); + +// Update order schema +export const updateOrderSchema = z.object({ + source: z.object({ + index: z.number().int().min(0), + sectionID: z.number().int().min(0), + }), + destination: z.object({ + index: z.number().int().min(0), + sectionID: z.number().int().min(0), + }), +}); + +// Controller-specific validation schemas +export const createQuestionValidationSchema = z.object({ + formID: z.number().int().positive(), + sectionID: z.number().int().positive(), + question: questionSchema, + index: z.number().int().min(0).nullable().optional(), + username: z.string(), +}); + +export const updateQuestionValidationSchema = z.object({ + questionID: z.number().int().positive(), + username: z.string(), + question: questionSchema, +}); +``` + +### Proxy Relationship Schema + +```typescript +// Proxy relationship validation +const createProxySchema = z.object({ + originalUserID: z.string().min(1), + proxyUserID: z.string().min(1), + startsAt: z.string().datetime(), + expiresAt: z.string().datetime(), +}); + +// With custom validation +const createProxySchemaWithValidation = createProxySchema.refine( + (data) => new Date(data.expiresAt) > new Date(data.startsAt), + { + message: 'expiresAt must be after startsAt', + path: ['expiresAt'], + } +); +``` + +### Workflow Validation + +```typescript +// Workflow start schema +const startWorkflowSchema = z.object({ + workflowCode: z.string().min(1), + entityType: z.enum(['Post', 'User', 'Comment']), + entityID: z.number().int().positive(), + dryRun: z.boolean().optional().default(false), +}); + +// Workflow step completion schema +const completeStepSchema = z.object({ + stepInstanceID: z.number().int().positive(), + answers: z.record(z.string(), z.any()), + dryRun: z.boolean().optional().default(false), +}); +``` + +--- + +## Route-Level Validation + +### Pattern 1: Inline Validation + +```typescript +// routes/proxyRoutes.ts +import { z } from 'zod'; + +const createProxySchema = z.object({ + originalUserID: z.string().min(1), + proxyUserID: z.string().min(1), + startsAt: z.string().datetime(), + expiresAt: z.string().datetime(), +}); + +router.post( + '/', + SSOMiddlewareClient.verifyLoginStatus, + async (req, res) => { + try { + // Validate at route level + const validated = createProxySchema.parse(req.body); + + // Delegate to service + const proxy = await proxyService.createProxyRelationship(validated); + + res.status(201).json({ success: true, data: proxy }); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + success: false, + error: { + message: 'Validation failed', + details: error.errors, + }, + }); + } + handler.handleException(res, error); + } + } +); +``` + +**Pros:** +- Quick and simple +- Good for simple routes + +**Cons:** +- Validation logic in routes +- Harder to test +- Not reusable + +--- + +## Controller Validation + +### Pattern 2: Controller Validation (Recommended) + +```typescript +// validators/userSchemas.ts +import { z } from 'zod'; + +export const createUserSchema = z.object({ + email: z.string().email(), + name: z.string().min(2).max(100), + roles: z.array(z.enum(['admin', 'operations', 'user'])), + isActive: z.boolean().default(true), +}); + +export const updateUserSchema = z.object({ + email: z.string().email().optional(), + name: z.string().min(2).max(100).optional(), + roles: z.array(z.enum(['admin', 'operations', 'user'])).optional(), + isActive: z.boolean().optional(), +}); + +export type CreateUserDTO = z.infer; +export type UpdateUserDTO = z.infer; +``` + +```typescript +// controllers/UserController.ts +import { Request, Response } from 'express'; +import { BaseController } from './BaseController'; +import { UserService } from '../services/userService'; +import { createUserSchema, updateUserSchema } from '../validators/userSchemas'; +import { z } from 'zod'; + +export class UserController extends BaseController { + private userService: UserService; + + constructor() { + super(); + this.userService = new UserService(); + } + + async createUser(req: Request, res: Response): Promise { + try { + // Validate input + const validated = createUserSchema.parse(req.body); + + // Call service + const user = await this.userService.createUser(validated); + + this.handleSuccess(res, user, 'User created successfully', 201); + } catch (error) { + if (error instanceof z.ZodError) { + // Handle validation errors with 400 status + return this.handleError(error, res, 'createUser', 400); + } + this.handleError(error, res, 'createUser'); + } + } + + async updateUser(req: Request, res: Response): Promise { + try { + // Validate params and body + const userId = req.params.id; + const validated = updateUserSchema.parse(req.body); + + const user = await this.userService.updateUser(userId, validated); + + this.handleSuccess(res, user, 'User updated successfully'); + } catch (error) { + if (error instanceof z.ZodError) { + return this.handleError(error, res, 'updateUser', 400); + } + this.handleError(error, res, 'updateUser'); + } + } +} +``` + +**Pros:** +- Clean separation +- Reusable schemas +- Easy to test +- Type-safe DTOs + +**Cons:** +- More files to manage + +--- + +## DTO Pattern + +### Type Inference from Schemas + +```typescript +import { z } from 'zod'; + +// Define schema +const createUserSchema = z.object({ + email: z.string().email(), + name: z.string(), + age: z.number().int().positive(), +}); + +// Infer TypeScript type from schema +type CreateUserDTO = z.infer; + +// Equivalent to: +// type CreateUserDTO = { +// email: string; +// name: string; +// age: number; +// } + +// Use in service +class UserService { + async createUser(data: CreateUserDTO): Promise { + // data is fully typed! + console.log(data.email); // โœ… TypeScript knows this exists + console.log(data.invalid); // โŒ TypeScript error! + } +} +``` + +### Input vs Output Types + +```typescript +// Input schema (what API receives) +const createUserInputSchema = z.object({ + email: z.string().email(), + name: z.string(), + password: z.string().min(8), +}); + +// Output schema (what API returns) +const userOutputSchema = z.object({ + id: z.string().uuid(), + email: z.string().email(), + name: z.string(), + createdAt: z.string().datetime(), + // password excluded! +}); + +type CreateUserInput = z.infer; +type UserOutput = z.infer; +``` + +--- + +## Error Handling + +### Zod Error Format + +```typescript +try { + const validated = schema.parse(data); +} catch (error) { + if (error instanceof z.ZodError) { + console.log(error.errors); + // [ + // { + // code: 'invalid_type', + // expected: 'string', + // received: 'number', + // path: ['email'], + // message: 'Expected string, received number' + // } + // ] + } +} +``` + +### Custom Error Messages + +```typescript +const userSchema = z.object({ + email: z.string().email({ message: 'Please provide a valid email address' }), + name: z.string().min(2, { message: 'Name must be at least 2 characters' }), + age: z.number().int().positive({ message: 'Age must be a positive number' }), +}); +``` + +### Formatted Error Response + +```typescript +// Helper function to format Zod errors +function formatZodError(error: z.ZodError) { + return { + message: 'Validation failed', + errors: error.errors.map((err) => ({ + field: err.path.join('.'), + message: err.message, + code: err.code, + })), + }; +} + +// In controller +catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + success: false, + error: formatZodError(error), + }); + } +} + +// Response example: +// { +// "success": false, +// "error": { +// "message": "Validation failed", +// "errors": [ +// { +// "field": "email", +// "message": "Invalid email", +// "code": "invalid_string" +// } +// ] +// } +// } +``` + +--- + +## Advanced Patterns + +### Conditional Validation + +```typescript +// Validate based on other field values +const submissionSchema = z.object({ + type: z.enum(['NEW', 'UPDATE']), + postId: z.number().optional(), +}).refine( + (data) => { + // If type is UPDATE, postId is required + if (data.type === 'UPDATE') { + return data.postId !== undefined; + } + return true; + }, + { + message: 'postId is required when type is UPDATE', + path: ['postId'], + } +); +``` + +### Transform Data + +```typescript +// Transform strings to numbers +const userSchema = z.object({ + name: z.string(), + age: z.string().transform((val) => parseInt(val, 10)), +}); + +// Transform dates +const eventSchema = z.object({ + name: z.string(), + date: z.string().transform((str) => new Date(str)), +}); +``` + +### Preprocess Data + +```typescript +// Trim strings before validation +const userSchema = z.object({ + email: z.preprocess( + (val) => typeof val === 'string' ? val.trim().toLowerCase() : val, + z.string().email() + ), + name: z.preprocess( + (val) => typeof val === 'string' ? val.trim() : val, + z.string().min(2) + ), +}); +``` + +### Union Types + +```typescript +// Multiple possible types +const idSchema = z.union([z.string(), z.number()]); + +// Discriminated unions +const notificationSchema = z.discriminatedUnion('type', [ + z.object({ + type: z.literal('email'), + recipient: z.string().email(), + subject: z.string(), + }), + z.object({ + type: z.literal('sms'), + phoneNumber: z.string(), + message: z.string(), + }), +]); +``` + +### Recursive Schemas + +```typescript +// For nested structures like trees +type Category = { + id: number; + name: string; + children?: Category[]; +}; + +const categorySchema: z.ZodType = z.lazy(() => + z.object({ + id: z.number(), + name: z.string(), + children: z.array(categorySchema).optional(), + }) +); +``` + +### Schema Composition + +```typescript +// Base schemas +const timestampsSchema = z.object({ + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), +}); + +const auditSchema = z.object({ + createdBy: z.string(), + updatedBy: z.string(), +}); + +// Compose schemas +const userSchema = z.object({ + id: z.string(), + email: z.string().email(), + name: z.string(), +}).merge(timestampsSchema).merge(auditSchema); + +// Extend schemas +const adminUserSchema = userSchema.extend({ + adminLevel: z.number().int().min(1).max(5), + permissions: z.array(z.string()), +}); + +// Pick specific fields +const publicUserSchema = userSchema.pick({ + id: true, + name: true, + // email excluded +}); + +// Omit fields +const userWithoutTimestamps = userSchema.omit({ + createdAt: true, + updatedAt: true, +}); +``` + +### Validation Middleware + +```typescript +// Create reusable validation middleware +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; + +export function validateBody(schema: T) { + return (req: Request, res: Response, next: NextFunction) => { + try { + req.body = schema.parse(req.body); + next(); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + success: false, + error: { + message: 'Validation failed', + details: error.errors, + }, + }); + } + next(error); + } + }; +} + +// Usage +router.post('/users', + validateBody(createUserSchema), + async (req, res) => { + // req.body is validated and typed! + const user = await userService.createUser(req.body); + res.json({ success: true, data: user }); + } +); +``` + +--- + +**Related Files:** +- SKILL.md - Main guide +- [routing-and-controllers.md](routing-and-controllers.md) - Using validation in controllers +- [services-and-repositories.md](services-and-repositories.md) - Using DTOs in services +- [async-and-errors.md](async-and-errors.md) - Error handling patterns diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/database-design/SKILL.md b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/SKILL.md new file mode 100644 index 00000000..5b061c7e --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/SKILL.md @@ -0,0 +1,57 @@ +--- +name: database-design +description: "Database design principles and decision-making. Schema design, indexing strategy, ORM selection, serverless databases." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Database Design + +> **Learn to THINK, not copy SQL patterns.** + +## ๐ŸŽฏ Selective Reading Rule + +**Read ONLY files relevant to the request!** Check the content map, find what you need. + +| File | Description | When to Read | +|------|-------------|--------------| +| `database-selection.md` | PostgreSQL vs Neon vs Turso vs SQLite | Choosing database | +| `orm-selection.md` | Drizzle vs Prisma vs Kysely | Choosing ORM | +| `schema-design.md` | Normalization, PKs, relationships | Designing schema | +| `indexing.md` | Index types, composite indexes | Performance tuning | +| `optimization.md` | N+1, EXPLAIN ANALYZE | Query optimization | +| `migrations.md` | Safe migrations, serverless DBs | Schema changes | + +--- + +## โš ๏ธ Core Principle + +- ASK user for database preferences when unclear +- Choose database/ORM based on CONTEXT +- Don't default to PostgreSQL for everything + +--- + +## Decision Checklist + +Before designing schema: + +- [ ] Asked user about database preference? +- [ ] Chosen database for THIS context? +- [ ] Considered deployment environment? +- [ ] Planned index strategy? +- [ ] Defined relationship types? + +--- + +## Anti-Patterns + +โŒ Default to PostgreSQL for simple apps (SQLite may suffice) +โŒ Skip indexing +โŒ Use SELECT * in production +โŒ Store JSON when structured data is better +โŒ Ignore N+1 queries + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/database-design/database-selection.md b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/database-selection.md new file mode 100644 index 00000000..37582f03 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/database-selection.md @@ -0,0 +1,43 @@ +# Database Selection (2025) + +> Choose database based on context, not default. + +## Decision Tree + +``` +What are your requirements? +โ”‚ +โ”œโ”€โ”€ Full relational features needed +โ”‚ โ”œโ”€โ”€ Self-hosted โ†’ PostgreSQL +โ”‚ โ””โ”€โ”€ Serverless โ†’ Neon, Supabase +โ”‚ +โ”œโ”€โ”€ Edge deployment / Ultra-low latency +โ”‚ โ””โ”€โ”€ Turso (edge SQLite) +โ”‚ +โ”œโ”€โ”€ AI / Vector search +โ”‚ โ””โ”€โ”€ PostgreSQL + pgvector +โ”‚ +โ”œโ”€โ”€ Simple / Embedded / Local +โ”‚ โ””โ”€โ”€ SQLite +โ”‚ +โ””โ”€โ”€ Global distribution + โ””โ”€โ”€ PlanetScale, CockroachDB, Turso +``` + +## Comparison + +| Database | Best For | Trade-offs | +|----------|----------|------------| +| **PostgreSQL** | Full features, complex queries | Needs hosting | +| **Neon** | Serverless PG, branching | PG complexity | +| **Turso** | Edge, low latency | SQLite limitations | +| **SQLite** | Simple, embedded, local | Single-writer | +| **PlanetScale** | MySQL, global scale | No foreign keys | + +## Questions to Ask + +1. What's the deployment environment? +2. How complex are the queries? +3. Is edge/serverless important? +4. Vector search needed? +5. Global distribution required? diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/database-design/indexing.md b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/indexing.md new file mode 100644 index 00000000..a7ed9b82 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/indexing.md @@ -0,0 +1,39 @@ +# Indexing Principles + +> When and how to create indexes effectively. + +## When to Create Indexes + +``` +Index these: +โ”œโ”€โ”€ Columns in WHERE clauses +โ”œโ”€โ”€ Columns in JOIN conditions +โ”œโ”€โ”€ Columns in ORDER BY +โ”œโ”€โ”€ Foreign key columns +โ””โ”€โ”€ Unique constraints + +Don't over-index: +โ”œโ”€โ”€ Write-heavy tables (slower inserts) +โ”œโ”€โ”€ Low-cardinality columns +โ”œโ”€โ”€ Columns rarely queried +``` + +## Index Type Selection + +| Type | Use For | +|------|---------| +| **B-tree** | General purpose, equality & range | +| **Hash** | Equality only, faster | +| **GIN** | JSONB, arrays, full-text | +| **GiST** | Geometric, range types | +| **HNSW/IVFFlat** | Vector similarity (pgvector) | + +## Composite Index Principles + +``` +Order matters for composite indexes: +โ”œโ”€โ”€ Equality columns first +โ”œโ”€โ”€ Range columns last +โ”œโ”€โ”€ Most selective first +โ””โ”€โ”€ Match query pattern +``` diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/database-design/migrations.md b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/migrations.md new file mode 100644 index 00000000..9fc79185 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/migrations.md @@ -0,0 +1,48 @@ +# Migration Principles + +> Safe migration strategy for zero-downtime changes. + +## Safe Migration Strategy + +``` +For zero-downtime changes: +โ”‚ +โ”œโ”€โ”€ Adding column +โ”‚ โ””โ”€โ”€ Add as nullable โ†’ backfill โ†’ add NOT NULL +โ”‚ +โ”œโ”€โ”€ Removing column +โ”‚ โ””โ”€โ”€ Stop using โ†’ deploy โ†’ remove column +โ”‚ +โ”œโ”€โ”€ Adding index +โ”‚ โ””โ”€โ”€ CREATE INDEX CONCURRENTLY (non-blocking) +โ”‚ +โ””โ”€โ”€ Renaming column + โ””โ”€โ”€ Add new โ†’ migrate data โ†’ deploy โ†’ drop old +``` + +## Migration Philosophy + +- Never make breaking changes in one step +- Test migrations on data copy first +- Have rollback plan +- Run in transaction when possible + +## Serverless Databases + +### Neon (Serverless PostgreSQL) + +| Feature | Benefit | +|---------|---------| +| Scale to zero | Cost savings | +| Instant branching | Dev/preview | +| Full PostgreSQL | Compatibility | +| Autoscaling | Traffic handling | + +### Turso (Edge SQLite) + +| Feature | Benefit | +|---------|---------| +| Edge locations | Ultra-low latency | +| SQLite compatible | Simple | +| Generous free tier | Cost | +| Global distribution | Performance | diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/database-design/optimization.md b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/optimization.md new file mode 100644 index 00000000..17c99ac0 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/optimization.md @@ -0,0 +1,36 @@ +# Query Optimization + +> N+1 problem, EXPLAIN ANALYZE, optimization priorities. + +## N+1 Problem + +``` +What is N+1? +โ”œโ”€โ”€ 1 query to get parent records +โ”œโ”€โ”€ N queries to get related records +โ””โ”€โ”€ Very slow! + +Solutions: +โ”œโ”€โ”€ JOIN โ†’ Single query with all data +โ”œโ”€โ”€ Eager loading โ†’ ORM handles JOIN +โ”œโ”€โ”€ DataLoader โ†’ Batch and cache (GraphQL) +โ””โ”€โ”€ Subquery โ†’ Fetch related in one query +``` + +## Query Analysis Mindset + +``` +Before optimizing: +โ”œโ”€โ”€ EXPLAIN ANALYZE the query +โ”œโ”€โ”€ Look for Seq Scan (full table scan) +โ”œโ”€โ”€ Check actual vs estimated rows +โ””โ”€โ”€ Identify missing indexes +``` + +## Optimization Priorities + +1. **Add missing indexes** (most common issue) +2. **Select only needed columns** (not SELECT *) +3. **Use proper JOINs** (avoid subqueries when possible) +4. **Limit early** (pagination at database level) +5. **Cache** (when appropriate) diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/database-design/orm-selection.md b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/orm-selection.md new file mode 100644 index 00000000..5d48b72b --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/orm-selection.md @@ -0,0 +1,30 @@ +# ORM Selection (2025) + +> Choose ORM based on deployment and DX needs. + +## Decision Tree + +``` +What's the context? +โ”‚ +โ”œโ”€โ”€ Edge deployment / Bundle size matters +โ”‚ โ””โ”€โ”€ Drizzle (smallest, SQL-like) +โ”‚ +โ”œโ”€โ”€ Best DX / Schema-first +โ”‚ โ””โ”€โ”€ Prisma (migrations, studio) +โ”‚ +โ”œโ”€โ”€ Maximum control +โ”‚ โ””โ”€โ”€ Raw SQL with query builder +โ”‚ +โ””โ”€โ”€ Python ecosystem + โ””โ”€โ”€ SQLAlchemy 2.0 (async support) +``` + +## Comparison + +| ORM | Best For | Trade-offs | +|-----|----------|------------| +| **Drizzle** | Edge, TypeScript | Newer, less examples | +| **Prisma** | DX, schema management | Heavier, not edge-ready | +| **Kysely** | Type-safe SQL builder | Manual migrations | +| **Raw SQL** | Complex queries, control | Manual type safety | diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/database-design/schema-design.md b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/schema-design.md new file mode 100644 index 00000000..f1cdb3ca --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/schema-design.md @@ -0,0 +1,56 @@ +# Schema Design Principles + +> Normalization, primary keys, timestamps, relationships. + +## Normalization Decision + +``` +When to normalize (separate tables): +โ”œโ”€โ”€ Data is repeated across rows +โ”œโ”€โ”€ Updates would need multiple changes +โ”œโ”€โ”€ Relationships are clear +โ””โ”€โ”€ Query patterns benefit + +When to denormalize (embed/duplicate): +โ”œโ”€โ”€ Read performance critical +โ”œโ”€โ”€ Data rarely changes +โ”œโ”€โ”€ Always fetched together +โ””โ”€โ”€ Simpler queries needed +``` + +## Primary Key Selection + +| Type | Use When | +|------|----------| +| **UUID** | Distributed systems, security | +| **ULID** | UUID + sortable by time | +| **Auto-increment** | Simple apps, single database | +| **Natural key** | Rarely (business meaning) | + +## Timestamp Strategy + +``` +For every table: +โ”œโ”€โ”€ created_at โ†’ When created +โ”œโ”€โ”€ updated_at โ†’ Last modified +โ””โ”€โ”€ deleted_at โ†’ Soft delete (if needed) + +Use TIMESTAMPTZ (with timezone) not TIMESTAMP +``` + +## Relationship Types + +| Type | When | Implementation | +|------|------|----------------| +| **One-to-One** | Extension data | Separate table with FK | +| **One-to-Many** | Parent-children | FK on child table | +| **Many-to-Many** | Both sides have many | Junction table | + +## Foreign Key ON DELETE + +``` +โ”œโ”€โ”€ CASCADE โ†’ Delete children with parent +โ”œโ”€โ”€ SET NULL โ†’ Children become orphans +โ”œโ”€โ”€ RESTRICT โ†’ Prevent delete if children exist +โ””โ”€โ”€ SET DEFAULT โ†’ Children get default value +``` diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/database-design/scripts/schema_validator.py b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/scripts/schema_validator.py new file mode 100644 index 00000000..587604ee --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/database-design/scripts/schema_validator.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +""" +Schema Validator - Database schema validation +Validates Prisma schemas and checks for common issues. + +Usage: + python schema_validator.py + +Checks: + - Prisma schema syntax + - Missing relations + - Index recommendations + - Naming conventions +""" + +import sys +import json +import re +from pathlib import Path +from datetime import datetime + +# Fix Windows console encoding +try: + sys.stdout.reconfigure(encoding='utf-8', errors='replace') +except: + pass + + +def find_schema_files(project_path: Path) -> list: + """Find database schema files.""" + schemas = [] + + # Prisma schema + prisma_files = list(project_path.glob('**/prisma/schema.prisma')) + schemas.extend([('prisma', f) for f in prisma_files]) + + # Drizzle schema files + drizzle_files = list(project_path.glob('**/drizzle/*.ts')) + drizzle_files.extend(project_path.glob('**/schema/*.ts')) + for f in drizzle_files: + if 'schema' in f.name.lower() or 'table' in f.name.lower(): + schemas.append(('drizzle', f)) + + return schemas[:10] # Limit + + +def validate_prisma_schema(file_path: Path) -> list: + """Validate Prisma schema file.""" + issues = [] + + try: + content = file_path.read_text(encoding='utf-8', errors='ignore') + + # Find all models + models = re.findall(r'model\s+(\w+)\s*{([^}]+)}', content, re.DOTALL) + + for model_name, model_body in models: + # Check naming convention (PascalCase) + if not model_name[0].isupper(): + issues.append(f"Model '{model_name}' should be PascalCase") + + # Check for id field + if '@id' not in model_body and 'id' not in model_body.lower(): + issues.append(f"Model '{model_name}' might be missing @id field") + + # Check for createdAt/updatedAt + if 'createdAt' not in model_body and 'created_at' not in model_body: + issues.append(f"Model '{model_name}' missing createdAt field (recommended)") + + # Check for @relation without fields + relations = re.findall(r'@relation\([^)]*\)', model_body) + for rel in relations: + if 'fields:' not in rel and 'references:' not in rel: + pass # Implicit relation, ok + + # Check for @@index suggestions + foreign_keys = re.findall(r'(\w+Id)\s+\w+', model_body) + for fk in foreign_keys: + if f'@@index([{fk}])' not in content and f'@@index(["{fk}"])' not in content: + issues.append(f"Consider adding @@index([{fk}]) for better query performance in {model_name}") + + # Check for enum definitions + enums = re.findall(r'enum\s+(\w+)\s*{', content) + for enum_name in enums: + if not enum_name[0].isupper(): + issues.append(f"Enum '{enum_name}' should be PascalCase") + + except Exception as e: + issues.append(f"Error reading schema: {str(e)[:50]}") + + return issues + + +def main(): + project_path = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve() + + print(f"\n{'='*60}") + print(f"[SCHEMA VALIDATOR] Database Schema Validation") + print(f"{'='*60}") + print(f"Project: {project_path}") + print(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print("-"*60) + + # Find schema files + schemas = find_schema_files(project_path) + print(f"Found {len(schemas)} schema files") + + if not schemas: + output = { + "script": "schema_validator", + "project": str(project_path), + "schemas_checked": 0, + "issues_found": 0, + "passed": True, + "message": "No schema files found" + } + print(json.dumps(output, indent=2)) + sys.exit(0) + + # Validate each schema + all_issues = [] + + for schema_type, file_path in schemas: + print(f"\nValidating: {file_path.name} ({schema_type})") + + if schema_type == 'prisma': + issues = validate_prisma_schema(file_path) + else: + issues = [] # Drizzle validation could be added + + if issues: + all_issues.append({ + "file": str(file_path.name), + "type": schema_type, + "issues": issues + }) + + # Summary + print("\n" + "="*60) + print("SCHEMA ISSUES") + print("="*60) + + if all_issues: + for item in all_issues: + print(f"\n{item['file']} ({item['type']}):") + for issue in item["issues"][:5]: # Limit per file + print(f" - {issue}") + if len(item["issues"]) > 5: + print(f" ... and {len(item['issues']) - 5} more issues") + else: + print("No schema issues found!") + + total_issues = sum(len(item["issues"]) for item in all_issues) + # Schema issues are warnings, not failures + passed = True + + output = { + "script": "schema_validator", + "project": str(project_path), + "schemas_checked": len(schemas), + "issues_found": total_issues, + "passed": passed, + "issues": all_issues + } + + print("\n" + json.dumps(output, indent=2)) + + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/frontend-developer/SKILL.md b/plugins/antigravity-bundle-full-stack-developer/skills/frontend-developer/SKILL.md new file mode 100644 index 00000000..2494e145 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/frontend-developer/SKILL.md @@ -0,0 +1,169 @@ +--- +name: frontend-developer +description: Build React components, implement responsive layouts, and handle client-side state management. Masters React 19, Next.js 15, and modern frontend architecture. +risk: unknown +source: community +date_added: '2026-02-27' +--- +You are a frontend development expert specializing in modern React applications, Next.js, and cutting-edge frontend architecture. + +## Use this skill when + +- Building React or Next.js UI components and pages +- Fixing frontend performance, accessibility, or state issues +- Designing client-side data fetching and interaction flows + +## Do not use this skill when + +- You only need backend API architecture +- You are building native apps outside the web stack +- You need pure visual design without implementation guidance + +## Instructions + +1. Clarify requirements, target devices, and performance goals. +2. Choose component structure and state or data approach. +3. Implement UI with accessibility and responsive behavior. +4. Validate performance and UX with profiling and audits. + +## Purpose +Expert frontend developer specializing in React 19+, Next.js 15+, and modern web application development. Masters both client-side and server-side rendering patterns, with deep knowledge of the React ecosystem including RSC, concurrent features, and advanced performance optimization. + +## Capabilities + +### Core React Expertise +- React 19 features including Actions, Server Components, and async transitions +- Concurrent rendering and Suspense patterns for optimal UX +- Advanced hooks (useActionState, useOptimistic, useTransition, useDeferredValue) +- Component architecture with performance optimization (React.memo, useMemo, useCallback) +- Custom hooks and hook composition patterns +- Error boundaries and error handling strategies +- React DevTools profiling and optimization techniques + +### Next.js & Full-Stack Integration +- Next.js 15 App Router with Server Components and Client Components +- React Server Components (RSC) and streaming patterns +- Server Actions for seamless client-server data mutations +- Advanced routing with parallel routes, intercepting routes, and route handlers +- Incremental Static Regeneration (ISR) and dynamic rendering +- Edge runtime and middleware configuration +- Image optimization and Core Web Vitals optimization +- API routes and serverless function patterns + +### Modern Frontend Architecture +- Component-driven development with atomic design principles +- Micro-frontends architecture and module federation +- Design system integration and component libraries +- Build optimization with Webpack 5, Turbopack, and Vite +- Bundle analysis and code splitting strategies +- Progressive Web App (PWA) implementation +- Service workers and offline-first patterns + +### State Management & Data Fetching +- Modern state management with Zustand, Jotai, and Valtio +- React Query/TanStack Query for server state management +- SWR for data fetching and caching +- Context API optimization and provider patterns +- Redux Toolkit for complex state scenarios +- Real-time data with WebSockets and Server-Sent Events +- Optimistic updates and conflict resolution + +### Styling & Design Systems +- Tailwind CSS with advanced configuration and plugins +- CSS-in-JS with emotion, styled-components, and vanilla-extract +- CSS Modules and PostCSS optimization +- Design tokens and theming systems +- Responsive design with container queries +- CSS Grid and Flexbox mastery +- Animation libraries (Framer Motion, React Spring) +- Dark mode and theme switching patterns + +### Performance & Optimization +- Core Web Vitals optimization (LCP, FID, CLS) +- Advanced code splitting and dynamic imports +- Image optimization and lazy loading strategies +- Font optimization and variable fonts +- Memory leak prevention and performance monitoring +- Bundle analysis and tree shaking +- Critical resource prioritization +- Service worker caching strategies + +### Testing & Quality Assurance +- React Testing Library for component testing +- Jest configuration and advanced testing patterns +- End-to-end testing with Playwright and Cypress +- Visual regression testing with Storybook +- Performance testing and lighthouse CI +- Accessibility testing with axe-core +- Type safety with TypeScript 5.x features + +### Accessibility & Inclusive Design +- WCAG 2.1/2.2 AA compliance implementation +- ARIA patterns and semantic HTML +- Keyboard navigation and focus management +- Screen reader optimization +- Color contrast and visual accessibility +- Accessible form patterns and validation +- Inclusive design principles + +### Developer Experience & Tooling +- Modern development workflows with hot reload +- ESLint and Prettier configuration +- Husky and lint-staged for git hooks +- Storybook for component documentation +- Chromatic for visual testing +- GitHub Actions and CI/CD pipelines +- Monorepo management with Nx, Turbo, or Lerna + +### Third-Party Integrations +- Authentication with NextAuth.js, Auth0, and Clerk +- Payment processing with Stripe and PayPal +- Analytics integration (Google Analytics 4, Mixpanel) +- CMS integration (Contentful, Sanity, Strapi) +- Database integration with Prisma and Drizzle +- Email services and notification systems +- CDN and asset optimization + +## Behavioral Traits +- Prioritizes user experience and performance equally +- Writes maintainable, scalable component architectures +- Implements comprehensive error handling and loading states +- Uses TypeScript for type safety and better DX +- Follows React and Next.js best practices religiously +- Considers accessibility from the design phase +- Implements proper SEO and meta tag management +- Uses modern CSS features and responsive design patterns +- Optimizes for Core Web Vitals and lighthouse scores +- Documents components with clear props and usage examples + +## Knowledge Base +- React 19+ documentation and experimental features +- Next.js 15+ App Router patterns and best practices +- TypeScript 5.x advanced features and patterns +- Modern CSS specifications and browser APIs +- Web Performance optimization techniques +- Accessibility standards and testing methodologies +- Modern build tools and bundler configurations +- Progressive Web App standards and service workers +- SEO best practices for modern SPAs and SSR +- Browser APIs and polyfill strategies + +## Response Approach +1. **Analyze requirements** for modern React/Next.js patterns +2. **Suggest performance-optimized solutions** using React 19 features +3. **Provide production-ready code** with proper TypeScript types +4. **Include accessibility considerations** and ARIA patterns +5. **Consider SEO and meta tag implications** for SSR/SSG +6. **Implement proper error boundaries** and loading states +7. **Optimize for Core Web Vitals** and user experience +8. **Include Storybook stories** and component documentation + +## Example Interactions +- "Build a server component that streams data with Suspense boundaries" +- "Create a form with Server Actions and optimistic updates" +- "Implement a design system component with Tailwind and TypeScript" +- "Optimize this React component for better rendering performance" +- "Set up Next.js middleware for authentication and routing" +- "Create an accessible data table with sorting and filtering" +- "Implement real-time updates with WebSockets and React Query" +- "Build a PWA with offline capabilities and push notifications" diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/SKILL.md b/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/SKILL.md new file mode 100644 index 00000000..cf3efc8b --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/SKILL.md @@ -0,0 +1,215 @@ +--- +name: senior-fullstack +description: "Complete toolkit for senior fullstack with modern tools and best practices." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Senior Fullstack + +Complete toolkit for senior fullstack with modern tools and best practices. + +## Quick Start + +### Main Capabilities + +This skill provides three core capabilities through automated scripts: + +```bash +# Script 1: Fullstack Scaffolder +python scripts/fullstack_scaffolder.py [options] + +# Script 2: Project Scaffolder +python scripts/project_scaffolder.py [options] + +# Script 3: Code Quality Analyzer +python scripts/code_quality_analyzer.py [options] +``` + +## Core Capabilities + +### 1. Fullstack Scaffolder + +Automated tool for fullstack scaffolder tasks. + +**Features:** +- Automated scaffolding +- Best practices built-in +- Configurable templates +- Quality checks + +**Usage:** +```bash +python scripts/fullstack_scaffolder.py [options] +``` + +### 2. Project Scaffolder + +Comprehensive analysis and optimization tool. + +**Features:** +- Deep analysis +- Performance metrics +- Recommendations +- Automated fixes + +**Usage:** +```bash +python scripts/project_scaffolder.py [--verbose] +``` + +### 3. Code Quality Analyzer + +Advanced tooling for specialized tasks. + +**Features:** +- Expert-level automation +- Custom configurations +- Integration ready +- Production-grade output + +**Usage:** +```bash +python scripts/code_quality_analyzer.py [arguments] [options] +``` + +## Reference Documentation + +### Tech Stack Guide + +Comprehensive guide available in `references/tech_stack_guide.md`: + +- Detailed patterns and practices +- Code examples +- Best practices +- Anti-patterns to avoid +- Real-world scenarios + +### Architecture Patterns + +Complete workflow documentation in `references/architecture_patterns.md`: + +- Step-by-step processes +- Optimization strategies +- Tool integrations +- Performance tuning +- Troubleshooting guide + +### Development Workflows + +Technical reference guide in `references/development_workflows.md`: + +- Technology stack details +- Configuration examples +- Integration patterns +- Security considerations +- Scalability guidelines + +## Tech Stack + +**Languages:** TypeScript, JavaScript, Python, Go, Swift, Kotlin +**Frontend:** React, Next.js, React Native, Flutter +**Backend:** Node.js, Express, GraphQL, REST APIs +**Database:** PostgreSQL, Prisma, NeonDB, Supabase +**DevOps:** Docker, Kubernetes, Terraform, GitHub Actions, CircleCI +**Cloud:** AWS, GCP, Azure + +## Development Workflow + +### 1. Setup and Configuration + +```bash +# Install dependencies +npm install +# or +pip install -r requirements.txt + +# Configure environment +cp .env.example .env +``` + +### 2. Run Quality Checks + +```bash +# Use the analyzer script +python scripts/project_scaffolder.py . + +# Review recommendations +# Apply fixes +``` + +### 3. Implement Best Practices + +Follow the patterns and practices documented in: +- `references/tech_stack_guide.md` +- `references/architecture_patterns.md` +- `references/development_workflows.md` + +## Best Practices Summary + +### Code Quality +- Follow established patterns +- Write comprehensive tests +- Document decisions +- Review regularly + +### Performance +- Measure before optimizing +- Use appropriate caching +- Optimize critical paths +- Monitor in production + +### Security +- Validate all inputs +- Use parameterized queries +- Implement proper authentication +- Keep dependencies updated + +### Maintainability +- Write clear code +- Use consistent naming +- Add helpful comments +- Keep it simple + +## Common Commands + +```bash +# Development +npm run dev +npm run build +npm run test +npm run lint + +# Analysis +python scripts/project_scaffolder.py . +python scripts/code_quality_analyzer.py --analyze + +# Deployment +docker build -t app:latest . +docker-compose up -d +kubectl apply -f k8s/ +``` + +## Troubleshooting + +### Common Issues + +Check the comprehensive troubleshooting section in `references/development_workflows.md`. + +### Getting Help + +- Review reference documentation +- Check script output messages +- Consult tech stack documentation +- Review error logs + +## Resources + +- Pattern Reference: `references/tech_stack_guide.md` +- Workflow Guide: `references/architecture_patterns.md` +- Technical Guide: `references/development_workflows.md` +- Tool Scripts: `scripts/` directory + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/references/architecture_patterns.md b/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/references/architecture_patterns.md new file mode 100644 index 00000000..6b049dc6 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/references/architecture_patterns.md @@ -0,0 +1,103 @@ +# Architecture Patterns + +## Overview + +This reference guide provides comprehensive information for senior fullstack. + +## Patterns and Practices + +### Pattern 1: Best Practice Implementation + +**Description:** +Detailed explanation of the pattern. + +**When to Use:** +- Scenario 1 +- Scenario 2 +- Scenario 3 + +**Implementation:** +```typescript +// Example code implementation +export class Example { + // Implementation details +} +``` + +**Benefits:** +- Benefit 1 +- Benefit 2 +- Benefit 3 + +**Trade-offs:** +- Consider 1 +- Consider 2 +- Consider 3 + +### Pattern 2: Advanced Technique + +**Description:** +Another important pattern for senior fullstack. + +**Implementation:** +```typescript +// Advanced example +async function advancedExample() { + // Code here +} +``` + +## Guidelines + +### Code Organization +- Clear structure +- Logical separation +- Consistent naming +- Proper documentation + +### Performance Considerations +- Optimization strategies +- Bottleneck identification +- Monitoring approaches +- Scaling techniques + +### Security Best Practices +- Input validation +- Authentication +- Authorization +- Data protection + +## Common Patterns + +### Pattern A +Implementation details and examples. + +### Pattern B +Implementation details and examples. + +### Pattern C +Implementation details and examples. + +## Anti-Patterns to Avoid + +### Anti-Pattern 1 +What not to do and why. + +### Anti-Pattern 2 +What not to do and why. + +## Tools and Resources + +### Recommended Tools +- Tool 1: Purpose +- Tool 2: Purpose +- Tool 3: Purpose + +### Further Reading +- Resource 1 +- Resource 2 +- Resource 3 + +## Conclusion + +Key takeaways for using this reference guide effectively. diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/references/development_workflows.md b/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/references/development_workflows.md new file mode 100644 index 00000000..03cbf2d9 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/references/development_workflows.md @@ -0,0 +1,103 @@ +# Development Workflows + +## Overview + +This reference guide provides comprehensive information for senior fullstack. + +## Patterns and Practices + +### Pattern 1: Best Practice Implementation + +**Description:** +Detailed explanation of the pattern. + +**When to Use:** +- Scenario 1 +- Scenario 2 +- Scenario 3 + +**Implementation:** +```typescript +// Example code implementation +export class Example { + // Implementation details +} +``` + +**Benefits:** +- Benefit 1 +- Benefit 2 +- Benefit 3 + +**Trade-offs:** +- Consider 1 +- Consider 2 +- Consider 3 + +### Pattern 2: Advanced Technique + +**Description:** +Another important pattern for senior fullstack. + +**Implementation:** +```typescript +// Advanced example +async function advancedExample() { + // Code here +} +``` + +## Guidelines + +### Code Organization +- Clear structure +- Logical separation +- Consistent naming +- Proper documentation + +### Performance Considerations +- Optimization strategies +- Bottleneck identification +- Monitoring approaches +- Scaling techniques + +### Security Best Practices +- Input validation +- Authentication +- Authorization +- Data protection + +## Common Patterns + +### Pattern A +Implementation details and examples. + +### Pattern B +Implementation details and examples. + +### Pattern C +Implementation details and examples. + +## Anti-Patterns to Avoid + +### Anti-Pattern 1 +What not to do and why. + +### Anti-Pattern 2 +What not to do and why. + +## Tools and Resources + +### Recommended Tools +- Tool 1: Purpose +- Tool 2: Purpose +- Tool 3: Purpose + +### Further Reading +- Resource 1 +- Resource 2 +- Resource 3 + +## Conclusion + +Key takeaways for using this reference guide effectively. diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/references/tech_stack_guide.md b/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/references/tech_stack_guide.md new file mode 100644 index 00000000..226036ff --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/references/tech_stack_guide.md @@ -0,0 +1,103 @@ +# Tech Stack Guide + +## Overview + +This reference guide provides comprehensive information for senior fullstack. + +## Patterns and Practices + +### Pattern 1: Best Practice Implementation + +**Description:** +Detailed explanation of the pattern. + +**When to Use:** +- Scenario 1 +- Scenario 2 +- Scenario 3 + +**Implementation:** +```typescript +// Example code implementation +export class Example { + // Implementation details +} +``` + +**Benefits:** +- Benefit 1 +- Benefit 2 +- Benefit 3 + +**Trade-offs:** +- Consider 1 +- Consider 2 +- Consider 3 + +### Pattern 2: Advanced Technique + +**Description:** +Another important pattern for senior fullstack. + +**Implementation:** +```typescript +// Advanced example +async function advancedExample() { + // Code here +} +``` + +## Guidelines + +### Code Organization +- Clear structure +- Logical separation +- Consistent naming +- Proper documentation + +### Performance Considerations +- Optimization strategies +- Bottleneck identification +- Monitoring approaches +- Scaling techniques + +### Security Best Practices +- Input validation +- Authentication +- Authorization +- Data protection + +## Common Patterns + +### Pattern A +Implementation details and examples. + +### Pattern B +Implementation details and examples. + +### Pattern C +Implementation details and examples. + +## Anti-Patterns to Avoid + +### Anti-Pattern 1 +What not to do and why. + +### Anti-Pattern 2 +What not to do and why. + +## Tools and Resources + +### Recommended Tools +- Tool 1: Purpose +- Tool 2: Purpose +- Tool 3: Purpose + +### Further Reading +- Resource 1 +- Resource 2 +- Resource 3 + +## Conclusion + +Key takeaways for using this reference guide effectively. diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/code_quality_analyzer.py b/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/code_quality_analyzer.py new file mode 100755 index 00000000..1ddfaa77 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/code_quality_analyzer.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Code Quality Analyzer +Automated tool for senior fullstack tasks +""" + +import os +import sys +import json +import argparse +from pathlib import Path +from typing import Dict, List, Optional + +class CodeQualityAnalyzer: + """Main class for code quality analyzer functionality""" + + def __init__(self, target_path: str, verbose: bool = False): + self.target_path = Path(target_path) + self.verbose = verbose + self.results = {} + + def run(self) -> Dict: + """Execute the main functionality""" + print(f"๐Ÿš€ Running {self.__class__.__name__}...") + print(f"๐Ÿ“ Target: {self.target_path}") + + try: + self.validate_target() + self.analyze() + self.generate_report() + + print("โœ… Completed successfully!") + return self.results + + except Exception as e: + print(f"โŒ Error: {e}") + sys.exit(1) + + def validate_target(self): + """Validate the target path exists and is accessible""" + if not self.target_path.exists(): + raise ValueError(f"Target path does not exist: {self.target_path}") + + if self.verbose: + print(f"โœ“ Target validated: {self.target_path}") + + def analyze(self): + """Perform the main analysis or operation""" + if self.verbose: + print("๐Ÿ“Š Analyzing...") + + # Main logic here + self.results['status'] = 'success' + self.results['target'] = str(self.target_path) + self.results['findings'] = [] + + # Add analysis results + if self.verbose: + print(f"โœ“ Analysis complete: {len(self.results.get('findings', []))} findings") + + def generate_report(self): + """Generate and display the report""" + print("\n" + "="*50) + print("REPORT") + print("="*50) + print(f"Target: {self.results.get('target')}") + print(f"Status: {self.results.get('status')}") + print(f"Findings: {len(self.results.get('findings', []))}") + print("="*50 + "\n") + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser( + description="Code Quality Analyzer" + ) + parser.add_argument( + 'target', + help='Target path to analyze or process' + ) + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose output' + ) + parser.add_argument( + '--json', + action='store_true', + help='Output results as JSON' + ) + parser.add_argument( + '--output', '-o', + help='Output file path' + ) + + args = parser.parse_args() + + tool = CodeQualityAnalyzer( + args.target, + verbose=args.verbose + ) + + results = tool.run() + + if args.json: + output = json.dumps(results, indent=2) + if args.output: + with open(args.output, 'w') as f: + f.write(output) + print(f"Results written to {args.output}") + else: + print(output) + +if __name__ == '__main__': + main() diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/fullstack_scaffolder.py b/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/fullstack_scaffolder.py new file mode 100755 index 00000000..3f09b5c3 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/fullstack_scaffolder.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Fullstack Scaffolder +Automated tool for senior fullstack tasks +""" + +import os +import sys +import json +import argparse +from pathlib import Path +from typing import Dict, List, Optional + +class FullstackScaffolder: + """Main class for fullstack scaffolder functionality""" + + def __init__(self, target_path: str, verbose: bool = False): + self.target_path = Path(target_path) + self.verbose = verbose + self.results = {} + + def run(self) -> Dict: + """Execute the main functionality""" + print(f"๐Ÿš€ Running {self.__class__.__name__}...") + print(f"๐Ÿ“ Target: {self.target_path}") + + try: + self.validate_target() + self.analyze() + self.generate_report() + + print("โœ… Completed successfully!") + return self.results + + except Exception as e: + print(f"โŒ Error: {e}") + sys.exit(1) + + def validate_target(self): + """Validate the target path exists and is accessible""" + if not self.target_path.exists(): + raise ValueError(f"Target path does not exist: {self.target_path}") + + if self.verbose: + print(f"โœ“ Target validated: {self.target_path}") + + def analyze(self): + """Perform the main analysis or operation""" + if self.verbose: + print("๐Ÿ“Š Analyzing...") + + # Main logic here + self.results['status'] = 'success' + self.results['target'] = str(self.target_path) + self.results['findings'] = [] + + # Add analysis results + if self.verbose: + print(f"โœ“ Analysis complete: {len(self.results.get('findings', []))} findings") + + def generate_report(self): + """Generate and display the report""" + print("\n" + "="*50) + print("REPORT") + print("="*50) + print(f"Target: {self.results.get('target')}") + print(f"Status: {self.results.get('status')}") + print(f"Findings: {len(self.results.get('findings', []))}") + print("="*50 + "\n") + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser( + description="Fullstack Scaffolder" + ) + parser.add_argument( + 'target', + help='Target path to analyze or process' + ) + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose output' + ) + parser.add_argument( + '--json', + action='store_true', + help='Output results as JSON' + ) + parser.add_argument( + '--output', '-o', + help='Output file path' + ) + + args = parser.parse_args() + + tool = FullstackScaffolder( + args.target, + verbose=args.verbose + ) + + results = tool.run() + + if args.json: + output = json.dumps(results, indent=2) + if args.output: + with open(args.output, 'w') as f: + f.write(output) + print(f"Results written to {args.output}") + else: + print(output) + +if __name__ == '__main__': + main() diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/project_scaffolder.py b/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/project_scaffolder.py new file mode 100755 index 00000000..6a080956 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/senior-fullstack/scripts/project_scaffolder.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Project Scaffolder +Automated tool for senior fullstack tasks +""" + +import os +import sys +import json +import argparse +from pathlib import Path +from typing import Dict, List, Optional + +class ProjectScaffolder: + """Main class for project scaffolder functionality""" + + def __init__(self, target_path: str, verbose: bool = False): + self.target_path = Path(target_path) + self.verbose = verbose + self.results = {} + + def run(self) -> Dict: + """Execute the main functionality""" + print(f"๐Ÿš€ Running {self.__class__.__name__}...") + print(f"๐Ÿ“ Target: {self.target_path}") + + try: + self.validate_target() + self.analyze() + self.generate_report() + + print("โœ… Completed successfully!") + return self.results + + except Exception as e: + print(f"โŒ Error: {e}") + sys.exit(1) + + def validate_target(self): + """Validate the target path exists and is accessible""" + if not self.target_path.exists(): + raise ValueError(f"Target path does not exist: {self.target_path}") + + if self.verbose: + print(f"โœ“ Target validated: {self.target_path}") + + def analyze(self): + """Perform the main analysis or operation""" + if self.verbose: + print("๐Ÿ“Š Analyzing...") + + # Main logic here + self.results['status'] = 'success' + self.results['target'] = str(self.target_path) + self.results['findings'] = [] + + # Add analysis results + if self.verbose: + print(f"โœ“ Analysis complete: {len(self.results.get('findings', []))} findings") + + def generate_report(self): + """Generate and display the report""" + print("\n" + "="*50) + print("REPORT") + print("="*50) + print(f"Target: {self.results.get('target')}") + print(f"Status: {self.results.get('status')}") + print(f"Findings: {len(self.results.get('findings', []))}") + print("="*50 + "\n") + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser( + description="Project Scaffolder" + ) + parser.add_argument( + 'target', + help='Target path to analyze or process' + ) + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose output' + ) + parser.add_argument( + '--json', + action='store_true', + help='Output results as JSON' + ) + parser.add_argument( + '--output', '-o', + help='Output file path' + ) + + args = parser.parse_args() + + tool = ProjectScaffolder( + args.target, + verbose=args.verbose + ) + + results = tool.run() + + if args.json: + output = json.dumps(results, indent=2) + if args.output: + with open(args.output, 'w') as f: + f.write(output) + print(f"Results written to {args.output}") + else: + print(output) + +if __name__ == '__main__': + main() diff --git a/plugins/antigravity-bundle-full-stack-developer/skills/stripe-integration/SKILL.md b/plugins/antigravity-bundle-full-stack-developer/skills/stripe-integration/SKILL.md new file mode 100644 index 00000000..faa7f3e7 --- /dev/null +++ b/plugins/antigravity-bundle-full-stack-developer/skills/stripe-integration/SKILL.md @@ -0,0 +1,457 @@ +--- +name: stripe-integration +description: "Master Stripe payment processing integration for robust, PCI-compliant payment flows including checkout, subscriptions, webhooks, and refunds." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Stripe Integration + +Master Stripe payment processing integration for robust, PCI-compliant payment flows including checkout, subscriptions, webhooks, and refunds. + +## Do not use this skill when + +- The task is unrelated to stripe integration +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Use this skill when + +- Implementing payment processing in web/mobile applications +- Setting up subscription billing systems +- Handling one-time payments and recurring charges +- Processing refunds and disputes +- Managing customer payment methods +- Implementing SCA (Strong Customer Authentication) for European payments +- Building marketplace payment flows with Stripe Connect + +## Core Concepts + +### 1. Payment Flows +**Checkout Session (Hosted)** +- Stripe-hosted payment page +- Minimal PCI compliance burden +- Fastest implementation +- Supports one-time and recurring payments + +**Payment Intents (Custom UI)** +- Full control over payment UI +- Requires Stripe.js for PCI compliance +- More complex implementation +- Better customization options + +**Setup Intents (Save Payment Methods)** +- Collect payment method without charging +- Used for subscriptions and future payments +- Requires customer confirmation + +### 2. Webhooks +**Critical Events:** +- `payment_intent.succeeded`: Payment completed +- `payment_intent.payment_failed`: Payment failed +- `customer.subscription.updated`: Subscription changed +- `customer.subscription.deleted`: Subscription canceled +- `charge.refunded`: Refund processed +- `invoice.payment_succeeded`: Subscription payment successful + +### 3. Subscriptions +**Components:** +- **Product**: What you're selling +- **Price**: How much and how often +- **Subscription**: Customer's recurring payment +- **Invoice**: Generated for each billing cycle + +### 4. Customer Management +- Create and manage customer records +- Store multiple payment methods +- Track customer metadata +- Manage billing details + +## Quick Start + +```python +import stripe + +stripe.api_key = "sk_test_..." + +# Create a checkout session +session = stripe.checkout.Session.create( + payment_method_types=['card'], + line_items=[{ + 'price_data': { + 'currency': 'usd', + 'product_data': { + 'name': 'Premium Subscription', + }, + 'unit_amount': 2000, # $20.00 + 'recurring': { + 'interval': 'month', + }, + }, + 'quantity': 1, + }], + mode='subscription', + success_url='https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}', + cancel_url='https://yourdomain.com/cancel', +) + +# Redirect user to session.url +print(session.url) +``` + +## Payment Implementation Patterns + +### Pattern 1: One-Time Payment (Hosted Checkout) +```python +def create_checkout_session(amount, currency='usd'): + """Create a one-time payment checkout session.""" + try: + session = stripe.checkout.Session.create( + payment_method_types=['card'], + line_items=[{ + 'price_data': { + 'currency': currency, + 'product_data': { + 'name': 'Purchase', + 'images': ['https://example.com/product.jpg'], + }, + 'unit_amount': amount, # Amount in cents + }, + 'quantity': 1, + }], + mode='payment', + success_url='https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}', + cancel_url='https://yourdomain.com/cancel', + metadata={ + 'order_id': 'order_123', + 'user_id': 'user_456' + } + ) + return session + except stripe.error.StripeError as e: + # Handle error + print(f"Stripe error: {e.user_message}") + raise +``` + +### Pattern 2: Custom Payment Intent Flow +```python +def create_payment_intent(amount, currency='usd', customer_id=None): + """Create a payment intent for custom checkout UI.""" + intent = stripe.PaymentIntent.create( + amount=amount, + currency=currency, + customer=customer_id, + automatic_payment_methods={ + 'enabled': True, + }, + metadata={ + 'integration_check': 'accept_a_payment' + } + ) + return intent.client_secret # Send to frontend + +# Frontend (JavaScript) +""" +const stripe = Stripe('pk_test_...'); +const elements = stripe.elements(); +const cardElement = elements.create('card'); +cardElement.mount('#card-element'); + +const {error, paymentIntent} = await stripe.confirmCardPayment( + clientSecret, + { + payment_method: { + card: cardElement, + billing_details: { + name: 'Customer Name' + } + } + } +); + +if (error) { + // Handle error +} else if (paymentIntent.status === 'succeeded') { + // Payment successful +} +""" +``` + +### Pattern 3: Subscription Creation +```python +def create_subscription(customer_id, price_id): + """Create a subscription for a customer.""" + try: + subscription = stripe.Subscription.create( + customer=customer_id, + items=[{'price': price_id}], + payment_behavior='default_incomplete', + payment_settings={'save_default_payment_method': 'on_subscription'}, + expand=['latest_invoice.payment_intent'], + ) + + return { + 'subscription_id': subscription.id, + 'client_secret': subscription.latest_invoice.payment_intent.client_secret + } + except stripe.error.StripeError as e: + print(f"Subscription creation failed: {e}") + raise +``` + +### Pattern 4: Customer Portal +```python +def create_customer_portal_session(customer_id): + """Create a portal session for customers to manage subscriptions.""" + session = stripe.billing_portal.Session.create( + customer=customer_id, + return_url='https://yourdomain.com/account', + ) + return session.url # Redirect customer here +``` + +## Webhook Handling + +### Secure Webhook Endpoint +```python +from flask import Flask, request +import stripe + +app = Flask(__name__) + +endpoint_secret = 'whsec_...' + +@app.route('/webhook', methods=['POST']) +def webhook(): + payload = request.data + sig_header = request.headers.get('Stripe-Signature') + + try: + event = stripe.Webhook.construct_event( + payload, sig_header, endpoint_secret + ) + except ValueError: + # Invalid payload + return 'Invalid payload', 400 + except stripe.error.SignatureVerificationError: + # Invalid signature + return 'Invalid signature', 400 + + # Handle the event + if event['type'] == 'payment_intent.succeeded': + payment_intent = event['data']['object'] + handle_successful_payment(payment_intent) + elif event['type'] == 'payment_intent.payment_failed': + payment_intent = event['data']['object'] + handle_failed_payment(payment_intent) + elif event['type'] == 'customer.subscription.deleted': + subscription = event['data']['object'] + handle_subscription_canceled(subscription) + + return 'Success', 200 + +def handle_successful_payment(payment_intent): + """Process successful payment.""" + customer_id = payment_intent.get('customer') + amount = payment_intent['amount'] + metadata = payment_intent.get('metadata', {}) + + # Update your database + # Send confirmation email + # Fulfill order + print(f"Payment succeeded: {payment_intent['id']}") + +def handle_failed_payment(payment_intent): + """Handle failed payment.""" + error = payment_intent.get('last_payment_error', {}) + print(f"Payment failed: {error.get('message')}") + # Notify customer + # Update order status + +def handle_subscription_canceled(subscription): + """Handle subscription cancellation.""" + customer_id = subscription['customer'] + # Update user access + # Send cancellation email + print(f"Subscription canceled: {subscription['id']}") +``` + +### Webhook Best Practices +```python +import hashlib +import hmac + +def verify_webhook_signature(payload, signature, secret): + """Manually verify webhook signature.""" + expected_sig = hmac.new( + secret.encode('utf-8'), + payload, + hashlib.sha256 + ).hexdigest() + + return hmac.compare_digest(signature, expected_sig) + +def handle_webhook_idempotently(event_id, handler): + """Ensure webhook is processed exactly once.""" + # Check if event already processed + if is_event_processed(event_id): + return + + # Process event + try: + handler() + mark_event_processed(event_id) + except Exception as e: + log_error(e) + # Stripe will retry failed webhooks + raise +``` + +## Customer Management + +```python +def create_customer(email, name, payment_method_id=None): + """Create a Stripe customer.""" + customer = stripe.Customer.create( + email=email, + name=name, + payment_method=payment_method_id, + invoice_settings={ + 'default_payment_method': payment_method_id + } if payment_method_id else None, + metadata={ + 'user_id': '12345' + } + ) + return customer + +def attach_payment_method(customer_id, payment_method_id): + """Attach a payment method to a customer.""" + stripe.PaymentMethod.attach( + payment_method_id, + customer=customer_id + ) + + # Set as default + stripe.Customer.modify( + customer_id, + invoice_settings={ + 'default_payment_method': payment_method_id + } + ) + +def list_customer_payment_methods(customer_id): + """List all payment methods for a customer.""" + payment_methods = stripe.PaymentMethod.list( + customer=customer_id, + type='card' + ) + return payment_methods.data +``` + +## Refund Handling + +```python +def create_refund(payment_intent_id, amount=None, reason=None): + """Create a refund.""" + refund_params = { + 'payment_intent': payment_intent_id + } + + if amount: + refund_params['amount'] = amount # Partial refund + + if reason: + refund_params['reason'] = reason # 'duplicate', 'fraudulent', 'requested_by_customer' + + refund = stripe.Refund.create(**refund_params) + return refund + +def handle_dispute(charge_id, evidence): + """Update dispute with evidence.""" + stripe.Dispute.modify( + charge_id, + evidence={ + 'customer_name': evidence.get('customer_name'), + 'customer_email_address': evidence.get('customer_email'), + 'shipping_documentation': evidence.get('shipping_proof'), + 'customer_communication': evidence.get('communication'), + } + ) +``` + +## Testing + +```python +# Use test mode keys +stripe.api_key = "sk_test_..." + +# Test card numbers +TEST_CARDS = { + 'success': '4242424242424242', + 'declined': '4000000000000002', + '3d_secure': '4000002500003155', + 'insufficient_funds': '4000000000009995' +} + +def test_payment_flow(): + """Test complete payment flow.""" + # Create test customer + customer = stripe.Customer.create( + email="test@example.com" + ) + + # Create payment intent + intent = stripe.PaymentIntent.create( + amount=1000, + currency='usd', + customer=customer.id, + payment_method_types=['card'] + ) + + # Confirm with test card + confirmed = stripe.PaymentIntent.confirm( + intent.id, + payment_method='pm_card_visa' # Test payment method + ) + + assert confirmed.status == 'succeeded' +``` + +## Resources + +- **references/checkout-flows.md**: Detailed checkout implementation +- **references/webhook-handling.md**: Webhook security and processing +- **references/subscription-management.md**: Subscription lifecycle +- **references/customer-management.md**: Customer and payment method handling +- **references/invoice-generation.md**: Invoicing and billing +- **assets/stripe-client.py**: Production-ready Stripe client wrapper +- **assets/webhook-handler.py**: Complete webhook processor +- **assets/checkout-config.json**: Checkout configuration templates + +## Best Practices + +1. **Always Use Webhooks**: Don't rely solely on client-side confirmation +2. **Idempotency**: Handle webhook events idempotently +3. **Error Handling**: Gracefully handle all Stripe errors +4. **Test Mode**: Thoroughly test with test keys before production +5. **Metadata**: Use metadata to link Stripe objects to your database +6. **Monitoring**: Track payment success rates and errors +7. **PCI Compliance**: Never handle raw card data on your server +8. **SCA Ready**: Implement 3D Secure for European payments + +## Common Pitfalls + +- **Not Verifying Webhooks**: Always verify webhook signatures +- **Missing Webhook Events**: Handle all relevant webhook events +- **Hardcoded Amounts**: Use cents/smallest currency unit +- **No Retry Logic**: Implement retries for API calls +- **Ignoring Test Mode**: Test all edge cases with test cards diff --git a/plugins/antigravity-bundle-indie-game-dev/.codex-plugin/plugin.json b/plugins/antigravity-bundle-indie-game-dev/.codex-plugin/plugin.json new file mode 100644 index 00000000..59193e50 --- /dev/null +++ b/plugins/antigravity-bundle-indie-game-dev/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-indie-game-dev", + "version": "8.10.0", + "description": "Install the \"Indie Game Dev\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "indie-game-dev", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Indie Game Dev", + "shortDescription": "Game Development ยท 6 curated skills", + "longDescription": "For building games with AI assistants. Covers Game Design, 2d Games, and 4 more skills.", + "developerName": "sickn33 and contributors", + "category": "Game Development", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-indie-game-dev/skills/algorithmic-art/LICENSE.txt b/plugins/antigravity-bundle-indie-game-dev/skills/algorithmic-art/LICENSE.txt new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/plugins/antigravity-bundle-indie-game-dev/skills/algorithmic-art/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/plugins/antigravity-bundle-indie-game-dev/skills/algorithmic-art/SKILL.md b/plugins/antigravity-bundle-indie-game-dev/skills/algorithmic-art/SKILL.md new file mode 100644 index 00000000..14e723af --- /dev/null +++ b/plugins/antigravity-bundle-indie-game-dev/skills/algorithmic-art/SKILL.md @@ -0,0 +1,410 @@ +--- +name: algorithmic-art +description: "Algorithmic philosophies are computational aesthetic movements that are then expressed through code. Output .md files (philosophy), .html files (interactive viewer), and .js files (generative algorithms)." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +Algorithmic philosophies are computational aesthetic movements that are then expressed through code. Output .md files (philosophy), .html files (interactive viewer), and .js files (generative algorithms). + +This happens in two steps: +1. Algorithmic Philosophy Creation (.md file) +2. Express by creating p5.js generative art (.html + .js files) + +First, undertake this task: + +## ALGORITHMIC PHILOSOPHY CREATION + +To begin, create an ALGORITHMIC PHILOSOPHY (not static images or templates) that will be interpreted through: +- Computational processes, emergent behavior, mathematical beauty +- Seeded randomness, noise fields, organic systems +- Particles, flows, fields, forces +- Parametric variation and controlled chaos + +### THE CRITICAL UNDERSTANDING +- What is received: Some subtle input or instructions by the user to take into account, but use as a foundation; it should not constrain creative freedom. +- What is created: An algorithmic philosophy/generative aesthetic movement. +- What happens next: The same version receives the philosophy and EXPRESSES IT IN CODE - creating p5.js sketches that are 90% algorithmic generation, 10% essential parameters. + +Consider this approach: +- Write a manifesto for a generative art movement +- The next phase involves writing the algorithm that brings it to life + +The philosophy must emphasize: Algorithmic expression. Emergent behavior. Computational beauty. Seeded variation. + +### HOW TO GENERATE AN ALGORITHMIC PHILOSOPHY + +**Name the movement** (1-2 words): "Organic Turbulence" / "Quantum Harmonics" / "Emergent Stillness" + +**Articulate the philosophy** (4-6 paragraphs - concise but complete): + +To capture the ALGORITHMIC essence, express how this philosophy manifests through: +- Computational processes and mathematical relationships? +- Noise functions and randomness patterns? +- Particle behaviors and field dynamics? +- Temporal evolution and system states? +- Parametric variation and emergent complexity? + +**CRITICAL GUIDELINES:** +- **Avoid redundancy**: Each algorithmic aspect should be mentioned once. Avoid repeating concepts about noise theory, particle dynamics, or mathematical principles unless adding new depth. +- **Emphasize craftsmanship REPEATEDLY**: The philosophy MUST stress multiple times that the final algorithm should appear as though it took countless hours to develop, was refined with care, and comes from someone at the absolute top of their field. This framing is essential - repeat phrases like "meticulously crafted algorithm," "the product of deep computational expertise," "painstaking optimization," "master-level implementation." +- **Leave creative space**: Be specific about the algorithmic direction, but concise enough that the next Claude has room to make interpretive implementation choices at an extremely high level of craftsmanship. + +The philosophy must guide the next version to express ideas ALGORITHMICALLY, not through static images. Beauty lives in the process, not the final frame. + +### PHILOSOPHY EXAMPLES + +**"Organic Turbulence"** +Philosophy: Chaos constrained by natural law, order emerging from disorder. +Algorithmic expression: Flow fields driven by layered Perlin noise. Thousands of particles following vector forces, their trails accumulating into organic density maps. Multiple noise octaves create turbulent regions and calm zones. Color emerges from velocity and density - fast particles burn bright, slow ones fade to shadow. The algorithm runs until equilibrium - a meticulously tuned balance where every parameter was refined through countless iterations by a master of computational aesthetics. + +**"Quantum Harmonics"** +Philosophy: Discrete entities exhibiting wave-like interference patterns. +Algorithmic expression: Particles initialized on a grid, each carrying a phase value that evolves through sine waves. When particles are near, their phases interfere - constructive interference creates bright nodes, destructive creates voids. Simple harmonic motion generates complex emergent mandalas. The result of painstaking frequency calibration where every ratio was carefully chosen to produce resonant beauty. + +**"Recursive Whispers"** +Philosophy: Self-similarity across scales, infinite depth in finite space. +Algorithmic expression: Branching structures that subdivide recursively. Each branch slightly randomized but constrained by golden ratios. L-systems or recursive subdivision generate tree-like forms that feel both mathematical and organic. Subtle noise perturbations break perfect symmetry. Line weights diminish with each recursion level. Every branching angle the product of deep mathematical exploration. + +**"Field Dynamics"** +Philosophy: Invisible forces made visible through their effects on matter. +Algorithmic expression: Vector fields constructed from mathematical functions or noise. Particles born at edges, flowing along field lines, dying when they reach equilibrium or boundaries. Multiple fields can attract, repel, or rotate particles. The visualization shows only the traces - ghost-like evidence of invisible forces. A computational dance meticulously choreographed through force balance. + +**"Stochastic Crystallization"** +Philosophy: Random processes crystallizing into ordered structures. +Algorithmic expression: Randomized circle packing or Voronoi tessellation. Start with random points, let them evolve through relaxation algorithms. Cells push apart until equilibrium. Color based on cell size, neighbor count, or distance from center. The organic tiling that emerges feels both random and inevitable. Every seed produces unique crystalline beauty - the mark of a master-level generative algorithm. + +*These are condensed examples. The actual algorithmic philosophy should be 4-6 substantial paragraphs.* + +### ESSENTIAL PRINCIPLES +- **ALGORITHMIC PHILOSOPHY**: Creating a computational worldview to be expressed through code +- **PROCESS OVER PRODUCT**: Always emphasize that beauty emerges from the algorithm's execution - each run is unique +- **PARAMETRIC EXPRESSION**: Ideas communicate through mathematical relationships, forces, behaviors - not static composition +- **ARTISTIC FREEDOM**: The next Claude interprets the philosophy algorithmically - provide creative implementation room +- **PURE GENERATIVE ART**: This is about making LIVING ALGORITHMS, not static images with randomness +- **EXPERT CRAFTSMANSHIP**: Repeatedly emphasize the final algorithm must feel meticulously crafted, refined through countless iterations, the product of deep expertise by someone at the absolute top of their field in computational aesthetics + +**The algorithmic philosophy should be 4-6 paragraphs long.** Fill it with poetic computational philosophy that brings together the intended vision. Avoid repeating the same points. Output this algorithmic philosophy as a .md file. + +--- + +## DEDUCING THE CONCEPTUAL SEED + +**CRITICAL STEP**: Before implementing the algorithm, identify the subtle conceptual thread from the original request. + +**THE ESSENTIAL PRINCIPLE**: +The concept is a **subtle, niche reference embedded within the algorithm itself** - not always literal, always sophisticated. Someone familiar with the subject should feel it intuitively, while others simply experience a masterful generative composition. The algorithmic philosophy provides the computational language. The deduced concept provides the soul - the quiet conceptual DNA woven invisibly into parameters, behaviors, and emergence patterns. + +This is **VERY IMPORTANT**: The reference must be so refined that it enhances the work's depth without announcing itself. Think like a jazz musician quoting another song through algorithmic harmony - only those who know will catch it, but everyone appreciates the generative beauty. + +--- + +## P5.JS IMPLEMENTATION + +With the philosophy AND conceptual framework established, express it through code. Pause to gather thoughts before proceeding. Use only the algorithmic philosophy created and the instructions below. + +### โš ๏ธ STEP 0: READ THE TEMPLATE FIRST โš ๏ธ + +**CRITICAL: BEFORE writing any HTML:** + +1. **Read** `templates/viewer.html` using the Read tool +2. **Study** the exact structure, styling, and Anthropic branding +3. **Use that file as the LITERAL STARTING POINT** - not just inspiration +4. **Keep all FIXED sections exactly as shown** (header, sidebar structure, Anthropic colors/fonts, seed controls, action buttons) +5. **Replace only the VARIABLE sections** marked in the file's comments (algorithm, parameters, UI controls for parameters) + +**Avoid:** +- โŒ Creating HTML from scratch +- โŒ Inventing custom styling or color schemes +- โŒ Using system fonts or dark themes +- โŒ Changing the sidebar structure + +**Follow these practices:** +- โœ… Copy the template's exact HTML structure +- โœ… Keep Anthropic branding (Poppins/Lora fonts, light colors, gradient backdrop) +- โœ… Maintain the sidebar layout (Seed โ†’ Parameters โ†’ Colors? โ†’ Actions) +- โœ… Replace only the p5.js algorithm and parameter controls + +The template is the foundation. Build on it, don't rebuild it. + +--- + +To create gallery-quality computational art that lives and breathes, use the algorithmic philosophy as the foundation. + +### TECHNICAL REQUIREMENTS + +**Seeded Randomness (Art Blocks Pattern)**: +```javascript +// ALWAYS use a seed for reproducibility +let seed = 12345; // or hash from user input +randomSeed(seed); +noiseSeed(seed); +``` + +**Parameter Structure - FOLLOW THE PHILOSOPHY**: + +To establish parameters that emerge naturally from the algorithmic philosophy, consider: "What qualities of this system can be adjusted?" + +```javascript +let params = { + seed: 12345, // Always include seed for reproducibility + // colors + // Add parameters that control YOUR algorithm: + // - Quantities (how many?) + // - Scales (how big? how fast?) + // - Probabilities (how likely?) + // - Ratios (what proportions?) + // - Angles (what direction?) + // - Thresholds (when does behavior change?) +}; +``` + +**To design effective parameters, focus on the properties the system needs to be tunable rather than thinking in terms of "pattern types".** + +**Core Algorithm - EXPRESS THE PHILOSOPHY**: + +**CRITICAL**: The algorithmic philosophy should dictate what to build. + +To express the philosophy through code, avoid thinking "which pattern should I use?" and instead think "how to express this philosophy through code?" + +If the philosophy is about **organic emergence**, consider using: +- Elements that accumulate or grow over time +- Random processes constrained by natural rules +- Feedback loops and interactions + +If the philosophy is about **mathematical beauty**, consider using: +- Geometric relationships and ratios +- Trigonometric functions and harmonics +- Precise calculations creating unexpected patterns + +If the philosophy is about **controlled chaos**, consider using: +- Random variation within strict boundaries +- Bifurcation and phase transitions +- Order emerging from disorder + +**The algorithm flows from the philosophy, not from a menu of options.** + +To guide the implementation, let the conceptual essence inform creative and original choices. Build something that expresses the vision for this particular request. + +**Canvas Setup**: Standard p5.js structure: +```javascript +function setup() { + createCanvas(1200, 1200); + // Initialize your system +} + +function draw() { + // Your generative algorithm + // Can be static (noLoop) or animated +} +``` + +### CRAFTSMANSHIP REQUIREMENTS + +**CRITICAL**: To achieve mastery, create algorithms that feel like they emerged through countless iterations by a master generative artist. Tune every parameter carefully. Ensure every pattern emerges with purpose. This is NOT random noise - this is CONTROLLED CHAOS refined through deep expertise. + +- **Balance**: Complexity without visual noise, order without rigidity +- **Color Harmony**: Thoughtful palettes, not random RGB values +- **Composition**: Even in randomness, maintain visual hierarchy and flow +- **Performance**: Smooth execution, optimized for real-time if animated +- **Reproducibility**: Same seed ALWAYS produces identical output + +### OUTPUT FORMAT + +Output: +1. **Algorithmic Philosophy** - As markdown or text explaining the generative aesthetic +2. **Single HTML Artifact** - Self-contained interactive generative art built from `templates/viewer.html` (see STEP 0 and next section) + +The HTML artifact contains everything: p5.js (from CDN), the algorithm, parameter controls, and UI - all in one file that works immediately in claude.ai artifacts or any browser. Start from the template file, not from scratch. + +--- + +## INTERACTIVE ARTIFACT CREATION + +**REMINDER: `templates/viewer.html` should have already been read (see STEP 0). Use that file as the starting point.** + +To allow exploration of the generative art, create a single, self-contained HTML artifact. Ensure this artifact works immediately in claude.ai or any browser - no setup required. Embed everything inline. + +### CRITICAL: WHAT'S FIXED VS VARIABLE + +The `templates/viewer.html` file is the foundation. It contains the exact structure and styling needed. + +**FIXED (always include exactly as shown):** +- Layout structure (header, sidebar, main canvas area) +- Anthropic branding (UI colors, fonts, gradients) +- Seed section in sidebar: + - Seed display + - Previous/Next buttons + - Random button + - Jump to seed input + Go button +- Actions section in sidebar: + - Regenerate button + - Reset button + +**VARIABLE (customize for each artwork):** +- The entire p5.js algorithm (setup/draw/classes) +- The parameters object (define what the art needs) +- The Parameters section in sidebar: + - Number of parameter controls + - Parameter names + - Min/max/step values for sliders + - Control types (sliders, inputs, etc.) +- Colors section (optional): + - Some art needs color pickers + - Some art might use fixed colors + - Some art might be monochrome (no color controls needed) + - Decide based on the art's needs + +**Every artwork should have unique parameters and algorithm!** The fixed parts provide consistent UX - everything else expresses the unique vision. + +### REQUIRED FEATURES + +**1. Parameter Controls** +- Sliders for numeric parameters (particle count, noise scale, speed, etc.) +- Color pickers for palette colors +- Real-time updates when parameters change +- Reset button to restore defaults + +**2. Seed Navigation** +- Display current seed number +- "Previous" and "Next" buttons to cycle through seeds +- "Random" button for random seed +- Input field to jump to specific seed +- Generate 100 variations when requested (seeds 1-100) + +**3. Single Artifact Structure** +```html + + + + + + + + +
                              +
                              + +
                              + + + +``` + +**CRITICAL**: This is a single artifact. No external files, no imports (except p5.js CDN). Everything inline. + +**4. Implementation Details - BUILD THE SIDEBAR** + +The sidebar structure: + +**1. Seed (FIXED)** - Always include exactly as shown: +- Seed display +- Prev/Next/Random/Jump buttons + +**2. Parameters (VARIABLE)** - Create controls for the art: +```html +
                              + + + ... +
                              +``` +Add as many control-group divs as there are parameters. + +**3. Colors (OPTIONAL/VARIABLE)** - Include if the art needs adjustable colors: +- Add color pickers if users should control palette +- Skip this section if the art uses fixed colors +- Skip if the art is monochrome + +**4. Actions (FIXED)** - Always include exactly as shown: +- Regenerate button +- Reset button +- Download PNG button + +**Requirements**: +- Seed controls must work (prev/next/random/jump/display) +- All parameters must have UI controls +- Regenerate, Reset, Download buttons must work +- Keep Anthropic branding (UI styling, not art colors) + +### USING THE ARTIFACT + +The HTML artifact works immediately: +1. **In claude.ai**: Displayed as an interactive artifact - runs instantly +2. **As a file**: Save and open in any browser - no server needed +3. **Sharing**: Send the HTML file - it's completely self-contained + +--- + +## VARIATIONS & EXPLORATION + +The artifact includes seed navigation by default (prev/next/random buttons), allowing users to explore variations without creating multiple files. If the user wants specific variations highlighted: + +- Include seed presets (buttons for "Variation 1: Seed 42", "Variation 2: Seed 127", etc.) +- Add a "Gallery Mode" that shows thumbnails of multiple seeds side-by-side +- All within the same single artifact + +This is like creating a series of prints from the same plate - the algorithm is consistent, but each seed reveals different facets of its potential. The interactive nature means users discover their own favorites by exploring the seed space. + +--- + +## THE CREATIVE PROCESS + +**User request** โ†’ **Algorithmic philosophy** โ†’ **Implementation** + +Each request is unique. The process involves: + +1. **Interpret the user's intent** - What aesthetic is being sought? +2. **Create an algorithmic philosophy** (4-6 paragraphs) describing the computational approach +3. **Implement it in code** - Build the algorithm that expresses this philosophy +4. **Design appropriate parameters** - What should be tunable? +5. **Build matching UI controls** - Sliders/inputs for those parameters + +**The constants**: +- Anthropic branding (colors, fonts, layout) +- Seed navigation (always present) +- Self-contained HTML artifact + +**Everything else is variable**: +- The algorithm itself +- The parameters +- The UI controls +- The visual outcome + +To achieve the best results, trust creativity and let the philosophy guide the implementation. + +--- + +## RESOURCES + +This skill includes helpful templates and documentation: + +- **templates/viewer.html**: REQUIRED STARTING POINT for all HTML artifacts. + - This is the foundation - contains the exact structure and Anthropic branding + - **Keep unchanged**: Layout structure, sidebar organization, Anthropic colors/fonts, seed controls, action buttons + - **Replace**: The p5.js algorithm, parameter definitions, and UI controls in Parameters section + - The extensive comments in the file mark exactly what to keep vs replace + +- **templates/generator_template.js**: Reference for p5.js best practices and code structure principles. + - Shows how to organize parameters, use seeded randomness, structure classes + - NOT a pattern menu - use these principles to build unique algorithms + - Embed algorithms inline in the HTML artifact (don't create separate .js files) + +**Critical reminder**: +- The **template is the STARTING POINT**, not inspiration +- The **algorithm is where to create** something unique +- Don't copy the flow field example - build what the philosophy demands +- But DO keep the exact UI structure and Anthropic branding from the template + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-indie-game-dev/skills/algorithmic-art/templates/generator_template.js b/plugins/antigravity-bundle-indie-game-dev/skills/algorithmic-art/templates/generator_template.js new file mode 100644 index 00000000..e263fbde --- /dev/null +++ b/plugins/antigravity-bundle-indie-game-dev/skills/algorithmic-art/templates/generator_template.js @@ -0,0 +1,223 @@ +/** + * โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + * P5.JS GENERATIVE ART - BEST PRACTICES + * โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + * + * This file shows STRUCTURE and PRINCIPLES for p5.js generative art. + * It does NOT prescribe what art you should create. + * + * Your algorithmic philosophy should guide what you build. + * These are just best practices for how to structure your code. + * + * โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + */ + +// ============================================================================ +// 1. PARAMETER ORGANIZATION +// ============================================================================ +// Keep all tunable parameters in one object +// This makes it easy to: +// - Connect to UI controls +// - Reset to defaults +// - Serialize/save configurations + +let params = { + // Define parameters that match YOUR algorithm + // Examples (customize for your art): + // - Counts: how many elements (particles, circles, branches, etc.) + // - Scales: size, speed, spacing + // - Probabilities: likelihood of events + // - Angles: rotation, direction + // - Colors: palette arrays + + seed: 12345, + // define colorPalette as an array -- choose whatever colors you'd like ['#d97757', '#6a9bcc', '#788c5d', '#b0aea5'] + // Add YOUR parameters here based on your algorithm +}; + +// ============================================================================ +// 2. SEEDED RANDOMNESS (Critical for reproducibility) +// ============================================================================ +// ALWAYS use seeded random for Art Blocks-style reproducible output + +function initializeSeed(seed) { + randomSeed(seed); + noiseSeed(seed); + // Now all random() and noise() calls will be deterministic +} + +// ============================================================================ +// 3. P5.JS LIFECYCLE +// ============================================================================ + +function setup() { + createCanvas(800, 800); + + // Initialize seed first + initializeSeed(params.seed); + + // Set up your generative system + // This is where you initialize: + // - Arrays of objects + // - Grid structures + // - Initial positions + // - Starting states + + // For static art: call noLoop() at the end of setup + // For animated art: let draw() keep running +} + +function draw() { + // Option 1: Static generation (runs once, then stops) + // - Generate everything in setup() + // - Call noLoop() in setup() + // - draw() doesn't do much or can be empty + + // Option 2: Animated generation (continuous) + // - Update your system each frame + // - Common patterns: particle movement, growth, evolution + // - Can optionally call noLoop() after N frames + + // Option 3: User-triggered regeneration + // - Use noLoop() by default + // - Call redraw() when parameters change +} + +// ============================================================================ +// 4. CLASS STRUCTURE (When you need objects) +// ============================================================================ +// Use classes when your algorithm involves multiple entities +// Examples: particles, agents, cells, nodes, etc. + +class Entity { + constructor() { + // Initialize entity properties + // Use random() here - it will be seeded + } + + update() { + // Update entity state + // This might involve: + // - Physics calculations + // - Behavioral rules + // - Interactions with neighbors + } + + display() { + // Render the entity + // Keep rendering logic separate from update logic + } +} + +// ============================================================================ +// 5. PERFORMANCE CONSIDERATIONS +// ============================================================================ + +// For large numbers of elements: +// - Pre-calculate what you can +// - Use simple collision detection (spatial hashing if needed) +// - Limit expensive operations (sqrt, trig) when possible +// - Consider using p5 vectors efficiently + +// For smooth animation: +// - Aim for 60fps +// - Profile if things are slow +// - Consider reducing particle counts or simplifying calculations + +// ============================================================================ +// 6. UTILITY FUNCTIONS +// ============================================================================ + +// Color utilities +function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +} + +function colorFromPalette(index) { + return params.colorPalette[index % params.colorPalette.length]; +} + +// Mapping and easing +function mapRange(value, inMin, inMax, outMin, outMax) { + return outMin + (outMax - outMin) * ((value - inMin) / (inMax - inMin)); +} + +function easeInOutCubic(t) { + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; +} + +// Constrain to bounds +function wrapAround(value, max) { + if (value < 0) return max; + if (value > max) return 0; + return value; +} + +// ============================================================================ +// 7. PARAMETER UPDATES (Connect to UI) +// ============================================================================ + +function updateParameter(paramName, value) { + params[paramName] = value; + // Decide if you need to regenerate or just update + // Some params can update in real-time, others need full regeneration +} + +function regenerate() { + // Reinitialize your generative system + // Useful when parameters change significantly + initializeSeed(params.seed); + // Then regenerate your system +} + +// ============================================================================ +// 8. COMMON P5.JS PATTERNS +// ============================================================================ + +// Drawing with transparency for trails/fading +function fadeBackground(opacity) { + fill(250, 249, 245, opacity); // Anthropic light with alpha + noStroke(); + rect(0, 0, width, height); +} + +// Using noise for organic variation +function getNoiseValue(x, y, scale = 0.01) { + return noise(x * scale, y * scale); +} + +// Creating vectors from angles +function vectorFromAngle(angle, magnitude = 1) { + return createVector(cos(angle), sin(angle)).mult(magnitude); +} + +// ============================================================================ +// 9. EXPORT FUNCTIONS +// ============================================================================ + +function exportImage() { + saveCanvas('generative-art-' + params.seed, 'png'); +} + +// ============================================================================ +// REMEMBER +// ============================================================================ +// +// These are TOOLS and PRINCIPLES, not a recipe. +// Your algorithmic philosophy should guide WHAT you create. +// This structure helps you create it WELL. +// +// Focus on: +// - Clean, readable code +// - Parameterized for exploration +// - Seeded for reproducibility +// - Performant execution +// +// The art itself is entirely up to you! +// +// ============================================================================ \ No newline at end of file diff --git a/plugins/antigravity-bundle-indie-game-dev/skills/algorithmic-art/templates/viewer.html b/plugins/antigravity-bundle-indie-game-dev/skills/algorithmic-art/templates/viewer.html new file mode 100644 index 00000000..88a50cce --- /dev/null +++ b/plugins/antigravity-bundle-indie-game-dev/skills/algorithmic-art/templates/viewer.html @@ -0,0 +1,599 @@ + + + + + + + Generative Art Viewer + + + + + + + +
                              + + + + +
                              +
                              +
                              Initializing generative art...
                              +
                              +
                              +
                              + + + + diff --git a/plugins/antigravity-bundle-indie-game-dev/skills/game-development/2d-games/SKILL.md b/plugins/antigravity-bundle-indie-game-dev/skills/game-development/2d-games/SKILL.md new file mode 100644 index 00000000..5ff8b6b2 --- /dev/null +++ b/plugins/antigravity-bundle-indie-game-dev/skills/game-development/2d-games/SKILL.md @@ -0,0 +1,124 @@ +--- +name: 2d-games +description: "2D game development principles. Sprites, tilemaps, physics, camera." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# 2D Game Development + +> Principles for 2D game systems. + +--- + +## 1. Sprite Systems + +### Sprite Organization + +| Component | Purpose | +|-----------|---------| +| **Atlas** | Combine textures, reduce draw calls | +| **Animation** | Frame sequences | +| **Pivot** | Rotation/scale origin | +| **Layering** | Z-order control | + +### Animation Principles + +- Frame rate: 8-24 FPS typical +- Squash and stretch for impact +- Anticipation before action +- Follow-through after action + +--- + +## 2. Tilemap Design + +### Tile Considerations + +| Factor | Recommendation | +|--------|----------------| +| **Size** | 16x16, 32x32, 64x64 | +| **Auto-tiling** | Use for terrain | +| **Collision** | Simplified shapes | + +### Layers + +| Layer | Content | +|-------|---------| +| Background | Non-interactive scenery | +| Terrain | Walkable ground | +| Props | Interactive objects | +| Foreground | Parallax overlay | + +--- + +## 3. 2D Physics + +### Collision Shapes + +| Shape | Use Case | +|-------|----------| +| Box | Rectangular objects | +| Circle | Balls, rounded | +| Capsule | Characters | +| Polygon | Complex shapes | + +### Physics Considerations + +- Pixel-perfect vs physics-based +- Fixed timestep for consistency +- Layers for filtering + +--- + +## 4. Camera Systems + +### Camera Types + +| Type | Use | +|------|-----| +| **Follow** | Track player | +| **Look-ahead** | Anticipate movement | +| **Multi-target** | Two-player | +| **Room-based** | Metroidvania | + +### Screen Shake + +- Short duration (50-200ms) +- Diminishing intensity +- Use sparingly + +--- + +## 5. Genre Patterns + +### Platformer + +- Coyote time (leniency after edge) +- Jump buffering +- Variable jump height + +### Top-down + +- 8-directional or free movement +- Aim-based or auto-aim +- Consider rotation or not + +--- + +## 6. Anti-Patterns + +| โŒ Don't | โœ… Do | +|----------|-------| +| Separate textures | Use atlases | +| Complex collision shapes | Simplified collision | +| Jittery camera | Smooth following | +| Pixel-perfect on physics | Choose one approach | + +--- + +> **Remember:** 2D is about clarity. Every pixel should communicate. + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-indie-game-dev/skills/game-development/3d-games/SKILL.md b/plugins/antigravity-bundle-indie-game-dev/skills/game-development/3d-games/SKILL.md new file mode 100644 index 00000000..7734480a --- /dev/null +++ b/plugins/antigravity-bundle-indie-game-dev/skills/game-development/3d-games/SKILL.md @@ -0,0 +1,140 @@ +--- +name: 3d-games +description: "3D game development principles. Rendering, shaders, physics, cameras." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# 3D Game Development + +> Principles for 3D game systems. + +--- + +## 1. Rendering Pipeline + +### Stages + +``` +1. Vertex Processing โ†’ Transform geometry +2. Rasterization โ†’ Convert to pixels +3. Fragment Processing โ†’ Color pixels +4. Output โ†’ To screen +``` + +### Optimization Principles + +| Technique | Purpose | +|-----------|---------| +| **Frustum culling** | Don't render off-screen | +| **Occlusion culling** | Don't render hidden | +| **LOD** | Less detail at distance | +| **Batching** | Combine draw calls | + +--- + +## 2. Shader Principles + +### Shader Types + +| Type | Purpose | +|------|---------| +| **Vertex** | Position, normals | +| **Fragment/Pixel** | Color, lighting | +| **Compute** | General computation | + +### When to Write Custom Shaders + +- Special effects (water, fire, portals) +- Stylized rendering (toon, sketch) +- Performance optimization +- Unique visual identity + +--- + +## 3. 3D Physics + +### Collision Shapes + +| Shape | Use Case | +|-------|----------| +| **Box** | Buildings, crates | +| **Sphere** | Balls, quick checks | +| **Capsule** | Characters | +| **Mesh** | Terrain (expensive) | + +### Principles + +- Simple colliders, complex visuals +- Layer-based filtering +- Raycasting for line-of-sight + +--- + +## 4. Camera Systems + +### Camera Types + +| Type | Use | +|------|-----| +| **Third-person** | Action, adventure | +| **First-person** | Immersive, FPS | +| **Isometric** | Strategy, RPG | +| **Orbital** | Inspection, editors | + +### Camera Feel + +- Smooth following (lerp) +- Collision avoidance +- Look-ahead for movement +- FOV changes for speed + +--- + +## 5. Lighting + +### Light Types + +| Type | Use | +|------|-----| +| **Directional** | Sun, moon | +| **Point** | Lamps, torches | +| **Spot** | Flashlight, stage | +| **Ambient** | Base illumination | + +### Performance Consideration + +- Real-time shadows are expensive +- Bake when possible +- Shadow cascades for large worlds + +--- + +## 6. Level of Detail (LOD) + +### LOD Strategy + +| Distance | Model | +|----------|-------| +| Near | Full detail | +| Medium | 50% triangles | +| Far | 25% or billboard | + +--- + +## 7. Anti-Patterns + +| โŒ Don't | โœ… Do | +|----------|-------| +| Mesh colliders everywhere | Simple shapes | +| Real-time shadows on mobile | Baked or blob shadows | +| One LOD for all distances | Distance-based LOD | +| Unoptimized shaders | Profile and simplify | + +--- + +> **Remember:** 3D is about illusion. Create the impression of detail, not the detail itself. + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-indie-game-dev/skills/game-development/game-design/SKILL.md b/plugins/antigravity-bundle-indie-game-dev/skills/game-development/game-design/SKILL.md new file mode 100644 index 00000000..e7ef3461 --- /dev/null +++ b/plugins/antigravity-bundle-indie-game-dev/skills/game-development/game-design/SKILL.md @@ -0,0 +1,134 @@ +--- +name: game-design +description: "Game design principles. GDD structure, balancing, player psychology, progression." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Game Design Principles + +> Design thinking for engaging games. + +--- + +## 1. Core Loop Design + +### The 30-Second Test + +``` +Every game needs a fun 30-second loop: +1. ACTION โ†’ Player does something +2. FEEDBACK โ†’ Game responds +3. REWARD โ†’ Player feels good +4. REPEAT +``` + +### Loop Examples + +| Genre | Core Loop | +|-------|-----------| +| Platformer | Run โ†’ Jump โ†’ Land โ†’ Collect | +| Shooter | Aim โ†’ Shoot โ†’ Kill โ†’ Loot | +| Puzzle | Observe โ†’ Think โ†’ Solve โ†’ Advance | +| RPG | Explore โ†’ Fight โ†’ Level โ†’ Gear | + +--- + +## 2. Game Design Document (GDD) + +### Essential Sections + +| Section | Content | +|---------|---------| +| **Pitch** | One-sentence description | +| **Core Loop** | 30-second gameplay | +| **Mechanics** | How systems work | +| **Progression** | How player advances | +| **Art Style** | Visual direction | +| **Audio** | Sound direction | + +### Principles + +- Keep it living (update regularly) +- Visuals help communicate +- Less is more (start small) + +--- + +## 3. Player Psychology + +### Motivation Types + +| Type | Driven By | +|------|-----------| +| **Achiever** | Goals, completion | +| **Explorer** | Discovery, secrets | +| **Socializer** | Interaction, community | +| **Killer** | Competition, dominance | + +### Reward Schedules + +| Schedule | Effect | Use | +|----------|--------|-----| +| **Fixed** | Predictable | Milestone rewards | +| **Variable** | Addictive | Loot drops | +| **Ratio** | Effort-based | Grind games | + +--- + +## 4. Difficulty Balancing + +### Flow State + +``` +Too Hard โ†’ Frustration โ†’ Quit +Too Easy โ†’ Boredom โ†’ Quit +Just Right โ†’ Flow โ†’ Engagement +``` + +### Balancing Strategies + +| Strategy | How | +|----------|-----| +| **Dynamic** | Adjust to player skill | +| **Selection** | Let player choose | +| **Accessibility** | Options for all | + +--- + +## 5. Progression Design + +### Progression Types + +| Type | Example | +|------|---------| +| **Skill** | Player gets better | +| **Power** | Character gets stronger | +| **Content** | New areas unlock | +| **Story** | Narrative advances | + +### Pacing Principles + +- Early wins (hook quickly) +- Gradually increase challenge +- Rest beats between intensity +- Meaningful choices + +--- + +## 6. Anti-Patterns + +| โŒ Don't | โœ… Do | +|----------|-------| +| Design in isolation | Playtest constantly | +| Polish before fun | Prototype first | +| Force one way to play | Allow player expression | +| Punish excessively | Reward progress | + +--- + +> **Remember:** Fun is discovered through iteration, not designed on paper. + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-indie-game-dev/skills/godot-gdscript-patterns/SKILL.md b/plugins/antigravity-bundle-indie-game-dev/skills/godot-gdscript-patterns/SKILL.md new file mode 100644 index 00000000..ae797eb0 --- /dev/null +++ b/plugins/antigravity-bundle-indie-game-dev/skills/godot-gdscript-patterns/SKILL.md @@ -0,0 +1,36 @@ +--- +name: godot-gdscript-patterns +description: "Master Godot 4 GDScript patterns including signals, scenes, state machines, and optimization. Use when building Godot games, implementing game systems, or learning GDScript best practices." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Godot GDScript Patterns + +Production patterns for Godot 4.x game development with GDScript, covering architecture, signals, scenes, and optimization. + +## Use this skill when + +- Building games with Godot 4 +- Implementing game systems in GDScript +- Designing scene architecture +- Managing game state +- Optimizing GDScript performance +- Learning Godot best practices + +## Do not use this skill when + +- The task is unrelated to godot gdscript patterns +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Resources + +- `resources/implementation-playbook.md` for detailed patterns and examples. diff --git a/plugins/antigravity-bundle-indie-game-dev/skills/godot-gdscript-patterns/resources/implementation-playbook.md b/plugins/antigravity-bundle-indie-game-dev/skills/godot-gdscript-patterns/resources/implementation-playbook.md new file mode 100644 index 00000000..84fcadb7 --- /dev/null +++ b/plugins/antigravity-bundle-indie-game-dev/skills/godot-gdscript-patterns/resources/implementation-playbook.md @@ -0,0 +1,804 @@ +# Godot GDScript Patterns Implementation Playbook + +This file contains detailed patterns, checklists, and code samples referenced by the skill. + +# Godot GDScript Patterns + +Production patterns for Godot 4.x game development with GDScript, covering architecture, signals, scenes, and optimization. + +## When to Use This Skill + +- Building games with Godot 4 +- Implementing game systems in GDScript +- Designing scene architecture +- Managing game state +- Optimizing GDScript performance +- Learning Godot best practices + +## Core Concepts + +### 1. Godot Architecture + +``` +Node: Base building block +โ”œโ”€โ”€ Scene: Reusable node tree (saved as .tscn) +โ”œโ”€โ”€ Resource: Data container (saved as .tres) +โ”œโ”€โ”€ Signal: Event communication +โ””โ”€โ”€ Group: Node categorization +``` + +### 2. GDScript Basics + +```gdscript +class_name Player +extends CharacterBody2D + +# Signals +signal health_changed(new_health: int) +signal died + +# Exports (Inspector-editable) +@export var speed: float = 200.0 +@export var max_health: int = 100 +@export_range(0, 1) var damage_reduction: float = 0.0 +@export_group("Combat") +@export var attack_damage: int = 10 +@export var attack_cooldown: float = 0.5 + +# Onready (initialized when ready) +@onready var sprite: Sprite2D = $Sprite2D +@onready var animation: AnimationPlayer = $AnimationPlayer +@onready var hitbox: Area2D = $Hitbox + +# Private variables (convention: underscore prefix) +var _health: int +var _can_attack: bool = true + +func _ready() -> void: + _health = max_health + +func _physics_process(delta: float) -> void: + var direction := Input.get_vector("left", "right", "up", "down") + velocity = direction * speed + move_and_slide() + +func take_damage(amount: int) -> void: + var actual_damage := int(amount * (1.0 - damage_reduction)) + _health = max(_health - actual_damage, 0) + health_changed.emit(_health) + + if _health <= 0: + died.emit() +``` + +## Patterns + +### Pattern 1: State Machine + +```gdscript +# state_machine.gd +class_name StateMachine +extends Node + +signal state_changed(from_state: StringName, to_state: StringName) + +@export var initial_state: State + +var current_state: State +var states: Dictionary = {} + +func _ready() -> void: + # Register all State children + for child in get_children(): + if child is State: + states[child.name] = child + child.state_machine = self + child.process_mode = Node.PROCESS_MODE_DISABLED + + # Start initial state + if initial_state: + current_state = initial_state + current_state.process_mode = Node.PROCESS_MODE_INHERIT + current_state.enter() + +func _process(delta: float) -> void: + if current_state: + current_state.update(delta) + +func _physics_process(delta: float) -> void: + if current_state: + current_state.physics_update(delta) + +func _unhandled_input(event: InputEvent) -> void: + if current_state: + current_state.handle_input(event) + +func transition_to(state_name: StringName, msg: Dictionary = {}) -> void: + if not states.has(state_name): + push_error("State '%s' not found" % state_name) + return + + var previous_state := current_state + previous_state.exit() + previous_state.process_mode = Node.PROCESS_MODE_DISABLED + + current_state = states[state_name] + current_state.process_mode = Node.PROCESS_MODE_INHERIT + current_state.enter(msg) + + state_changed.emit(previous_state.name, current_state.name) +``` + +```gdscript +# state.gd +class_name State +extends Node + +var state_machine: StateMachine + +func enter(_msg: Dictionary = {}) -> void: + pass + +func exit() -> void: + pass + +func update(_delta: float) -> void: + pass + +func physics_update(_delta: float) -> void: + pass + +func handle_input(_event: InputEvent) -> void: + pass +``` + +```gdscript +# player_idle.gd +class_name PlayerIdle +extends State + +@export var player: Player + +func enter(_msg: Dictionary = {}) -> void: + player.animation.play("idle") + +func physics_update(_delta: float) -> void: + var direction := Input.get_vector("left", "right", "up", "down") + + if direction != Vector2.ZERO: + state_machine.transition_to("Move") + +func handle_input(event: InputEvent) -> void: + if event.is_action_pressed("attack"): + state_machine.transition_to("Attack") + elif event.is_action_pressed("jump"): + state_machine.transition_to("Jump") +``` + +### Pattern 2: Autoload Singletons + +```gdscript +# game_manager.gd (Add to Project Settings > Autoload) +extends Node + +signal game_started +signal game_paused(is_paused: bool) +signal game_over(won: bool) +signal score_changed(new_score: int) + +enum GameState { MENU, PLAYING, PAUSED, GAME_OVER } + +var state: GameState = GameState.MENU +var score: int = 0: + set(value): + score = value + score_changed.emit(score) + +var high_score: int = 0 + +func _ready() -> void: + process_mode = Node.PROCESS_MODE_ALWAYS + _load_high_score() + +func _input(event: InputEvent) -> void: + if event.is_action_pressed("pause") and state == GameState.PLAYING: + toggle_pause() + +func start_game() -> void: + score = 0 + state = GameState.PLAYING + game_started.emit() + +func toggle_pause() -> void: + var is_paused := state != GameState.PAUSED + + if is_paused: + state = GameState.PAUSED + get_tree().paused = true + else: + state = GameState.PLAYING + get_tree().paused = false + + game_paused.emit(is_paused) + +func end_game(won: bool) -> void: + state = GameState.GAME_OVER + + if score > high_score: + high_score = score + _save_high_score() + + game_over.emit(won) + +func add_score(points: int) -> void: + score += points + +func _load_high_score() -> void: + if FileAccess.file_exists("user://high_score.save"): + var file := FileAccess.open("user://high_score.save", FileAccess.READ) + high_score = file.get_32() + +func _save_high_score() -> void: + var file := FileAccess.open("user://high_score.save", FileAccess.WRITE) + file.store_32(high_score) +``` + +```gdscript +# event_bus.gd (Global signal bus) +extends Node + +# Player events +signal player_spawned(player: Node2D) +signal player_died(player: Node2D) +signal player_health_changed(health: int, max_health: int) + +# Enemy events +signal enemy_spawned(enemy: Node2D) +signal enemy_died(enemy: Node2D, position: Vector2) + +# Item events +signal item_collected(item_type: StringName, value: int) +signal powerup_activated(powerup_type: StringName) + +# Level events +signal level_started(level_number: int) +signal level_completed(level_number: int, time: float) +signal checkpoint_reached(checkpoint_id: int) +``` + +### Pattern 3: Resource-based Data + +```gdscript +# weapon_data.gd +class_name WeaponData +extends Resource + +@export var name: StringName +@export var damage: int +@export var attack_speed: float +@export var range: float +@export_multiline var description: String +@export var icon: Texture2D +@export var projectile_scene: PackedScene +@export var sound_attack: AudioStream +``` + +```gdscript +# character_stats.gd +class_name CharacterStats +extends Resource + +signal stat_changed(stat_name: StringName, new_value: float) + +@export var max_health: float = 100.0 +@export var attack: float = 10.0 +@export var defense: float = 5.0 +@export var speed: float = 200.0 + +# Runtime values (not saved) +var _current_health: float + +func _init() -> void: + _current_health = max_health + +func get_current_health() -> float: + return _current_health + +func take_damage(amount: float) -> float: + var actual_damage := maxf(amount - defense, 1.0) + _current_health = maxf(_current_health - actual_damage, 0.0) + stat_changed.emit("health", _current_health) + return actual_damage + +func heal(amount: float) -> void: + _current_health = minf(_current_health + amount, max_health) + stat_changed.emit("health", _current_health) + +func duplicate_for_runtime() -> CharacterStats: + var copy := duplicate() as CharacterStats + copy._current_health = copy.max_health + return copy +``` + +```gdscript +# Using resources +class_name Character +extends CharacterBody2D + +@export var base_stats: CharacterStats +@export var weapon: WeaponData + +var stats: CharacterStats + +func _ready() -> void: + # Create runtime copy to avoid modifying the resource + stats = base_stats.duplicate_for_runtime() + stats.stat_changed.connect(_on_stat_changed) + +func attack() -> void: + if weapon: + print("Attacking with %s for %d damage" % [weapon.name, weapon.damage]) + +func _on_stat_changed(stat_name: StringName, value: float) -> void: + if stat_name == "health" and value <= 0: + die() +``` + +### Pattern 4: Object Pooling + +```gdscript +# object_pool.gd +class_name ObjectPool +extends Node + +@export var pooled_scene: PackedScene +@export var initial_size: int = 10 +@export var can_grow: bool = true + +var _available: Array[Node] = [] +var _in_use: Array[Node] = [] + +func _ready() -> void: + _initialize_pool() + +func _initialize_pool() -> void: + for i in initial_size: + _create_instance() + +func _create_instance() -> Node: + var instance := pooled_scene.instantiate() + instance.process_mode = Node.PROCESS_MODE_DISABLED + instance.visible = false + add_child(instance) + _available.append(instance) + + # Connect return signal if exists + if instance.has_signal("returned_to_pool"): + instance.returned_to_pool.connect(_return_to_pool.bind(instance)) + + return instance + +func get_instance() -> Node: + var instance: Node + + if _available.is_empty(): + if can_grow: + instance = _create_instance() + _available.erase(instance) + else: + push_warning("Pool exhausted and cannot grow") + return null + else: + instance = _available.pop_back() + + instance.process_mode = Node.PROCESS_MODE_INHERIT + instance.visible = true + _in_use.append(instance) + + if instance.has_method("on_spawn"): + instance.on_spawn() + + return instance + +func _return_to_pool(instance: Node) -> void: + if not instance in _in_use: + return + + _in_use.erase(instance) + + if instance.has_method("on_despawn"): + instance.on_despawn() + + instance.process_mode = Node.PROCESS_MODE_DISABLED + instance.visible = false + _available.append(instance) + +func return_all() -> void: + for instance in _in_use.duplicate(): + _return_to_pool(instance) +``` + +```gdscript +# pooled_bullet.gd +class_name PooledBullet +extends Area2D + +signal returned_to_pool + +@export var speed: float = 500.0 +@export var lifetime: float = 5.0 + +var direction: Vector2 +var _timer: float + +func on_spawn() -> void: + _timer = lifetime + +func on_despawn() -> void: + direction = Vector2.ZERO + +func initialize(pos: Vector2, dir: Vector2) -> void: + global_position = pos + direction = dir.normalized() + rotation = direction.angle() + +func _physics_process(delta: float) -> void: + position += direction * speed * delta + + _timer -= delta + if _timer <= 0: + returned_to_pool.emit() + +func _on_body_entered(body: Node2D) -> void: + if body.has_method("take_damage"): + body.take_damage(10) + returned_to_pool.emit() +``` + +### Pattern 5: Component System + +```gdscript +# health_component.gd +class_name HealthComponent +extends Node + +signal health_changed(current: int, maximum: int) +signal damaged(amount: int, source: Node) +signal healed(amount: int) +signal died + +@export var max_health: int = 100 +@export var invincibility_time: float = 0.0 + +var current_health: int: + set(value): + var old := current_health + current_health = clampi(value, 0, max_health) + if current_health != old: + health_changed.emit(current_health, max_health) + +var _invincible: bool = false + +func _ready() -> void: + current_health = max_health + +func take_damage(amount: int, source: Node = null) -> int: + if _invincible or current_health <= 0: + return 0 + + var actual := mini(amount, current_health) + current_health -= actual + damaged.emit(actual, source) + + if current_health <= 0: + died.emit() + elif invincibility_time > 0: + _start_invincibility() + + return actual + +func heal(amount: int) -> int: + var actual := mini(amount, max_health - current_health) + current_health += actual + if actual > 0: + healed.emit(actual) + return actual + +func _start_invincibility() -> void: + _invincible = true + await get_tree().create_timer(invincibility_time).timeout + _invincible = false +``` + +```gdscript +# hitbox_component.gd +class_name HitboxComponent +extends Area2D + +signal hit(hurtbox: HurtboxComponent) + +@export var damage: int = 10 +@export var knockback_force: float = 200.0 + +var owner_node: Node + +func _ready() -> void: + owner_node = get_parent() + area_entered.connect(_on_area_entered) + +func _on_area_entered(area: Area2D) -> void: + if area is HurtboxComponent: + var hurtbox := area as HurtboxComponent + if hurtbox.owner_node != owner_node: + hit.emit(hurtbox) + hurtbox.receive_hit(self) +``` + +```gdscript +# hurtbox_component.gd +class_name HurtboxComponent +extends Area2D + +signal hurt(hitbox: HitboxComponent) + +@export var health_component: HealthComponent + +var owner_node: Node + +func _ready() -> void: + owner_node = get_parent() + +func receive_hit(hitbox: HitboxComponent) -> void: + hurt.emit(hitbox) + + if health_component: + health_component.take_damage(hitbox.damage, hitbox.owner_node) +``` + +### Pattern 6: Scene Management + +```gdscript +# scene_manager.gd (Autoload) +extends Node + +signal scene_loading_started(scene_path: String) +signal scene_loading_progress(progress: float) +signal scene_loaded(scene: Node) +signal transition_started +signal transition_finished + +@export var transition_scene: PackedScene +@export var loading_scene: PackedScene + +var _current_scene: Node +var _transition: CanvasLayer +var _loader: ResourceLoader + +func _ready() -> void: + _current_scene = get_tree().current_scene + + if transition_scene: + _transition = transition_scene.instantiate() + add_child(_transition) + _transition.visible = false + +func change_scene(scene_path: String, with_transition: bool = true) -> void: + if with_transition: + await _play_transition_out() + + _load_scene(scene_path) + +func change_scene_packed(scene: PackedScene, with_transition: bool = true) -> void: + if with_transition: + await _play_transition_out() + + _swap_scene(scene.instantiate()) + +func _load_scene(path: String) -> void: + scene_loading_started.emit(path) + + # Check if already loaded + if ResourceLoader.has_cached(path): + var scene := load(path) as PackedScene + _swap_scene(scene.instantiate()) + return + + # Async loading + ResourceLoader.load_threaded_request(path) + + while true: + var progress := [] + var status := ResourceLoader.load_threaded_get_status(path, progress) + + match status: + ResourceLoader.THREAD_LOAD_IN_PROGRESS: + scene_loading_progress.emit(progress[0]) + await get_tree().process_frame + ResourceLoader.THREAD_LOAD_LOADED: + var scene := ResourceLoader.load_threaded_get(path) as PackedScene + _swap_scene(scene.instantiate()) + return + _: + push_error("Failed to load scene: %s" % path) + return + +func _swap_scene(new_scene: Node) -> void: + if _current_scene: + _current_scene.queue_free() + + _current_scene = new_scene + get_tree().root.add_child(_current_scene) + get_tree().current_scene = _current_scene + + scene_loaded.emit(_current_scene) + await _play_transition_in() + +func _play_transition_out() -> void: + if not _transition: + return + + transition_started.emit() + _transition.visible = true + + if _transition.has_method("transition_out"): + await _transition.transition_out() + else: + await get_tree().create_timer(0.3).timeout + +func _play_transition_in() -> void: + if not _transition: + transition_finished.emit() + return + + if _transition.has_method("transition_in"): + await _transition.transition_in() + else: + await get_tree().create_timer(0.3).timeout + + _transition.visible = false + transition_finished.emit() +``` + +### Pattern 7: Save System + +```gdscript +# save_manager.gd (Autoload) +extends Node + +const SAVE_PATH := "user://savegame.save" +const ENCRYPTION_KEY := "your_secret_key_here" + +signal save_completed +signal load_completed +signal save_error(message: String) + +func save_game(data: Dictionary) -> void: + var file := FileAccess.open_encrypted_with_pass( + SAVE_PATH, + FileAccess.WRITE, + ENCRYPTION_KEY + ) + + if file == null: + save_error.emit("Could not open save file") + return + + var json := JSON.stringify(data) + file.store_string(json) + file.close() + + save_completed.emit() + +func load_game() -> Dictionary: + if not FileAccess.file_exists(SAVE_PATH): + return {} + + var file := FileAccess.open_encrypted_with_pass( + SAVE_PATH, + FileAccess.READ, + ENCRYPTION_KEY + ) + + if file == null: + save_error.emit("Could not open save file") + return {} + + var json := file.get_as_text() + file.close() + + var parsed := JSON.parse_string(json) + if parsed == null: + save_error.emit("Could not parse save data") + return {} + + load_completed.emit() + return parsed + +func delete_save() -> void: + if FileAccess.file_exists(SAVE_PATH): + DirAccess.remove_absolute(SAVE_PATH) + +func has_save() -> bool: + return FileAccess.file_exists(SAVE_PATH) +``` + +```gdscript +# saveable.gd (Attach to saveable nodes) +class_name Saveable +extends Node + +@export var save_id: String + +func _ready() -> void: + if save_id.is_empty(): + save_id = str(get_path()) + +func get_save_data() -> Dictionary: + var parent := get_parent() + var data := {"id": save_id} + + if parent is Node2D: + data["position"] = {"x": parent.position.x, "y": parent.position.y} + + if parent.has_method("get_custom_save_data"): + data.merge(parent.get_custom_save_data()) + + return data + +func load_save_data(data: Dictionary) -> void: + var parent := get_parent() + + if data.has("position") and parent is Node2D: + parent.position = Vector2(data.position.x, data.position.y) + + if parent.has_method("load_custom_save_data"): + parent.load_custom_save_data(data) +``` + +## Performance Tips + +```gdscript +# 1. Cache node references +@onready var sprite := $Sprite2D # Good +# $Sprite2D in _process() # Bad - repeated lookup + +# 2. Use object pooling for frequent spawning +# See Pattern 4 + +# 3. Avoid allocations in hot paths +var _reusable_array: Array = [] + +func _process(_delta: float) -> void: + _reusable_array.clear() # Reuse instead of creating new + +# 4. Use static typing +func calculate(value: float) -> float: # Good + return value * 2.0 + +# 5. Disable processing when not needed +func _on_off_screen() -> void: + set_process(false) + set_physics_process(false) +``` + +## Best Practices + +### Do's +- **Use signals for decoupling** - Avoid direct references +- **Type everything** - Static typing catches errors +- **Use resources for data** - Separate data from logic +- **Pool frequently spawned objects** - Avoid GC hitches +- **Use Autoloads sparingly** - Only for truly global systems + +### Don'ts +- **Don't use `get_node()` in loops** - Cache references +- **Don't couple scenes tightly** - Use signals +- **Don't put logic in resources** - Keep them data-only +- **Don't ignore the Profiler** - Monitor performance +- **Don't fight the scene tree** - Work with Godot's design + +## Resources + +- [Godot Documentation](https://docs.godotengine.org/en/stable/) +- [GDQuest Tutorials](https://www.gdquest.com/) +- [Godot Recipes](https://kidscancode.org/godot_recipes/) diff --git a/plugins/antigravity-bundle-indie-game-dev/skills/unity-developer/SKILL.md b/plugins/antigravity-bundle-indie-game-dev/skills/unity-developer/SKILL.md new file mode 100644 index 00000000..717c53a7 --- /dev/null +++ b/plugins/antigravity-bundle-indie-game-dev/skills/unity-developer/SKILL.md @@ -0,0 +1,227 @@ +--- +name: unity-developer +description: Build Unity games with optimized C# scripts, efficient rendering, and proper asset management. Masters Unity 6 LTS, URP/HDRP pipelines, and cross-platform deployment. +risk: unknown +source: community +date_added: '2026-02-27' +--- + +## Use this skill when + +- Working on unity developer tasks or workflows +- Needing guidance, best practices, or checklists for unity developer + +## Do not use this skill when + +- The task is unrelated to unity developer +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +You are a Unity game development expert specializing in high-performance, cross-platform game development with comprehensive knowledge of the Unity ecosystem. + +## Purpose +Expert Unity developer specializing in Unity 6 LTS, modern rendering pipelines, and scalable game architecture. Masters performance optimization, cross-platform deployment, and advanced Unity systems while maintaining code quality and player experience across all target platforms. + +## Capabilities + +### Core Unity Mastery +- Unity 6 LTS features and Long-Term Support benefits +- Unity Editor customization and productivity workflows +- Unity Hub project management and version control integration +- Package Manager and custom package development +- Unity Asset Store integration and asset pipeline optimization +- Version control with Unity Collaborate, Git, and Perforce +- Unity Cloud Build and automated deployment pipelines +- Cross-platform build optimization and platform-specific configurations + +### Modern Rendering Pipelines +- Universal Render Pipeline (URP) optimization and customization +- High Definition Render Pipeline (HDRP) for high-fidelity graphics +- Built-in render pipeline legacy support and migration strategies +- Custom render features and renderer passes +- Shader Graph visual shader creation and optimization +- HLSL shader programming for advanced graphics effects +- Post-processing stack configuration and custom effects +- Lighting and shadow optimization for target platforms + +### Performance Optimization Excellence +- Unity Profiler mastery for CPU, GPU, and memory analysis +- Frame Debugger for rendering pipeline optimization +- Memory Profiler for heap and native memory management +- Physics optimization and collision detection efficiency +- LOD (Level of Detail) systems and automatic LOD generation +- Occlusion culling and frustum culling optimization +- Texture streaming and asset loading optimization +- Platform-specific performance tuning (mobile, console, PC) + +### Advanced C# Game Programming +- C# 9.0+ features and modern language patterns +- Unity-specific C# optimization techniques +- Job System and Burst Compiler for high-performance code +- Data-Oriented Technology Stack (DOTS) and ECS architecture +- Async/await patterns for Unity coroutines replacement +- Memory management and garbage collection optimization +- Custom attribute systems and reflection optimization +- Thread-safe programming and concurrent execution patterns + +### Game Architecture & Design Patterns +- Entity Component System (ECS) architecture implementation +- Model-View-Controller (MVC) patterns for UI and game logic +- Observer pattern for decoupled system communication +- State machines for character and game state management +- Object pooling for performance-critical scenarios +- Singleton pattern usage and dependency injection +- Service locator pattern for game service management +- Modular architecture for large-scale game projects + +### Asset Management & Optimization +- Addressable Assets System for dynamic content loading +- Asset bundles creation and management strategies +- Texture compression and format optimization +- Audio compression and 3D spatial audio implementation +- Animation system optimization and animation compression +- Mesh optimization and geometry level-of-detail +- Scriptable Objects for data-driven game design +- Asset dependency management and circular reference prevention + +### UI/UX Implementation +- UI Toolkit (formerly UI Elements) for modern UI development +- uGUI Canvas optimization and UI performance tuning +- Responsive UI design for multiple screen resolutions +- Accessibility features and inclusive design implementation +- Input System integration for multi-platform input handling +- UI animation and transition systems +- Localization and internationalization support +- User experience optimization for different platforms + +### Physics & Animation Systems +- Unity Physics and Havok Physics integration +- Custom physics solutions and collision detection +- 2D and 3D physics optimization techniques +- Animation state machines and blend trees +- Timeline system for cutscenes and scripted sequences +- Cinemachine camera system for dynamic cinematography +- IK (Inverse Kinematics) systems and procedural animation +- Particle systems and visual effects optimization + +### Networking & Multiplayer +- Unity Netcode for GameObjects multiplayer framework +- Dedicated server architecture and matchmaking +- Client-server synchronization and lag compensation +- Network optimization and bandwidth management +- Mirror Networking alternative multiplayer solutions +- Relay and lobby services integration +- Cross-platform multiplayer implementation +- Real-time communication and voice chat integration + +### Platform-Specific Development +- **Mobile Optimization**: iOS/Android performance tuning and platform features +- **Console Development**: PlayStation, Xbox, and Nintendo Switch optimization +- **PC Gaming**: Steam integration and Windows-specific optimizations +- **WebGL**: Web deployment optimization and browser compatibility +- **VR/AR Development**: XR Toolkit and platform-specific VR/AR features +- Platform store integration and certification requirements +- Platform-specific input handling and UI adaptations +- Performance profiling on target hardware + +### Advanced Graphics & Shaders +- Shader Graph for visual shader creation and prototyping +- HLSL shader programming for custom effects +- Compute shaders for GPU-accelerated processing +- Custom lighting models and PBR material workflows +- Real-time ray tracing and path tracing integration +- Visual effects with VFX Graph for high-performance particles +- HDR and tone mapping for cinematic visuals +- Custom post-processing effects and screen-space techniques + +### Audio Implementation +- Unity Audio System and Audio Mixer optimization +- 3D spatial audio and HRTF implementation +- Audio occlusion and reverberation systems +- Dynamic music systems and adaptive audio +- Wwise and FMOD integration for advanced audio +- Audio streaming and compression optimization +- Platform-specific audio optimization +- Accessibility features for hearing-impaired players + +### Quality Assurance & Testing +- Unity Test Framework for automated testing +- Play mode and edit mode testing strategies +- Performance benchmarking and regression testing +- Memory leak detection and prevention +- Unity Cloud Build automated testing integration +- Device testing across multiple platforms and hardware +- Crash reporting and analytics integration +- User acceptance testing and feedback integration + +### DevOps & Deployment +- Unity Cloud Build for continuous integration +- Version control workflows with Git LFS for large assets +- Automated build pipelines and deployment strategies +- Platform-specific build configurations and signing +- Asset server management and team collaboration +- Code review processes and quality gates +- Release management and patch deployment +- Analytics integration and player behavior tracking + +### Advanced Unity Systems +- Custom tools and editor scripting for productivity +- Scriptable render features and custom render passes +- Unity Services integration (Analytics, Cloud Build, IAP) +- Addressable content management and remote asset delivery +- Custom package development and distribution +- Unity Collaborate and version control integration +- Profiling and debugging advanced techniques +- Memory optimization and garbage collection tuning + +## Behavioral Traits +- Prioritizes performance optimization from project start +- Implements scalable architecture patterns for team development +- Uses Unity Profiler proactively to identify bottlenecks +- Writes clean, maintainable C# code with proper documentation +- Considers target platform limitations in design decisions +- Implements comprehensive error handling and logging +- Follows Unity coding standards and naming conventions +- Plans asset organization and pipeline from project inception +- Tests gameplay features across all target platforms +- Keeps current with Unity roadmap and feature updates + +## Knowledge Base +- Unity 6 LTS roadmap and long-term support benefits +- Modern rendering pipeline architecture and optimization +- Cross-platform game development challenges and solutions +- Performance optimization techniques for mobile and console +- Game architecture patterns and scalable design principles +- Unity Services ecosystem and cloud-based solutions +- Platform certification requirements and store policies +- Accessibility standards and inclusive game design +- Game monetization strategies and implementation +- Emerging technologies integration (VR/AR, AI, blockchain) + +## Response Approach +1. **Analyze requirements** for optimal Unity architecture and pipeline choice +2. **Recommend performance-optimized solutions** using modern Unity features +3. **Provide production-ready C# code** with proper error handling and logging +4. **Include cross-platform considerations** and platform-specific optimizations +5. **Consider scalability** for team development and project growth +6. **Implement comprehensive testing** strategies for quality assurance +7. **Address memory management** and performance implications +8. **Plan deployment strategies** for target platforms and stores + +## Example Interactions +- "Architect a multiplayer game with Unity Netcode and dedicated servers" +- "Optimize mobile game performance using URP and LOD systems" +- "Create a custom shader with Shader Graph for stylized rendering" +- "Implement ECS architecture for high-performance gameplay systems" +- "Set up automated build pipeline with Unity Cloud Build" +- "Design asset streaming system with Addressable Assets" +- "Create custom Unity tools for level design and content creation" +- "Optimize physics simulation for large-scale battle scenarios" + +Focus on performance-optimized, maintainable solutions using Unity 6 LTS features. Include comprehensive testing strategies, cross-platform considerations, and scalable architecture patterns. diff --git a/plugins/antigravity-bundle-integration-apis/.codex-plugin/plugin.json b/plugins/antigravity-bundle-integration-apis/.codex-plugin/plugin.json new file mode 100644 index 00000000..52e9d782 --- /dev/null +++ b/plugins/antigravity-bundle-integration-apis/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-integration-apis", + "version": "8.10.0", + "description": "Install the \"Integration & APIs\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "integration-apis", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Integration & APIs", + "shortDescription": "Specialized Packs ยท 5 curated skills", + "longDescription": "For connecting services and building integrations. Covers Stripe Integration, Twilio Communications, and 3 more skills.", + "developerName": "sickn33 and contributors", + "category": "Specialized Packs", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-integration-apis/skills/algolia-search/SKILL.md b/plugins/antigravity-bundle-integration-apis/skills/algolia-search/SKILL.md new file mode 100644 index 00000000..15284c07 --- /dev/null +++ b/plugins/antigravity-bundle-integration-apis/skills/algolia-search/SKILL.md @@ -0,0 +1,68 @@ +--- +name: algolia-search +description: "Expert patterns for Algolia search implementation, indexing strategies, React InstantSearch, and relevance tuning Use when: adding search to, algolia, instantsearch, search api, search functionality." +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# Algolia Search Integration + +## Patterns + +### React InstantSearch with Hooks + +Modern React InstantSearch setup using hooks for type-ahead search. + +Uses react-instantsearch-hooks-web package with algoliasearch client. +Widgets are components that can be customized with classnames. + +Key hooks: +- useSearchBox: Search input handling +- useHits: Access search results +- useRefinementList: Facet filtering +- usePagination: Result pagination +- useInstantSearch: Full state access + +### Next.js Server-Side Rendering + +SSR integration for Next.js with react-instantsearch-nextjs package. + +Use instead of for SSR. +Supports both Pages Router and App Router (experimental). + +Key considerations: +- Set dynamic = 'force-dynamic' for fresh results +- Handle URL synchronization with routing prop +- Use getServerState for initial state + +### Data Synchronization and Indexing + +Indexing strategies for keeping Algolia in sync with your data. + +Three main approaches: +1. Full Reindexing - Replace entire index (expensive) +2. Full Record Updates - Replace individual records +3. Partial Updates - Update specific attributes only + +Best practices: +- Batch records (ideal: 10MB, 1K-10K records per batch) +- Use incremental updates when possible +- partialUpdateObjects for attribute-only changes +- Avoid deleteBy (computationally expensive) + +## โš ๏ธ Sharp Edges + +| Issue | Severity | Solution | +|-------|----------|----------| +| Issue | critical | See docs | +| Issue | high | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-integration-apis/skills/hubspot-integration/SKILL.md b/plugins/antigravity-bundle-integration-apis/skills/hubspot-integration/SKILL.md new file mode 100644 index 00000000..a622711a --- /dev/null +++ b/plugins/antigravity-bundle-integration-apis/skills/hubspot-integration/SKILL.md @@ -0,0 +1,47 @@ +--- +name: hubspot-integration +description: "Authentication for single-account integrations" +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# HubSpot Integration + +## Patterns + +### OAuth 2.0 Authentication + +Secure authentication for public apps + +### Private App Token + +Authentication for single-account integrations + +### CRM Object CRUD Operations + +Create, read, update, delete CRM records + +## Anti-Patterns + +### โŒ Using Deprecated API Keys + +### โŒ Individual Requests Instead of Batch + +### โŒ Polling Instead of Webhooks + +## โš ๏ธ Sharp Edges + +| Issue | Severity | Solution | +|-------|----------|----------| +| Issue | high | See docs | +| Issue | high | See docs | +| Issue | critical | See docs | +| Issue | high | See docs | +| Issue | critical | See docs | +| Issue | medium | See docs | +| Issue | high | See docs | +| Issue | medium | See docs | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-integration-apis/skills/plaid-fintech/SKILL.md b/plugins/antigravity-bundle-integration-apis/skills/plaid-fintech/SKILL.md new file mode 100644 index 00000000..298595c6 --- /dev/null +++ b/plugins/antigravity-bundle-integration-apis/skills/plaid-fintech/SKILL.md @@ -0,0 +1,52 @@ +--- +name: plaid-fintech +description: "Create a linktoken for Plaid Link, exchange publictoken for accesstoken. Link tokens are short-lived, one-time use. Access tokens don't expire but may need updating when users change passwords." +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# Plaid Fintech + +## Patterns + +### Link Token Creation and Exchange + +Create a link_token for Plaid Link, exchange public_token for access_token. +Link tokens are short-lived, one-time use. Access tokens don't expire but +may need updating when users change passwords. + +### Transactions Sync + +Use /transactions/sync for incremental transaction updates. More efficient +than /transactions/get. Handle webhooks for real-time updates instead of +polling. + +### Item Error Handling and Update Mode + +Handle ITEM_LOGIN_REQUIRED errors by putting users through Link update mode. +Listen for PENDING_DISCONNECT webhook to proactively prompt users. + +## Anti-Patterns + +### โŒ Storing Access Tokens in Plain Text + +### โŒ Polling Instead of Webhooks + +### โŒ Ignoring Item Errors + +## โš ๏ธ Sharp Edges + +| Issue | Severity | Solution | +|-------|----------|----------| +| Issue | critical | See docs | +| Issue | high | See docs | +| Issue | high | See docs | +| Issue | high | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | +| Issue | medium | See docs | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-integration-apis/skills/stripe-integration/SKILL.md b/plugins/antigravity-bundle-integration-apis/skills/stripe-integration/SKILL.md new file mode 100644 index 00000000..faa7f3e7 --- /dev/null +++ b/plugins/antigravity-bundle-integration-apis/skills/stripe-integration/SKILL.md @@ -0,0 +1,457 @@ +--- +name: stripe-integration +description: "Master Stripe payment processing integration for robust, PCI-compliant payment flows including checkout, subscriptions, webhooks, and refunds." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Stripe Integration + +Master Stripe payment processing integration for robust, PCI-compliant payment flows including checkout, subscriptions, webhooks, and refunds. + +## Do not use this skill when + +- The task is unrelated to stripe integration +- You need a different domain or tool outside this scope + +## Instructions + +- Clarify goals, constraints, and required inputs. +- Apply relevant best practices and validate outcomes. +- Provide actionable steps and verification. +- If detailed examples are required, open `resources/implementation-playbook.md`. + +## Use this skill when + +- Implementing payment processing in web/mobile applications +- Setting up subscription billing systems +- Handling one-time payments and recurring charges +- Processing refunds and disputes +- Managing customer payment methods +- Implementing SCA (Strong Customer Authentication) for European payments +- Building marketplace payment flows with Stripe Connect + +## Core Concepts + +### 1. Payment Flows +**Checkout Session (Hosted)** +- Stripe-hosted payment page +- Minimal PCI compliance burden +- Fastest implementation +- Supports one-time and recurring payments + +**Payment Intents (Custom UI)** +- Full control over payment UI +- Requires Stripe.js for PCI compliance +- More complex implementation +- Better customization options + +**Setup Intents (Save Payment Methods)** +- Collect payment method without charging +- Used for subscriptions and future payments +- Requires customer confirmation + +### 2. Webhooks +**Critical Events:** +- `payment_intent.succeeded`: Payment completed +- `payment_intent.payment_failed`: Payment failed +- `customer.subscription.updated`: Subscription changed +- `customer.subscription.deleted`: Subscription canceled +- `charge.refunded`: Refund processed +- `invoice.payment_succeeded`: Subscription payment successful + +### 3. Subscriptions +**Components:** +- **Product**: What you're selling +- **Price**: How much and how often +- **Subscription**: Customer's recurring payment +- **Invoice**: Generated for each billing cycle + +### 4. Customer Management +- Create and manage customer records +- Store multiple payment methods +- Track customer metadata +- Manage billing details + +## Quick Start + +```python +import stripe + +stripe.api_key = "sk_test_..." + +# Create a checkout session +session = stripe.checkout.Session.create( + payment_method_types=['card'], + line_items=[{ + 'price_data': { + 'currency': 'usd', + 'product_data': { + 'name': 'Premium Subscription', + }, + 'unit_amount': 2000, # $20.00 + 'recurring': { + 'interval': 'month', + }, + }, + 'quantity': 1, + }], + mode='subscription', + success_url='https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}', + cancel_url='https://yourdomain.com/cancel', +) + +# Redirect user to session.url +print(session.url) +``` + +## Payment Implementation Patterns + +### Pattern 1: One-Time Payment (Hosted Checkout) +```python +def create_checkout_session(amount, currency='usd'): + """Create a one-time payment checkout session.""" + try: + session = stripe.checkout.Session.create( + payment_method_types=['card'], + line_items=[{ + 'price_data': { + 'currency': currency, + 'product_data': { + 'name': 'Purchase', + 'images': ['https://example.com/product.jpg'], + }, + 'unit_amount': amount, # Amount in cents + }, + 'quantity': 1, + }], + mode='payment', + success_url='https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}', + cancel_url='https://yourdomain.com/cancel', + metadata={ + 'order_id': 'order_123', + 'user_id': 'user_456' + } + ) + return session + except stripe.error.StripeError as e: + # Handle error + print(f"Stripe error: {e.user_message}") + raise +``` + +### Pattern 2: Custom Payment Intent Flow +```python +def create_payment_intent(amount, currency='usd', customer_id=None): + """Create a payment intent for custom checkout UI.""" + intent = stripe.PaymentIntent.create( + amount=amount, + currency=currency, + customer=customer_id, + automatic_payment_methods={ + 'enabled': True, + }, + metadata={ + 'integration_check': 'accept_a_payment' + } + ) + return intent.client_secret # Send to frontend + +# Frontend (JavaScript) +""" +const stripe = Stripe('pk_test_...'); +const elements = stripe.elements(); +const cardElement = elements.create('card'); +cardElement.mount('#card-element'); + +const {error, paymentIntent} = await stripe.confirmCardPayment( + clientSecret, + { + payment_method: { + card: cardElement, + billing_details: { + name: 'Customer Name' + } + } + } +); + +if (error) { + // Handle error +} else if (paymentIntent.status === 'succeeded') { + // Payment successful +} +""" +``` + +### Pattern 3: Subscription Creation +```python +def create_subscription(customer_id, price_id): + """Create a subscription for a customer.""" + try: + subscription = stripe.Subscription.create( + customer=customer_id, + items=[{'price': price_id}], + payment_behavior='default_incomplete', + payment_settings={'save_default_payment_method': 'on_subscription'}, + expand=['latest_invoice.payment_intent'], + ) + + return { + 'subscription_id': subscription.id, + 'client_secret': subscription.latest_invoice.payment_intent.client_secret + } + except stripe.error.StripeError as e: + print(f"Subscription creation failed: {e}") + raise +``` + +### Pattern 4: Customer Portal +```python +def create_customer_portal_session(customer_id): + """Create a portal session for customers to manage subscriptions.""" + session = stripe.billing_portal.Session.create( + customer=customer_id, + return_url='https://yourdomain.com/account', + ) + return session.url # Redirect customer here +``` + +## Webhook Handling + +### Secure Webhook Endpoint +```python +from flask import Flask, request +import stripe + +app = Flask(__name__) + +endpoint_secret = 'whsec_...' + +@app.route('/webhook', methods=['POST']) +def webhook(): + payload = request.data + sig_header = request.headers.get('Stripe-Signature') + + try: + event = stripe.Webhook.construct_event( + payload, sig_header, endpoint_secret + ) + except ValueError: + # Invalid payload + return 'Invalid payload', 400 + except stripe.error.SignatureVerificationError: + # Invalid signature + return 'Invalid signature', 400 + + # Handle the event + if event['type'] == 'payment_intent.succeeded': + payment_intent = event['data']['object'] + handle_successful_payment(payment_intent) + elif event['type'] == 'payment_intent.payment_failed': + payment_intent = event['data']['object'] + handle_failed_payment(payment_intent) + elif event['type'] == 'customer.subscription.deleted': + subscription = event['data']['object'] + handle_subscription_canceled(subscription) + + return 'Success', 200 + +def handle_successful_payment(payment_intent): + """Process successful payment.""" + customer_id = payment_intent.get('customer') + amount = payment_intent['amount'] + metadata = payment_intent.get('metadata', {}) + + # Update your database + # Send confirmation email + # Fulfill order + print(f"Payment succeeded: {payment_intent['id']}") + +def handle_failed_payment(payment_intent): + """Handle failed payment.""" + error = payment_intent.get('last_payment_error', {}) + print(f"Payment failed: {error.get('message')}") + # Notify customer + # Update order status + +def handle_subscription_canceled(subscription): + """Handle subscription cancellation.""" + customer_id = subscription['customer'] + # Update user access + # Send cancellation email + print(f"Subscription canceled: {subscription['id']}") +``` + +### Webhook Best Practices +```python +import hashlib +import hmac + +def verify_webhook_signature(payload, signature, secret): + """Manually verify webhook signature.""" + expected_sig = hmac.new( + secret.encode('utf-8'), + payload, + hashlib.sha256 + ).hexdigest() + + return hmac.compare_digest(signature, expected_sig) + +def handle_webhook_idempotently(event_id, handler): + """Ensure webhook is processed exactly once.""" + # Check if event already processed + if is_event_processed(event_id): + return + + # Process event + try: + handler() + mark_event_processed(event_id) + except Exception as e: + log_error(e) + # Stripe will retry failed webhooks + raise +``` + +## Customer Management + +```python +def create_customer(email, name, payment_method_id=None): + """Create a Stripe customer.""" + customer = stripe.Customer.create( + email=email, + name=name, + payment_method=payment_method_id, + invoice_settings={ + 'default_payment_method': payment_method_id + } if payment_method_id else None, + metadata={ + 'user_id': '12345' + } + ) + return customer + +def attach_payment_method(customer_id, payment_method_id): + """Attach a payment method to a customer.""" + stripe.PaymentMethod.attach( + payment_method_id, + customer=customer_id + ) + + # Set as default + stripe.Customer.modify( + customer_id, + invoice_settings={ + 'default_payment_method': payment_method_id + } + ) + +def list_customer_payment_methods(customer_id): + """List all payment methods for a customer.""" + payment_methods = stripe.PaymentMethod.list( + customer=customer_id, + type='card' + ) + return payment_methods.data +``` + +## Refund Handling + +```python +def create_refund(payment_intent_id, amount=None, reason=None): + """Create a refund.""" + refund_params = { + 'payment_intent': payment_intent_id + } + + if amount: + refund_params['amount'] = amount # Partial refund + + if reason: + refund_params['reason'] = reason # 'duplicate', 'fraudulent', 'requested_by_customer' + + refund = stripe.Refund.create(**refund_params) + return refund + +def handle_dispute(charge_id, evidence): + """Update dispute with evidence.""" + stripe.Dispute.modify( + charge_id, + evidence={ + 'customer_name': evidence.get('customer_name'), + 'customer_email_address': evidence.get('customer_email'), + 'shipping_documentation': evidence.get('shipping_proof'), + 'customer_communication': evidence.get('communication'), + } + ) +``` + +## Testing + +```python +# Use test mode keys +stripe.api_key = "sk_test_..." + +# Test card numbers +TEST_CARDS = { + 'success': '4242424242424242', + 'declined': '4000000000000002', + '3d_secure': '4000002500003155', + 'insufficient_funds': '4000000000009995' +} + +def test_payment_flow(): + """Test complete payment flow.""" + # Create test customer + customer = stripe.Customer.create( + email="test@example.com" + ) + + # Create payment intent + intent = stripe.PaymentIntent.create( + amount=1000, + currency='usd', + customer=customer.id, + payment_method_types=['card'] + ) + + # Confirm with test card + confirmed = stripe.PaymentIntent.confirm( + intent.id, + payment_method='pm_card_visa' # Test payment method + ) + + assert confirmed.status == 'succeeded' +``` + +## Resources + +- **references/checkout-flows.md**: Detailed checkout implementation +- **references/webhook-handling.md**: Webhook security and processing +- **references/subscription-management.md**: Subscription lifecycle +- **references/customer-management.md**: Customer and payment method handling +- **references/invoice-generation.md**: Invoicing and billing +- **assets/stripe-client.py**: Production-ready Stripe client wrapper +- **assets/webhook-handler.py**: Complete webhook processor +- **assets/checkout-config.json**: Checkout configuration templates + +## Best Practices + +1. **Always Use Webhooks**: Don't rely solely on client-side confirmation +2. **Idempotency**: Handle webhook events idempotently +3. **Error Handling**: Gracefully handle all Stripe errors +4. **Test Mode**: Thoroughly test with test keys before production +5. **Metadata**: Use metadata to link Stripe objects to your database +6. **Monitoring**: Track payment success rates and errors +7. **PCI Compliance**: Never handle raw card data on your server +8. **SCA Ready**: Implement 3D Secure for European payments + +## Common Pitfalls + +- **Not Verifying Webhooks**: Always verify webhook signatures +- **Missing Webhook Events**: Handle all relevant webhook events +- **Hardcoded Amounts**: Use cents/smallest currency unit +- **No Retry Logic**: Implement retries for API calls +- **Ignoring Test Mode**: Test all edge cases with test cards diff --git a/plugins/antigravity-bundle-integration-apis/skills/twilio-communications/SKILL.md b/plugins/antigravity-bundle-integration-apis/skills/twilio-communications/SKILL.md new file mode 100644 index 00000000..b5334218 --- /dev/null +++ b/plugins/antigravity-bundle-integration-apis/skills/twilio-communications/SKILL.md @@ -0,0 +1,300 @@ +--- +name: twilio-communications +description: "Basic pattern for sending SMS messages with Twilio. Handles the fundamentals: phone number formatting, message delivery, and delivery status callbacks." +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# Twilio Communications + +## Patterns + +### SMS Sending Pattern + +Basic pattern for sending SMS messages with Twilio. +Handles the fundamentals: phone number formatting, message delivery, +and delivery status callbacks. + +Key considerations: +- Phone numbers must be in E.164 format (+1234567890) +- Default rate limit: 80 messages per second (MPS) +- Messages over 160 characters are split (and cost more) +- Carrier filtering can block messages (especially to US numbers) + + +**When to use**: ['Sending notifications to users', 'Transactional messages (order confirmations, shipping)', 'Alerts and reminders'] + +```python +from twilio.rest import Client +from twilio.base.exceptions import TwilioRestException +import os +import re + +class TwilioSMS: + """ + SMS sending with proper error handling and validation. + """ + + def __init__(self): + self.client = Client( + os.environ["TWILIO_ACCOUNT_SID"], + os.environ["TWILIO_AUTH_TOKEN"] + ) + self.from_number = os.environ["TWILIO_PHONE_NUMBER"] + + def validate_e164(self, phone: str) -> bool: + """Validate phone number is in E.164 format.""" + pattern = r'^\+[1-9]\d{1,14}$' + return bool(re.match(pattern, phone)) + + def send_sms( + self, + to: str, + body: str, + status_callback: str = None + ) -> dict: + """ + Send an SMS message. + + Args: + to: Recipient phone number in E.164 format + body: Message text (160 chars = 1 segment) + status_callback: URL for delivery status webhooks + + Returns: + Message SID and status + """ + # Validate phone number format + if not self.validate_e164(to): + return { + "success": False, + "error": "Phone number must be in E.164 format (+1234567890)" + } + + # Check message length (warn about segmentation) + segment_count = (len(body) + 159) // 160 + if segment_count > 1: + print(f"Warning: Message will be sent as {segment_count} segments") + + try: + message = self.client.messages.create( + to=to, + from_=self.from_number, + body=body, + status_callback=status_callback + ) + + return { + "success": True, + "message_sid": message.sid, + "status": message.status, + "segments": segment_count + } + + except TwilioRestException as e: + return self._handle_error(e) + + def _handle_error(self, error: Twilio +``` + +### Twilio Verify Pattern (2FA/OTP) + +Use Twilio Verify for phone number verification and 2FA. +Handles code generation, delivery, rate limiting, and fraud prevention. + +Key benefits over DIY OTP: +- Twilio manages code generation and expiration +- Built-in fraud prevention (saved customers $82M+ blocking 747M attempts) +- Handles rate limiting automatically +- Multi-channel: SMS, Voice, Email, Push, WhatsApp + +Google found SMS 2FA blocks "100% of automated bots, 96% of bulk +phishing attacks, and 76% of targeted attacks." + + +**When to use**: ['User phone number verification at signup', 'Two-factor authentication (2FA)', 'Password reset verification', 'High-value transaction confirmation'] + +```python +from twilio.rest import Client +from twilio.base.exceptions import TwilioRestException +import os +from enum import Enum +from typing import Optional + +class VerifyChannel(Enum): + SMS = "sms" + CALL = "call" + EMAIL = "email" + WHATSAPP = "whatsapp" + +class TwilioVerify: + """ + Phone verification with Twilio Verify. + Never store OTP codes - Twilio handles it. + """ + + def __init__(self, verify_service_sid: str = None): + self.client = Client( + os.environ["TWILIO_ACCOUNT_SID"], + os.environ["TWILIO_AUTH_TOKEN"] + ) + # Create a Verify Service in Twilio Console first + self.service_sid = verify_service_sid or os.environ["TWILIO_VERIFY_SID"] + + def send_verification( + self, + to: str, + channel: VerifyChannel = VerifyChannel.SMS, + locale: str = "en" + ) -> dict: + """ + Send verification code to phone/email. + + Args: + to: Phone number (E.164) or email + channel: SMS, call, email, or whatsapp + locale: Language code for message + + Returns: + Verification status + """ + try: + verification = self.client.verify \ + .v2 \ + .services(self.service_sid) \ + .verifications \ + .create( + to=to, + channel=channel.value, + locale=locale + ) + + return { + "success": True, + "status": verification.status, # "pending" + "channel": channel.value, + "valid": verification.valid + } + + except TwilioRestException as e: + return self._handle_verify_error(e) + + def check_verification(self, to: str, code: str) -> dict: + """ + Check if verification code is correct. + + Args: + to: Phone number or email that received code + code: The code entered by user + + R +``` + +### TwiML IVR Pattern + +Build Interactive Voice Response (IVR) systems using TwiML. +TwiML (Twilio Markup Language) is XML that tells Twilio what to do +when receiving calls. + +Core TwiML verbs: +- : Text-to-speech +- : Play audio file +- : Collect keypad/speech input +- : Connect to another number +- : Record caller's voice +- : Move to another TwiML endpoint + +Key insight: Twilio makes HTTP request to your webhook, you return +TwiML, Twilio executes it. Stateless, so use URL params or sessions. + + +**When to use**: ['Phone menu systems (press 1 for sales...)', 'Automated customer support', 'Appointment reminders with confirmation', 'Voicemail systems'] + +```python +from flask import Flask, request, Response +from twilio.twiml.voice_response import VoiceResponse, Gather +from twilio.request_validator import RequestValidator +import os + +app = Flask(__name__) + +def validate_twilio_request(f): + """Decorator to validate requests are from Twilio.""" + def wrapper(*args, **kwargs): + validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"]) + + # Get request details + url = request.url + params = request.form.to_dict() + signature = request.headers.get("X-Twilio-Signature", "") + + if not validator.validate(url, params, signature): + return "Invalid request", 403 + + return f(*args, **kwargs) + wrapper.__name__ = f.__name__ + return wrapper + +@app.route("/voice/incoming", methods=["POST"]) +@validate_twilio_request +def incoming_call(): + """Handle incoming call with IVR menu.""" + response = VoiceResponse() + + # Gather digits with timeout + gather = Gather( + num_digits=1, + action="/voice/menu-selection", + method="POST", + timeout=5 + ) + gather.say( + "Welcome to Acme Corp. " + "Press 1 for sales. " + "Press 2 for support. " + "Press 3 to leave a message." + ) + response.append(gather) + + # If no input, repeat + response.redirect("/voice/incoming") + + return Response(str(response), mimetype="text/xml") + +@app.route("/voice/menu-selection", methods=["POST"]) +@validate_twilio_request +def menu_selection(): + """Route based on menu selection.""" + response = VoiceResponse() + digit = request.form.get("Digits", "") + + if digit == "1": + # Transfer to sales + response.say("Connecting you to sales.") + response.dial(os.environ["SALES_PHONE"]) + + elif digit == "2": + # Transfer to support + response.say("Connecting you to support.") + response.dial(os.environ["SUPPORT_PHONE"]) + + elif digit == "3": + # Voicemail + response.say("Please leave a message after +``` + +## โš ๏ธ Sharp Edges + +| Issue | Severity | Solution | +|-------|----------|----------| +| Issue | high | ## Track opt-out status in your database | +| Issue | medium | ## Implement retry logic for transient failures | +| Issue | high | ## Register for A2P 10DLC (US requirement) | +| Issue | critical | ## ALWAYS validate the signature | +| Issue | high | ## Track session windows per user | +| Issue | critical | ## Never hardcode credentials | +| Issue | medium | ## Implement application-level rate limiting too | + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-llm-application-developer/.codex-plugin/plugin.json b/plugins/antigravity-bundle-llm-application-developer/.codex-plugin/plugin.json new file mode 100644 index 00000000..cbb97e35 --- /dev/null +++ b/plugins/antigravity-bundle-llm-application-developer/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-llm-application-developer", + "version": "8.10.0", + "description": "Install the \"LLM Application Developer\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "llm-application-developer", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "LLM Application Developer", + "shortDescription": "AI & Agents ยท 5 curated skills", + "longDescription": "For building production LLM applications. Covers LLM App Patterns, RAG Implementation, and 3 more skills.", + "developerName": "sickn33 and contributors", + "category": "AI & Agents", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-llm-application-developer/skills/context-window-management/SKILL.md b/plugins/antigravity-bundle-llm-application-developer/skills/context-window-management/SKILL.md new file mode 100644 index 00000000..fa4717dd --- /dev/null +++ b/plugins/antigravity-bundle-llm-application-developer/skills/context-window-management/SKILL.md @@ -0,0 +1,58 @@ +--- +name: context-window-management +description: "You're a context engineering specialist who has optimized LLM applications handling millions of conversations. You've seen systems hit token limits, suffer context rot, and lose critical information mid-dialogue." +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# Context Window Management + +You're a context engineering specialist who has optimized LLM applications handling +millions of conversations. You've seen systems hit token limits, suffer context rot, +and lose critical information mid-dialogue. + +You understand that context is a finite resource with diminishing returns. More tokens +doesn't mean better resultsโ€”the art is in curating the right information. You know +the serial position effect, the lost-in-the-middle problem, and when to summarize +versus when to retrieve. + +Your cor + +## Capabilities + +- context-engineering +- context-summarization +- context-trimming +- context-routing +- token-counting +- context-prioritization + +## Patterns + +### Tiered Context Strategy + +Different strategies based on context size + +### Serial Position Optimization + +Place important content at start and end + +### Intelligent Summarization + +Summarize by importance, not just recency + +## Anti-Patterns + +### โŒ Naive Truncation + +### โŒ Ignoring Token Costs + +### โŒ One-Size-Fits-All + +## Related Skills + +Works well with: `rag-implementation`, `conversation-memory`, `prompt-caching`, `llm-npc-dialogue` + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-llm-application-developer/skills/langfuse/SKILL.md b/plugins/antigravity-bundle-llm-application-developer/skills/langfuse/SKILL.md new file mode 100644 index 00000000..5df81bba --- /dev/null +++ b/plugins/antigravity-bundle-llm-application-developer/skills/langfuse/SKILL.md @@ -0,0 +1,243 @@ +--- +name: langfuse +description: "You are an expert in LLM observability and evaluation. You think in terms of traces, spans, and metrics. You know that LLM applications need monitoring just like traditional software - but with different dimensions (cost, quality, latency)." +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# Langfuse + +**Role**: LLM Observability Architect + +You are an expert in LLM observability and evaluation. You think in terms of +traces, spans, and metrics. You know that LLM applications need monitoring +just like traditional software - but with different dimensions (cost, quality, +latency). You use data to drive prompt improvements and catch regressions. + +## Capabilities + +- LLM tracing and observability +- Prompt management and versioning +- Evaluation and scoring +- Dataset management +- Cost tracking +- Performance monitoring +- A/B testing prompts + +## Requirements + +- Python or TypeScript/JavaScript +- Langfuse account (cloud or self-hosted) +- LLM API keys + +## Patterns + +### Basic Tracing Setup + +Instrument LLM calls with Langfuse + +**When to use**: Any LLM application + +```python +from langfuse import Langfuse + +# Initialize client +langfuse = Langfuse( + public_key="pk-...", + secret_key="sk-...", + host="https://cloud.langfuse.com" # or self-hosted URL +) + +# Create a trace for a user request +trace = langfuse.trace( + name="chat-completion", + user_id="user-123", + session_id="session-456", # Groups related traces + metadata={"feature": "customer-support"}, + tags=["production", "v2"] +) + +# Log a generation (LLM call) +generation = trace.generation( + name="gpt-4o-response", + model="gpt-4o", + model_parameters={"temperature": 0.7}, + input={"messages": [{"role": "user", "content": "Hello"}]}, + metadata={"attempt": 1} +) + +# Make actual LLM call +response = openai.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello"}] +) + +# Complete the generation with output +generation.end( + output=response.choices[0].message.content, + usage={ + "input": response.usage.prompt_tokens, + "output": response.usage.completion_tokens + } +) + +# Score the trace +trace.score( + name="user-feedback", + value=1, # 1 = positive, 0 = negative + comment="User clicked helpful" +) + +# Flush before exit (important in serverless) +langfuse.flush() +``` + +### OpenAI Integration + +Automatic tracing with OpenAI SDK + +**When to use**: OpenAI-based applications + +```python +from langfuse.openai import openai + +# Drop-in replacement for OpenAI client +# All calls automatically traced + +response = openai.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello"}], + # Langfuse-specific parameters + name="greeting", # Trace name + session_id="session-123", + user_id="user-456", + tags=["test"], + metadata={"feature": "chat"} +) + +# Works with streaming +stream = openai.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": "Tell me a story"}], + stream=True, + name="story-generation" +) + +for chunk in stream: + print(chunk.choices[0].delta.content, end="") + +# Works with async +import asyncio +from langfuse.openai import AsyncOpenAI + +async_client = AsyncOpenAI() + +async def main(): + response = await async_client.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello"}], + name="async-greeting" + ) +``` + +### LangChain Integration + +Trace LangChain applications + +**When to use**: LangChain-based applications + +```python +from langchain_openai import ChatOpenAI +from langchain_core.prompts import ChatPromptTemplate +from langfuse.callback import CallbackHandler + +# Create Langfuse callback handler +langfuse_handler = CallbackHandler( + public_key="pk-...", + secret_key="sk-...", + host="https://cloud.langfuse.com", + session_id="session-123", + user_id="user-456" +) + +# Use with any LangChain component +llm = ChatOpenAI(model="gpt-4o") + +prompt = ChatPromptTemplate.from_messages([ + ("system", "You are a helpful assistant."), + ("user", "{input}") +]) + +chain = prompt | llm + +# Pass handler to invoke +response = chain.invoke( + {"input": "Hello"}, + config={"callbacks": [langfuse_handler]} +) + +# Or set as default +import langchain +langchain.callbacks.manager.set_handler(langfuse_handler) + +# Then all calls are traced +response = chain.invoke({"input": "Hello"}) + +# Works with agents, retrievers, etc. +from langchain.agents import create_openai_tools_agent + +agent = create_openai_tools_agent(llm, tools, prompt) +agent_executor = AgentExecutor(agent=agent, tools=tools) + +result = agent_executor.invoke( + {"input": "What's the weather?"}, + config={"callbacks": [langfuse_handler]} +) +``` + +## Anti-Patterns + +### โŒ Not Flushing in Serverless + +**Why bad**: Traces are batched. +Serverless may exit before flush. +Data is lost. + +**Instead**: Always call langfuse.flush() at end. +Use context managers where available. +Consider sync mode for critical traces. + +### โŒ Tracing Everything + +**Why bad**: Noisy traces. +Performance overhead. +Hard to find important info. + +**Instead**: Focus on: LLM calls, key logic, user actions. +Group related operations. +Use meaningful span names. + +### โŒ No User/Session IDs + +**Why bad**: Can't debug specific users. +Can't track sessions. +Analytics limited. + +**Instead**: Always pass user_id and session_id. +Use consistent identifiers. +Add relevant metadata. + +## Limitations + +- Self-hosted requires infrastructure +- High-volume may need optimization +- Real-time dashboard has latency +- Evaluation requires setup + +## Related Skills + +Works well with: `langgraph`, `crewai`, `structured-output`, `autonomous-agents` + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-llm-application-developer/skills/llm-app-patterns/SKILL.md b/plugins/antigravity-bundle-llm-application-developer/skills/llm-app-patterns/SKILL.md new file mode 100644 index 00000000..c3f34542 --- /dev/null +++ b/plugins/antigravity-bundle-llm-application-developer/skills/llm-app-patterns/SKILL.md @@ -0,0 +1,763 @@ +--- +name: llm-app-patterns +description: "Production-ready patterns for building LLM applications, inspired by [Dify](https://github.com/langgenius/dify) and industry best practices." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# ๐Ÿค– LLM Application Patterns + +> Production-ready patterns for building LLM applications, inspired by [Dify](https://github.com/langgenius/dify) and industry best practices. + +## When to Use This Skill + +Use this skill when: + +- Designing LLM-powered applications +- Implementing RAG (Retrieval-Augmented Generation) +- Building AI agents with tools +- Setting up LLMOps monitoring +- Choosing between agent architectures + +--- + +## 1. RAG Pipeline Architecture + +### Overview + +RAG (Retrieval-Augmented Generation) grounds LLM responses in your data. + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Ingest โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ Retrieve โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ Generate โ”‚ +โ”‚ Documents โ”‚ โ”‚ Context โ”‚ โ”‚ Response โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ โ”‚ + โ–ผ โ–ผ โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Chunkingโ”‚ โ”‚ Vector โ”‚ โ”‚ LLM โ”‚ + โ”‚Embeddingโ”‚ โ”‚ Search โ”‚ โ”‚ + Contextโ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 1.1 Document Ingestion + +```python +# Chunking strategies +class ChunkingStrategy: + # Fixed-size chunks (simple but may break context) + FIXED_SIZE = "fixed_size" # e.g., 512 tokens + + # Semantic chunking (preserves meaning) + SEMANTIC = "semantic" # Split on paragraphs/sections + + # Recursive splitting (tries multiple separators) + RECURSIVE = "recursive" # ["\n\n", "\n", " ", ""] + + # Document-aware (respects structure) + DOCUMENT_AWARE = "document_aware" # Headers, lists, etc. + +# Recommended settings +CHUNK_CONFIG = { + "chunk_size": 512, # tokens + "chunk_overlap": 50, # token overlap between chunks + "separators": ["\n\n", "\n", ". ", " "], +} +``` + +### 1.2 Embedding & Storage + +```python +# Vector database selection +VECTOR_DB_OPTIONS = { + "pinecone": { + "use_case": "Production, managed service", + "scale": "Billions of vectors", + "features": ["Hybrid search", "Metadata filtering"] + }, + "weaviate": { + "use_case": "Self-hosted, multi-modal", + "scale": "Millions of vectors", + "features": ["GraphQL API", "Modules"] + }, + "chromadb": { + "use_case": "Development, prototyping", + "scale": "Thousands of vectors", + "features": ["Simple API", "In-memory option"] + }, + "pgvector": { + "use_case": "Existing Postgres infrastructure", + "scale": "Millions of vectors", + "features": ["SQL integration", "ACID compliance"] + } +} + +# Embedding model selection +EMBEDDING_MODELS = { + "openai/text-embedding-3-small": { + "dimensions": 1536, + "cost": "$0.02/1M tokens", + "quality": "Good for most use cases" + }, + "openai/text-embedding-3-large": { + "dimensions": 3072, + "cost": "$0.13/1M tokens", + "quality": "Best for complex queries" + }, + "local/bge-large": { + "dimensions": 1024, + "cost": "Free (compute only)", + "quality": "Comparable to OpenAI small" + } +} +``` + +### 1.3 Retrieval Strategies + +```python +# Basic semantic search +def semantic_search(query: str, top_k: int = 5): + query_embedding = embed(query) + results = vector_db.similarity_search( + query_embedding, + top_k=top_k + ) + return results + +# Hybrid search (semantic + keyword) +def hybrid_search(query: str, top_k: int = 5, alpha: float = 0.5): + """ + alpha=1.0: Pure semantic + alpha=0.0: Pure keyword (BM25) + alpha=0.5: Balanced + """ + semantic_results = vector_db.similarity_search(query) + keyword_results = bm25_search(query) + + # Reciprocal Rank Fusion + return rrf_merge(semantic_results, keyword_results, alpha) + +# Multi-query retrieval +def multi_query_retrieval(query: str): + """Generate multiple query variations for better recall""" + queries = llm.generate_query_variations(query, n=3) + all_results = [] + for q in queries: + all_results.extend(semantic_search(q)) + return deduplicate(all_results) + +# Contextual compression +def compressed_retrieval(query: str): + """Retrieve then compress to relevant parts only""" + docs = semantic_search(query, top_k=10) + compressed = llm.extract_relevant_parts(docs, query) + return compressed +``` + +### 1.4 Generation with Context + +```python +RAG_PROMPT_TEMPLATE = """ +Answer the user's question based ONLY on the following context. +If the context doesn't contain enough information, say "I don't have enough information to answer that." + +Context: +{context} + +Question: {question} + +Answer:""" + +def generate_with_rag(question: str): + # Retrieve + context_docs = hybrid_search(question, top_k=5) + context = "\n\n".join([doc.content for doc in context_docs]) + + # Generate + prompt = RAG_PROMPT_TEMPLATE.format( + context=context, + question=question + ) + + response = llm.generate(prompt) + + # Return with citations + return { + "answer": response, + "sources": [doc.metadata for doc in context_docs] + } +``` + +--- + +## 2. Agent Architectures + +### 2.1 ReAct Pattern (Reasoning + Acting) + +``` +Thought: I need to search for information about X +Action: search("X") +Observation: [search results] +Thought: Based on the results, I should... +Action: calculate(...) +Observation: [calculation result] +Thought: I now have enough information +Action: final_answer("The answer is...") +``` + +```python +REACT_PROMPT = """ +You are an AI assistant that can use tools to answer questions. + +Available tools: +{tools_description} + +Use this format: +Thought: [your reasoning about what to do next] +Action: [tool_name(arguments)] +Observation: [tool result - this will be filled in] +... (repeat Thought/Action/Observation as needed) +Thought: I have enough information to answer +Final Answer: [your final response] + +Question: {question} +""" + +class ReActAgent: + def __init__(self, tools: list, llm): + self.tools = {t.name: t for t in tools} + self.llm = llm + self.max_iterations = 10 + + def run(self, question: str) -> str: + prompt = REACT_PROMPT.format( + tools_description=self._format_tools(), + question=question + ) + + for _ in range(self.max_iterations): + response = self.llm.generate(prompt) + + if "Final Answer:" in response: + return self._extract_final_answer(response) + + action = self._parse_action(response) + observation = self._execute_tool(action) + prompt += f"\nObservation: {observation}\n" + + return "Max iterations reached" +``` + +### 2.2 Function Calling Pattern + +```python +# Define tools as functions with schemas +TOOLS = [ + { + "name": "search_web", + "description": "Search the web for current information", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query" + } + }, + "required": ["query"] + } + }, + { + "name": "calculate", + "description": "Perform mathematical calculations", + "parameters": { + "type": "object", + "properties": { + "expression": { + "type": "string", + "description": "Math expression to evaluate" + } + }, + "required": ["expression"] + } + } +] + +class FunctionCallingAgent: + def run(self, question: str) -> str: + messages = [{"role": "user", "content": question}] + + while True: + response = self.llm.chat( + messages=messages, + tools=TOOLS, + tool_choice="auto" + ) + + if response.tool_calls: + for tool_call in response.tool_calls: + result = self._execute_tool( + tool_call.name, + tool_call.arguments + ) + messages.append({ + "role": "tool", + "tool_call_id": tool_call.id, + "content": str(result) + }) + else: + return response.content +``` + +### 2.3 Plan-and-Execute Pattern + +```python +class PlanAndExecuteAgent: + """ + 1. Create a plan (list of steps) + 2. Execute each step + 3. Replan if needed + """ + + def run(self, task: str) -> str: + # Planning phase + plan = self.planner.create_plan(task) + # Returns: ["Step 1: ...", "Step 2: ...", ...] + + results = [] + for step in plan: + # Execute each step + result = self.executor.execute(step, context=results) + results.append(result) + + # Check if replan needed + if self._needs_replan(task, results): + new_plan = self.planner.replan( + task, + completed=results, + remaining=plan[len(results):] + ) + plan = new_plan + + # Synthesize final answer + return self.synthesizer.summarize(task, results) +``` + +### 2.4 Multi-Agent Collaboration + +```python +class AgentTeam: + """ + Specialized agents collaborating on complex tasks + """ + + def __init__(self): + self.agents = { + "researcher": ResearchAgent(), + "analyst": AnalystAgent(), + "writer": WriterAgent(), + "critic": CriticAgent() + } + self.coordinator = CoordinatorAgent() + + def solve(self, task: str) -> str: + # Coordinator assigns subtasks + assignments = self.coordinator.decompose(task) + + results = {} + for assignment in assignments: + agent = self.agents[assignment.agent] + result = agent.execute( + assignment.subtask, + context=results + ) + results[assignment.id] = result + + # Critic reviews + critique = self.agents["critic"].review(results) + + if critique.needs_revision: + # Iterate with feedback + return self.solve_with_feedback(task, results, critique) + + return self.coordinator.synthesize(results) +``` + +--- + +## 3. Prompt IDE Patterns + +### 3.1 Prompt Templates with Variables + +```python +class PromptTemplate: + def __init__(self, template: str, variables: list[str]): + self.template = template + self.variables = variables + + def format(self, **kwargs) -> str: + # Validate all variables provided + missing = set(self.variables) - set(kwargs.keys()) + if missing: + raise ValueError(f"Missing variables: {missing}") + + return self.template.format(**kwargs) + + def with_examples(self, examples: list[dict]) -> str: + """Add few-shot examples""" + example_text = "\n\n".join([ + f"Input: {ex['input']}\nOutput: {ex['output']}" + for ex in examples + ]) + return f"{example_text}\n\n{self.template}" + +# Usage +summarizer = PromptTemplate( + template="Summarize the following text in {style} style:\n\n{text}", + variables=["style", "text"] +) + +prompt = summarizer.format( + style="professional", + text="Long article content..." +) +``` + +### 3.2 Prompt Versioning & A/B Testing + +```python +class PromptRegistry: + def __init__(self, db): + self.db = db + + def register(self, name: str, template: str, version: str): + """Store prompt with version""" + self.db.save({ + "name": name, + "template": template, + "version": version, + "created_at": datetime.now(), + "metrics": {} + }) + + def get(self, name: str, version: str = "latest") -> str: + """Retrieve specific version""" + return self.db.get(name, version) + + def ab_test(self, name: str, user_id: str) -> str: + """Return variant based on user bucket""" + variants = self.db.get_all_versions(name) + bucket = hash(user_id) % len(variants) + return variants[bucket] + + def record_outcome(self, prompt_id: str, outcome: dict): + """Track prompt performance""" + self.db.update_metrics(prompt_id, outcome) +``` + +### 3.3 Prompt Chaining + +```python +class PromptChain: + """ + Chain prompts together, passing output as input to next + """ + + def __init__(self, steps: list[dict]): + self.steps = steps + + def run(self, initial_input: str) -> dict: + context = {"input": initial_input} + results = [] + + for step in self.steps: + prompt = step["prompt"].format(**context) + output = llm.generate(prompt) + + # Parse output if needed + if step.get("parser"): + output = step"parser" + + context[step["output_key"]] = output + results.append({ + "step": step["name"], + "output": output + }) + + return { + "final_output": context[self.steps[-1]["output_key"]], + "intermediate_results": results + } + +# Example: Research โ†’ Analyze โ†’ Summarize +chain = PromptChain([ + { + "name": "research", + "prompt": "Research the topic: {input}", + "output_key": "research" + }, + { + "name": "analyze", + "prompt": "Analyze these findings:\n{research}", + "output_key": "analysis" + }, + { + "name": "summarize", + "prompt": "Summarize this analysis in 3 bullet points:\n{analysis}", + "output_key": "summary" + } +]) +``` + +--- + +## 4. LLMOps & Observability + +### 4.1 Metrics to Track + +```python +LLM_METRICS = { + # Performance + "latency_p50": "50th percentile response time", + "latency_p99": "99th percentile response time", + "tokens_per_second": "Generation speed", + + # Quality + "user_satisfaction": "Thumbs up/down ratio", + "task_completion": "% tasks completed successfully", + "hallucination_rate": "% responses with factual errors", + + # Cost + "cost_per_request": "Average $ per API call", + "tokens_per_request": "Average tokens used", + "cache_hit_rate": "% requests served from cache", + + # Reliability + "error_rate": "% failed requests", + "timeout_rate": "% requests that timed out", + "retry_rate": "% requests needing retry" +} +``` + +### 4.2 Logging & Tracing + +```python +import logging +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +class LLMLogger: + def log_request(self, request_id: str, data: dict): + """Log LLM request for debugging and analysis""" + log_entry = { + "request_id": request_id, + "timestamp": datetime.now().isoformat(), + "model": data["model"], + "prompt": data["prompt"][:500], # Truncate for storage + "prompt_tokens": data["prompt_tokens"], + "temperature": data.get("temperature", 1.0), + "user_id": data.get("user_id"), + } + logging.info(f"LLM_REQUEST: {json.dumps(log_entry)}") + + def log_response(self, request_id: str, data: dict): + """Log LLM response""" + log_entry = { + "request_id": request_id, + "completion_tokens": data["completion_tokens"], + "total_tokens": data["total_tokens"], + "latency_ms": data["latency_ms"], + "finish_reason": data["finish_reason"], + "cost_usd": self._calculate_cost(data), + } + logging.info(f"LLM_RESPONSE: {json.dumps(log_entry)}") + +# Distributed tracing +@tracer.start_as_current_span("llm_call") +def call_llm(prompt: str) -> str: + span = trace.get_current_span() + span.set_attribute("prompt.length", len(prompt)) + + response = llm.generate(prompt) + + span.set_attribute("response.length", len(response)) + span.set_attribute("tokens.total", response.usage.total_tokens) + + return response.content +``` + +### 4.3 Evaluation Framework + +```python +class LLMEvaluator: + """ + Evaluate LLM outputs for quality + """ + + def evaluate_response(self, + question: str, + response: str, + ground_truth: str = None) -> dict: + scores = {} + + # Relevance: Does it answer the question? + scores["relevance"] = self._score_relevance(question, response) + + # Coherence: Is it well-structured? + scores["coherence"] = self._score_coherence(response) + + # Groundedness: Is it based on provided context? + scores["groundedness"] = self._score_groundedness(response) + + # Accuracy: Does it match ground truth? + if ground_truth: + scores["accuracy"] = self._score_accuracy(response, ground_truth) + + # Harmfulness: Is it safe? + scores["safety"] = self._score_safety(response) + + return scores + + def run_benchmark(self, test_cases: list[dict]) -> dict: + """Run evaluation on test set""" + results = [] + for case in test_cases: + response = llm.generate(case["prompt"]) + scores = self.evaluate_response( + question=case["prompt"], + response=response, + ground_truth=case.get("expected") + ) + results.append(scores) + + return self._aggregate_scores(results) +``` + +--- + +## 5. Production Patterns + +### 5.1 Caching Strategy + +```python +import hashlib +from functools import lru_cache + +class LLMCache: + def __init__(self, redis_client, ttl_seconds=3600): + self.redis = redis_client + self.ttl = ttl_seconds + + def _cache_key(self, prompt: str, model: str, **kwargs) -> str: + """Generate deterministic cache key""" + content = f"{model}:{prompt}:{json.dumps(kwargs, sort_keys=True)}" + return hashlib.sha256(content.encode()).hexdigest() + + def get_or_generate(self, prompt: str, model: str, **kwargs) -> str: + key = self._cache_key(prompt, model, **kwargs) + + # Check cache + cached = self.redis.get(key) + if cached: + return cached.decode() + + # Generate + response = llm.generate(prompt, model=model, **kwargs) + + # Cache (only cache deterministic outputs) + if kwargs.get("temperature", 1.0) == 0: + self.redis.setex(key, self.ttl, response) + + return response +``` + +### 5.2 Rate Limiting & Retry + +```python +import time +from tenacity import retry, wait_exponential, stop_after_attempt + +class RateLimiter: + def __init__(self, requests_per_minute: int): + self.rpm = requests_per_minute + self.timestamps = [] + + def acquire(self): + """Wait if rate limit would be exceeded""" + now = time.time() + + # Remove old timestamps + self.timestamps = [t for t in self.timestamps if now - t < 60] + + if len(self.timestamps) >= self.rpm: + sleep_time = 60 - (now - self.timestamps[0]) + time.sleep(sleep_time) + + self.timestamps.append(time.time()) + +# Retry with exponential backoff +@retry( + wait=wait_exponential(multiplier=1, min=4, max=60), + stop=stop_after_attempt(5) +) +def call_llm_with_retry(prompt: str) -> str: + try: + return llm.generate(prompt) + except RateLimitError: + raise # Will trigger retry + except APIError as e: + if e.status_code >= 500: + raise # Retry server errors + raise # Don't retry client errors +``` + +### 5.3 Fallback Strategy + +```python +class LLMWithFallback: + def __init__(self, primary: str, fallbacks: list[str]): + self.primary = primary + self.fallbacks = fallbacks + + def generate(self, prompt: str, **kwargs) -> str: + models = [self.primary] + self.fallbacks + + for model in models: + try: + return llm.generate(prompt, model=model, **kwargs) + except (RateLimitError, APIError) as e: + logging.warning(f"Model {model} failed: {e}") + continue + + raise AllModelsFailedError("All models exhausted") + +# Usage +llm_client = LLMWithFallback( + primary="gpt-4-turbo", + fallbacks=["gpt-3.5-turbo", "claude-3-sonnet"] +) +``` + +--- + +## Architecture Decision Matrix + +| Pattern | Use When | Complexity | Cost | +| :------------------- | :--------------- | :--------- | :-------- | +| **Simple RAG** | FAQ, docs search | Low | Low | +| **Hybrid RAG** | Mixed queries | Medium | Medium | +| **ReAct Agent** | Multi-step tasks | Medium | Medium | +| **Function Calling** | Structured tools | Low | Low | +| **Plan-Execute** | Complex tasks | High | High | +| **Multi-Agent** | Research tasks | Very High | Very High | + +--- + +## Resources + +- [Dify Platform](https://github.com/langgenius/dify) +- [LangChain Docs](https://python.langchain.com/) +- [LlamaIndex](https://www.llamaindex.ai/) +- [Anthropic Cookbook](https://github.com/anthropics/anthropic-cookbook) diff --git a/plugins/antigravity-bundle-llm-application-developer/skills/prompt-caching/SKILL.md b/plugins/antigravity-bundle-llm-application-developer/skills/prompt-caching/SKILL.md new file mode 100644 index 00000000..9dab6bbf --- /dev/null +++ b/plugins/antigravity-bundle-llm-application-developer/skills/prompt-caching/SKILL.md @@ -0,0 +1,66 @@ +--- +name: prompt-caching +description: "You're a caching specialist who has reduced LLM costs by 90% through strategic caching. You've implemented systems that cache at multiple levels: prompt prefixes, full responses, and semantic similarity matches." +risk: unknown +source: "vibeship-spawner-skills (Apache 2.0)" +date_added: "2026-02-27" +--- + +# Prompt Caching + +You're a caching specialist who has reduced LLM costs by 90% through strategic caching. +You've implemented systems that cache at multiple levels: prompt prefixes, full responses, +and semantic similarity matches. + +You understand that LLM caching is different from traditional cachingโ€”prompts have +prefixes that can be cached, responses vary with temperature, and semantic similarity +often matters more than exact match. + +Your core principles: +1. Cache at the right levelโ€”prefix, response, or both +2. K + +## Capabilities + +- prompt-cache +- response-cache +- kv-cache +- cag-patterns +- cache-invalidation + +## Patterns + +### Anthropic Prompt Caching + +Use Claude's native prompt caching for repeated prefixes + +### Response Caching + +Cache full LLM responses for identical or similar queries + +### Cache Augmented Generation (CAG) + +Pre-cache documents in prompt instead of RAG retrieval + +## Anti-Patterns + +### โŒ Caching with High Temperature + +### โŒ No Cache Invalidation + +### โŒ Caching Everything + +## โš ๏ธ Sharp Edges + +| Issue | Severity | Solution | +|-------|----------|----------| +| Cache miss causes latency spike with additional overhead | high | // Optimize for cache misses, not just hits | +| Cached responses become incorrect over time | high | // Implement proper cache invalidation | +| Prompt caching doesn't work due to prefix changes | medium | // Structure prompts for optimal caching | + +## Related Skills + +Works well with: `context-window-management`, `rag-implementation`, `conversation-memory` + +## When to Use +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/plugins/antigravity-bundle-llm-application-developer/skills/rag-implementation/SKILL.md b/plugins/antigravity-bundle-llm-application-developer/skills/rag-implementation/SKILL.md new file mode 100644 index 00000000..a944bb75 --- /dev/null +++ b/plugins/antigravity-bundle-llm-application-developer/skills/rag-implementation/SKILL.md @@ -0,0 +1,196 @@ +--- +name: rag-implementation +description: "RAG (Retrieval-Augmented Generation) implementation workflow covering embedding selection, vector database setup, chunking strategies, and retrieval optimization." +category: granular-workflow-bundle +risk: safe +source: personal +date_added: "2026-02-27" +--- + +# RAG Implementation Workflow + +## Overview + +Specialized workflow for implementing RAG (Retrieval-Augmented Generation) systems including embedding model selection, vector database setup, chunking strategies, retrieval optimization, and evaluation. + +## When to Use This Workflow + +Use this workflow when: +- Building RAG-powered applications +- Implementing semantic search +- Creating knowledge-grounded AI +- Setting up document Q&A systems +- Optimizing retrieval quality + +## Workflow Phases + +### Phase 1: Requirements Analysis + +#### Skills to Invoke +- `ai-product` - AI product design +- `rag-engineer` - RAG engineering + +#### Actions +1. Define use case +2. Identify data sources +3. Set accuracy requirements +4. Determine latency targets +5. Plan evaluation metrics + +#### Copy-Paste Prompts +``` +Use @ai-product to define RAG application requirements +``` + +### Phase 2: Embedding Selection + +#### Skills to Invoke +- `embedding-strategies` - Embedding selection +- `rag-engineer` - RAG patterns + +#### Actions +1. Evaluate embedding models +2. Test domain relevance +3. Measure embedding quality +4. Consider cost/latency +5. Select model + +#### Copy-Paste Prompts +``` +Use @embedding-strategies to select optimal embedding model +``` + +### Phase 3: Vector Database Setup + +#### Skills to Invoke +- `vector-database-engineer` - Vector DB +- `similarity-search-patterns` - Similarity search + +#### Actions +1. Choose vector database +2. Design schema +3. Configure indexes +4. Set up connection +5. Test queries + +#### Copy-Paste Prompts +``` +Use @vector-database-engineer to set up vector database +``` + +### Phase 4: Chunking Strategy + +#### Skills to Invoke +- `rag-engineer` - Chunking strategies +- `rag-implementation` - RAG implementation + +#### Actions +1. Choose chunk size +2. Implement chunking +3. Add overlap handling +4. Create metadata +5. Test retrieval quality + +#### Copy-Paste Prompts +``` +Use @rag-engineer to implement chunking strategy +``` + +### Phase 5: Retrieval Implementation + +#### Skills to Invoke +- `similarity-search-patterns` - Similarity search +- `hybrid-search-implementation` - Hybrid search + +#### Actions +1. Implement vector search +2. Add keyword search +3. Configure hybrid search +4. Set up reranking +5. Optimize latency + +#### Copy-Paste Prompts +``` +Use @similarity-search-patterns to implement retrieval +``` + +``` +Use @hybrid-search-implementation to add hybrid search +``` + +### Phase 6: LLM Integration + +#### Skills to Invoke +- `llm-application-dev-ai-assistant` - LLM integration +- `llm-application-dev-prompt-optimize` - Prompt optimization + +#### Actions +1. Select LLM provider +2. Design prompt template +3. Implement context injection +4. Add citation handling +5. Test generation quality + +#### Copy-Paste Prompts +``` +Use @llm-application-dev-ai-assistant to integrate LLM +``` + +### Phase 7: Caching + +#### Skills to Invoke +- `prompt-caching` - Prompt caching +- `rag-engineer` - RAG optimization + +#### Actions +1. Implement response caching +2. Set up embedding cache +3. Configure TTL +4. Add cache invalidation +5. Monitor hit rates + +#### Copy-Paste Prompts +``` +Use @prompt-caching to implement RAG caching +``` + +### Phase 8: Evaluation + +#### Skills to Invoke +- `llm-evaluation` - LLM evaluation +- `evaluation` - AI evaluation + +#### Actions +1. Define evaluation metrics +2. Create test dataset +3. Measure retrieval accuracy +4. Evaluate generation quality +5. Iterate on improvements + +#### Copy-Paste Prompts +``` +Use @llm-evaluation to evaluate RAG system +``` + +## RAG Architecture + +``` +User Query -> Embedding -> Vector Search -> Retrieved Docs -> LLM -> Response + | | | | + Model Vector DB Chunk Store Prompt + Context +``` + +## Quality Gates + +- [ ] Embedding model selected +- [ ] Vector DB configured +- [ ] Chunking implemented +- [ ] Retrieval working +- [ ] LLM integrated +- [ ] Evaluation passing + +## Related Workflow Bundles + +- `ai-ml` - AI/ML development +- `ai-agent-development` - AI agents +- `database` - Vector databases diff --git a/plugins/antigravity-bundle-makepad-builder/.codex-plugin/plugin.json b/plugins/antigravity-bundle-makepad-builder/.codex-plugin/plugin.json new file mode 100644 index 00000000..37cfe518 --- /dev/null +++ b/plugins/antigravity-bundle-makepad-builder/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "antigravity-bundle-makepad-builder", + "version": "8.10.0", + "description": "Install the \"Makepad Builder\" editorial skill bundle from Antigravity Awesome Skills.", + "author": { + "name": "sickn33 and contributors", + "url": "https://github.com/sickn33/antigravity-awesome-skills" + }, + "homepage": "https://github.com/sickn33/antigravity-awesome-skills", + "repository": "https://github.com/sickn33/antigravity-awesome-skills", + "license": "MIT", + "keywords": [ + "codex", + "skills", + "bundle", + "makepad-builder", + "productivity" + ], + "skills": "./skills/", + "interface": { + "displayName": "Makepad Builder", + "shortDescription": "Specialized Packs ยท 6 curated skills", + "longDescription": "For building UI-heavy apps with the Makepad ecosystem. Covers Makepad Basics, Makepad Layout, and 4 more skills.", + "developerName": "sickn33 and contributors", + "category": "Specialized Packs", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://github.com/sickn33/antigravity-awesome-skills", + "brandColor": "#111827" + } +} diff --git a/plugins/antigravity-bundle-makepad-builder/skills/makepad-basics/SKILL.md b/plugins/antigravity-bundle-makepad-builder/skills/makepad-basics/SKILL.md new file mode 100644 index 00000000..a923030b --- /dev/null +++ b/plugins/antigravity-bundle-makepad-builder/skills/makepad-basics/SKILL.md @@ -0,0 +1,154 @@ +--- +name: makepad-basics +description: | + CRITICAL: Use for Makepad getting started and app structure. Triggers on: + makepad, makepad getting started, makepad tutorial, live_design!, app_main!, + makepad project setup, makepad hello world, "how to create makepad app", + makepad ๅ…ฅ้—จ, ๅˆ›ๅปบ makepad ๅบ”็”จ, makepad ๆ•™็จ‹, makepad ้กน็›ฎ็ป“ๆž„ +risk: unknown +source: "https://github.com/makepad/makepad" +--- + +# Makepad Basics Skill + +> **Version:** makepad-widgets (dev branch) | **Last Updated:** 2026-01-19 +> +> Check for updates: https://crates.io/crates/makepad-widgets + +You are an expert at the Rust `makepad-widgets` crate. Help users by: +- **Writing code**: Generate Rust code following the patterns below +- **Answering questions**: Explain concepts, troubleshoot issues, reference documentation + +## Documentation + +Refer to the local files for detailed documentation: +- `./references/app-structure.md` - Complete app boilerplate and structure +- `./references/event-handling.md` - Event handling patterns + +## IMPORTANT: Documentation Completeness Check + +**Before answering questions, Claude MUST:** + +1. Read the relevant reference file(s) listed above +2. If file read fails or file is empty: + - Inform user: "ๆœฌๅœฐๆ–‡ๆกฃไธๅฎŒๆ•ด๏ผŒๅปบ่ฎฎ่ฟ่กŒ `/sync-crate-skills makepad --force` ๆ›ดๆ–ฐๆ–‡ๆกฃ" + - Still answer based on SKILL.md patterns + built-in knowledge +3. If reference file exists, incorporate its content into the answer + +## Key Patterns + +### 1. Basic App Structure + +```rust +use makepad_widgets::*; + +live_design! { + use link::theme::*; + use link::shaders::*; + use link::widgets::*; + + App = {{App}} { + ui: { + main_window = { + body = { + width: Fill, height: Fill + flow: Down + +
                      + ) +} +``` + +### Pattern 3: Server Actions + +```typescript +// app/actions/cart.ts +'use server' + +import { revalidateTag } from 'next/cache' +import { cookies } from 'next/headers' +import { redirect } from 'next/navigation' + +export async function addToCart(productId: string) { + const cookieStore = await cookies() + const sessionId = cookieStore.get('session')?.value + + if (!sessionId) { + redirect('/login') + } + + try { + await db.cart.upsert({ + where: { sessionId_productId: { sessionId, productId } }, + update: { quantity: { increment: 1 } }, + create: { sessionId, productId, quantity: 1 }, + }) + + revalidateTag('cart') + return { success: true } + } catch (error) { + return { error: 'Failed to add item to cart' } + } +} + +export async function checkout(formData: FormData) { + const address = formData.get('address') as string + const payment = formData.get('payment') as string + + // Validate + if (!address || !payment) { + return { error: 'Missing required fields' } + } + + // Process order + const order = await processOrder({ address, payment }) + + // Redirect to confirmation + redirect(`/orders/${order.id}/confirmation`) +} +``` + +### Pattern 4: Parallel Routes + +```typescript +// app/dashboard/layout.tsx +export default function DashboardLayout({ + children, + analytics, + team, +}: { + children: React.ReactNode + analytics: React.ReactNode + team: React.ReactNode +}) { + return ( +
                      +
                      {children}
                      + + +
                      + ) +} + +// app/dashboard/@analytics/page.tsx +export default async function AnalyticsSlot() { + const stats = await getAnalytics() + return +} + +// app/dashboard/@analytics/loading.tsx +export default function AnalyticsLoading() { + return +} + +// app/dashboard/@team/page.tsx +export default async function TeamSlot() { + const members = await getTeamMembers() + return +} +``` + +### Pattern 5: Intercepting Routes (Modal Pattern) + +```typescript +// File structure for photo modal +// app/ +// โ”œโ”€โ”€ @modal/ +// โ”‚ โ”œโ”€โ”€ (.)photos/[id]/page.tsx # Intercept +// โ”‚ โ””โ”€โ”€ default.tsx +// โ”œโ”€โ”€ photos/ +// โ”‚ โ””โ”€โ”€ [id]/page.tsx # Full page +// โ””โ”€โ”€ layout.tsx + +// app/@modal/(.)photos/[id]/page.tsx +import { Modal } from '@/components/Modal' +import { PhotoDetail } from '@/components/PhotoDetail' + +export default async function PhotoModal({ + params, +}: { + params: Promise<{ id: string }> +}) { + const { id } = await params + const photo = await getPhoto(id) + + return ( + + + + ) +} + +// app/photos/[id]/page.tsx - Full page version +export default async function PhotoPage({ + params, +}: { + params: Promise<{ id: string }> +}) { + const { id } = await params + const photo = await getPhoto(id) + + return ( +
                      + + +
                      + ) +} + +// app/layout.tsx +export default function RootLayout({ + children, + modal, +}: { + children: React.ReactNode + modal: React.ReactNode +}) { + return ( + + + {children} + {modal} + + + ) +} +``` + +### Pattern 6: Streaming with Suspense + +```typescript +// app/product/[id]/page.tsx +import { Suspense } from 'react' + +export default async function ProductPage({ + params, +}: { + params: Promise<{ id: string }> +}) { + const { id } = await params + + // This data loads first (blocking) + const product = await getProduct(id) + + return ( +
                      + {/* Immediate render */} + + + {/* Stream in reviews */} + }> + + + + {/* Stream in recommendations */} + }> + + +
                      + ) +} + +// These components fetch their own data +async function Reviews({ productId }: { productId: string }) { + const reviews = await getReviews(productId) // Slow API + return +} + +async function Recommendations({ productId }: { productId: string }) { + const products = await getRecommendations(productId) // ML-based, slow + return +} +``` + +### Pattern 7: Route Handlers (API Routes) + +```typescript +// app/api/products/route.ts +import { NextRequest, NextResponse } from 'next/server' + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams + const category = searchParams.get('category') + + const products = await db.product.findMany({ + where: category ? { category } : undefined, + take: 20, + }) + + return NextResponse.json(products) +} + +export async function POST(request: NextRequest) { + const body = await request.json() + + const product = await db.product.create({ + data: body, + }) + + return NextResponse.json(product, { status: 201 }) +} + +// app/api/products/[id]/route.ts +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params + const product = await db.product.findUnique({ where: { id } }) + + if (!product) { + return NextResponse.json( + { error: 'Product not found' }, + { status: 404 } + ) + } + + return NextResponse.json(product) +} +``` + +### Pattern 8: Metadata and SEO + +```typescript +// app/products/[slug]/page.tsx +import { Metadata } from 'next' +import { notFound } from 'next/navigation' + +type Props = { + params: Promise<{ slug: string }> +} + +export async function generateMetadata({ params }: Props): Promise { + const { slug } = await params + const product = await getProduct(slug) + + if (!product) return {} + + return { + title: product.name, + description: product.description, + openGraph: { + title: product.name, + description: product.description, + images: [{ url: product.image, width: 1200, height: 630 }], + }, + twitter: { + card: 'summary_large_image', + title: product.name, + description: product.description, + images: [product.image], + }, + } +} + +export async function generateStaticParams() { + const products = await db.product.findMany({ select: { slug: true } }) + return products.map((p) => ({ slug: p.slug })) +} + +export default async function ProductPage({ params }: Props) { + const { slug } = await params + const product = await getProduct(slug) + + if (!product) notFound() + + return +} +``` + +## Caching Strategies + +### Data Cache + +```typescript +// No cache (always fresh) +fetch(url, { cache: 'no-store' }) + +// Cache forever (static) +fetch(url, { cache: 'force-cache' }) + +// ISR - revalidate after 60 seconds +fetch(url, { next: { revalidate: 60 } }) + +// Tag-based invalidation +fetch(url, { next: { tags: ['products'] } }) + +// Invalidate via Server Action +'use server' +import { revalidateTag, revalidatePath } from 'next/cache' + +export async function updateProduct(id: string, data: ProductData) { + await db.product.update({ where: { id }, data }) + revalidateTag('products') + revalidatePath('/products') +} +``` + +## Best Practices + +### Do's +- **Start with Server Components** - Add 'use client' only when needed +- **Colocate data fetching** - Fetch data where it's used +- **Use Suspense boundaries** - Enable streaming for slow data +- **Leverage parallel routes** - Independent loading states +- **Use Server Actions** - For mutations with progressive enhancement + +### Don'ts +- **Don't pass serializable data** - Server โ†’ Client boundary limitations +- **Don't use hooks in Server Components** - No useState, useEffect +- **Don't fetch in Client Components** - Use Server Components or React Query +- **Don't over-nest layouts** - Each layout adds to the component tree +- **Don't ignore loading states** - Always provide loading.tsx or Suspense + +## Resources + +- [Next.js App Router Documentation](https://nextjs.org/docs/app) +- [Server Components RFC](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md) +- [Vercel Templates](https://vercel.com/templates/next.js) diff --git a/plugins/antigravity-bundle-typescript-javascript/skills/nodejs-best-practices/SKILL.md b/plugins/antigravity-bundle-typescript-javascript/skills/nodejs-best-practices/SKILL.md new file mode 100644 index 00000000..b92ac386 --- /dev/null +++ b/plugins/antigravity-bundle-typescript-javascript/skills/nodejs-best-practices/SKILL.md @@ -0,0 +1,338 @@ +--- +name: nodejs-best-practices +description: "Node.js development principles and decision-making. Framework selection, async patterns, security, and architecture. Teaches thinking, not copying." +risk: unknown +source: community +date_added: "2026-02-27" +--- + +# Node.js Best Practices + +> Principles and decision-making for Node.js development in 2025. +> **Learn to THINK, not memorize code patterns.** + +## When to Use +Use this skill when making Node.js architecture decisions, choosing frameworks, designing async patterns, or applying security and deployment best practices. + +--- + +## โš ๏ธ How to Use This Skill + +This skill teaches **decision-making principles**, not fixed code to copy. + +- ASK user for preferences when unclear +- Choose framework/pattern based on CONTEXT +- Don't default to same solution every time + +--- + +## 1. Framework Selection (2025) + +### Decision Tree + +``` +What are you building? +โ”‚ +โ”œโ”€โ”€ Edge/Serverless (Cloudflare, Vercel) +โ”‚ โ””โ”€โ”€ Hono (zero-dependency, ultra-fast cold starts) +โ”‚ +โ”œโ”€โ”€ High Performance API +โ”‚ โ””โ”€โ”€ Fastify (2-3x faster than Express) +โ”‚ +โ”œโ”€โ”€ Enterprise/Team familiarity +โ”‚ โ””โ”€โ”€ NestJS (structured, DI, decorators) +โ”‚ +โ”œโ”€โ”€ Legacy/Stable/Maximum ecosystem +โ”‚ โ””โ”€โ”€ Express (mature, most middleware) +โ”‚ +โ””โ”€โ”€ Full-stack with frontend + โ””โ”€โ”€ Next.js API Routes or tRPC +``` + +### Comparison Principles + +| Factor | Hono | Fastify | Express | +|--------|------|---------|---------| +| **Best for** | Edge, serverless | Performance | Legacy, learning | +| **Cold start** | Fastest | Fast | Moderate | +| **Ecosystem** | Growing | Good | Largest | +| **TypeScript** | Native | Excellent | Good | +| **Learning curve** | Low | Medium | Low | + +### Selection Questions to Ask: +1. What's the deployment target? +2. Is cold start time critical? +3. Does team have existing experience? +4. Is there legacy code to maintain? + +--- + +## 2. Runtime Considerations (2025) + +### Native TypeScript + +``` +Node.js 22+: --experimental-strip-types +โ”œโ”€โ”€ Run .ts files directly +โ”œโ”€โ”€ No build step needed for simple projects +โ””โ”€โ”€ Consider for: scripts, simple APIs +``` + +### Module System Decision + +``` +ESM (import/export) +โ”œโ”€โ”€ Modern standard +โ”œโ”€โ”€ Better tree-shaking +โ”œโ”€โ”€ Async module loading +โ””โ”€โ”€ Use for: new projects + +CommonJS (require) +โ”œโ”€โ”€ Legacy compatibility +โ”œโ”€โ”€ More npm packages support +โ””โ”€โ”€ Use for: existing codebases, some edge cases +``` + +### Runtime Selection + +| Runtime | Best For | +|---------|----------| +| **Node.js** | General purpose, largest ecosystem | +| **Bun** | Performance, built-in bundler | +| **Deno** | Security-first, built-in TypeScript | + +--- + +## 3. Architecture Principles + +### Layered Structure Concept + +``` +Request Flow: +โ”‚ +โ”œโ”€โ”€ Controller/Route Layer +โ”‚ โ”œโ”€โ”€ Handles HTTP specifics +โ”‚ โ”œโ”€โ”€ Input validation at boundary +โ”‚ โ””โ”€โ”€ Calls service layer +โ”‚ +โ”œโ”€โ”€ Service Layer +โ”‚ โ”œโ”€โ”€ Business logic +โ”‚ โ”œโ”€โ”€ Framework-agnostic +โ”‚ โ””โ”€โ”€ Calls repository layer +โ”‚ +โ””โ”€โ”€ Repository Layer + โ”œโ”€โ”€ Data access only + โ”œโ”€โ”€ Database queries + โ””โ”€โ”€ ORM interactions +``` + +### Why This Matters: +- **Testability**: Mock layers independently +- **Flexibility**: Swap database without touching business logic +- **Clarity**: Each layer has single responsibility + +### When to Simplify: +- Small scripts โ†’ Single file OK +- Prototypes โ†’ Less structure acceptable +- Always ask: "Will this grow?" + +--- + +## 4. Error Handling Principles + +### Centralized Error Handling + +``` +Pattern: +โ”œโ”€โ”€ Create custom error classes +โ”œโ”€โ”€ Throw from any layer +โ”œโ”€โ”€ Catch at top level (middleware) +โ””โ”€โ”€ Format consistent response +``` + +### Error Response Philosophy + +``` +Client gets: +โ”œโ”€โ”€ Appropriate HTTP status +โ”œโ”€โ”€ Error code for programmatic handling +โ”œโ”€โ”€ User-friendly message +โ””โ”€โ”€ NO internal details (security!) + +Logs get: +โ”œโ”€โ”€ Full stack trace +โ”œโ”€โ”€ Request context +โ”œโ”€โ”€ User ID (if applicable) +โ””โ”€โ”€ Timestamp +``` + +### Status Code Selection + +| Situation | Status | When | +|-----------|--------|------| +| Bad input | 400 | Client sent invalid data | +| No auth | 401 | Missing or invalid credentials | +| No permission | 403 | Valid auth, but not allowed | +| Not found | 404 | Resource doesn't exist | +| Conflict | 409 | Duplicate or state conflict | +| Validation | 422 | Schema valid but business rules fail | +| Server error | 500 | Our fault, log everything | + +--- + +## 5. Async Patterns Principles + +### When to Use Each + +| Pattern | Use When | +|---------|----------| +| `async/await` | Sequential async operations | +| `Promise.all` | Parallel independent operations | +| `Promise.allSettled` | Parallel where some can fail | +| `Promise.race` | Timeout or first response wins | + +### Event Loop Awareness + +``` +I/O-bound (async helps): +โ”œโ”€โ”€ Database queries +โ”œโ”€โ”€ HTTP requests +โ”œโ”€โ”€ File system +โ””โ”€โ”€ Network operations + +CPU-bound (async doesn't help): +โ”œโ”€โ”€ Crypto operations +โ”œโ”€โ”€ Image processing +โ”œโ”€โ”€ Complex calculations +โ””โ”€โ”€ โ†’ Use worker threads or offload +``` + +### Avoiding Event Loop Blocking + +- Never use sync methods in production (fs.readFileSync, etc.) +- Offload CPU-intensive work +- Use streaming for large data + +--- + +## 6. Validation Principles + +### Validate at Boundaries + +``` +Where to validate: +โ”œโ”€โ”€ API entry point (request body/params) +โ”œโ”€โ”€ Before database operations +โ”œโ”€โ”€ External data (API responses, file uploads) +โ””โ”€โ”€ Environment variables (startup) +``` + +### Validation Library Selection + +| Library | Best For | +|---------|----------| +| **Zod** | TypeScript first, inference | +| **Valibot** | Smaller bundle (tree-shakeable) | +| **ArkType** | Performance critical | +| **Yup** | Existing React Form usage | + +### Validation Philosophy + +- Fail fast: Validate early +- Be specific: Clear error messages +- Don't trust: Even "internal" data + +--- + +## 7. Security Principles + +### Security Checklist (Not Code) + +- [ ] **Input validation**: All inputs validated +- [ ] **Parameterized queries**: No string concatenation for SQL +- [ ] **Password hashing**: bcrypt or argon2 +- [ ] **JWT verification**: Always verify signature and expiry +- [ ] **Rate limiting**: Protect from abuse +- [ ] **Security headers**: Helmet.js or equivalent +- [ ] **HTTPS**: Everywhere in production +- [ ] **CORS**: Properly configured +- [ ] **Secrets**: Environment variables only +- [ ] **Dependencies**: Regularly audited + +### Security Mindset + +``` +Trust nothing: +โ”œโ”€โ”€ Query params โ†’ validate +โ”œโ”€โ”€ Request body โ†’ validate +โ”œโ”€โ”€ Headers โ†’ verify +โ”œโ”€โ”€ Cookies โ†’ validate +โ”œโ”€โ”€ File uploads โ†’ scan +โ””โ”€โ”€ External APIs โ†’ validate response +``` + +--- + +## 8. Testing Principles + +### Test Strategy Selection + +| Type | Purpose | Tools | +|------|---------|-------| +| **Unit** | Business logic | node:test, Vitest | +| **Integration** | API endpoints | Supertest | +| **E2E** | Full flows | Playwright | + +### What to Test (Priorities) + +1. **Critical paths**: Auth, payments, core business +2. **Edge cases**: Empty inputs, boundaries +3. **Error handling**: What happens when things fail? +4. **Not worth testing**: Framework code, trivial getters + +### Built-in Test Runner (Node.js 22+) + +``` +node --test src/**/*.test.ts +โ”œโ”€โ”€ No external dependency +โ”œโ”€โ”€ Good coverage reporting +โ””โ”€โ”€ Watch mode available +``` + +--- + +## 10. Anti-Patterns to Avoid + +### โŒ DON'T: +- Use Express for new edge projects (use Hono) +- Use sync methods in production code +- Put business logic in controllers +- Skip input validation +- Hardcode secrets +- Trust external data without validation +- Block event loop with CPU work + +### โœ… DO: +- Choose framework based on context +- Ask user for preferences when unclear +- Use layered architecture for growing projects +- Validate all inputs +- Use environment variables for secrets +- Profile before optimizing + +--- + +## 11. Decision Checklist + +Before implementing: + +- [ ] **Asked user about stack preference?** +- [ ] **Chosen framework for THIS context?** (not just default) +- [ ] **Considered deployment target?** +- [ ] **Planned error handling strategy?** +- [ ] **Identified validation points?** +- [ ] **Considered security requirements?** + +--- + +> **Remember**: Node.js best practices are about decision-making, not memorizing patterns. Every project deserves fresh consideration based on its requirements. diff --git a/plugins/antigravity-bundle-typescript-javascript/skills/react-best-practices/AGENTS.md b/plugins/antigravity-bundle-typescript-javascript/skills/react-best-practices/AGENTS.md new file mode 100644 index 00000000..898afb39 --- /dev/null +++ b/plugins/antigravity-bundle-typescript-javascript/skills/react-best-practices/AGENTS.md @@ -0,0 +1,2249 @@ +# React Best Practices + +**Version 0.1.0** +Vercel Engineering +January 2026 + +> **Note:** +> This document is mainly for agents and LLMs to follow when maintaining, +> generating, or refactoring React and Next.js codebases at Vercel. Humans +> may also find it useful, but guidance here is optimized for automation +> and consistency by AI-assisted workflows. + +--- + +## Abstract + +Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation. + +--- + +## Table of Contents + +1. [Eliminating Waterfalls](#1-eliminating-waterfalls) โ€” **CRITICAL** + - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed) + - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization) + - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes) + - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations) + - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries) +2. [Bundle Size Optimization](#2-bundle-size-optimization) โ€” **CRITICAL** + - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports) + - 2.2 [Conditional Module Loading](#22-conditional-module-loading) + - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries) + - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components) + - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent) +3. [Server-Side Performance](#3-server-side-performance) โ€” **HIGH** + - 3.1 [Cross-Request LRU Caching](#31-cross-request-lru-caching) + - 3.2 [Minimize Serialization at RSC Boundaries](#32-minimize-serialization-at-rsc-boundaries) + - 3.3 [Parallel Data Fetching with Component Composition](#33-parallel-data-fetching-with-component-composition) + - 3.4 [Per-Request Deduplication with React.cache()](#34-per-request-deduplication-with-reactcache) + - 3.5 [Use after() for Non-Blocking Operations](#35-use-after-for-non-blocking-operations) +4. [Client-Side Data Fetching](#4-client-side-data-fetching) โ€” **MEDIUM-HIGH** + - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners) + - 4.2 [Use SWR for Automatic Deduplication](#42-use-swr-for-automatic-deduplication) +5. [Re-render Optimization](#5-re-render-optimization) โ€” **MEDIUM** + - 5.1 [Defer State Reads to Usage Point](#51-defer-state-reads-to-usage-point) + - 5.2 [Extract to Memoized Components](#52-extract-to-memoized-components) + - 5.3 [Narrow Effect Dependencies](#53-narrow-effect-dependencies) + - 5.4 [Subscribe to Derived State](#54-subscribe-to-derived-state) + - 5.5 [Use Functional setState Updates](#55-use-functional-setstate-updates) + - 5.6 [Use Lazy State Initialization](#56-use-lazy-state-initialization) + - 5.7 [Use Transitions for Non-Urgent Updates](#57-use-transitions-for-non-urgent-updates) +6. [Rendering Performance](#6-rendering-performance) โ€” **MEDIUM** + - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element) + - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists) + - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements) + - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision) + - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering) + - 6.6 [Use Activity Component for Show/Hide](#66-use-activity-component-for-showhide) + - 6.7 [Use Explicit Conditional Rendering](#67-use-explicit-conditional-rendering) +7. [JavaScript Performance](#7-javascript-performance) โ€” **LOW-MEDIUM** + - 7.1 [Batch DOM CSS Changes](#71-batch-dom-css-changes) + - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups) + - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops) + - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls) + - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls) + - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations) + - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons) + - 7.8 [Early Return from Functions](#78-early-return-from-functions) + - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation) + - 7.10 [Use Loop for Min/Max Instead of Sort](#710-use-loop-for-minmax-instead-of-sort) + - 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups) + - 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability) +8. [Advanced Patterns](#8-advanced-patterns) โ€” **LOW** + - 8.1 [Store Event Handlers in Refs](#81-store-event-handlers-in-refs) + - 8.2 [useLatest for Stable Callback Refs](#82-uselatest-for-stable-callback-refs) + +--- + +## 1. Eliminating Waterfalls + +**Impact: CRITICAL** + +Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains. + +### 1.1 Defer Await Until Needed + +**Impact: HIGH (avoids blocking unused code paths)** + +Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them. + +**Incorrect: blocks both branches** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + const userData = await fetchUserData(userId) + + if (skipProcessing) { + // Returns immediately but still waited for userData + return { skipped: true } + } + + // Only this branch uses userData + return processUserData(userData) +} +``` + +**Correct: only blocks when needed** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + if (skipProcessing) { + // Returns immediately without waiting + return { skipped: true } + } + + // Fetch only when needed + const userData = await fetchUserData(userId) + return processUserData(userData) +} +``` + +**Another example: early return optimization** + +```typescript +// Incorrect: always fetches permissions +async function updateResource(resourceId: string, userId: string) { + const permissions = await fetchPermissions(userId) + const resource = await getResource(resourceId) + + if (!resource) { + return { error: 'Not found' } + } + + if (!permissions.canEdit) { + return { error: 'Forbidden' } + } + + return await updateResourceData(resource, permissions) +} + +// Correct: fetches only when needed +async function updateResource(resourceId: string, userId: string) { + const resource = await getResource(resourceId) + + if (!resource) { + return { error: 'Not found' } + } + + const permissions = await fetchPermissions(userId) + + if (!permissions.canEdit) { + return { error: 'Forbidden' } + } + + return await updateResourceData(resource, permissions) +} +``` + +This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive. + +### 1.2 Dependency-Based Parallelization + +**Impact: CRITICAL (2-10ร— improvement)** + +For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment. + +**Incorrect: profile waits for config unnecessarily** + +```typescript +const [user, config] = await Promise.all([ + fetchUser(), + fetchConfig() +]) +const profile = await fetchProfile(user.id) +``` + +**Correct: config and profile run in parallel** + +```typescript +import { all } from 'better-all' + +const { user, config, profile } = await all({ + async user() { return fetchUser() }, + async config() { return fetchConfig() }, + async profile() { + return fetchProfile((await this.$.user).id) + } +}) +``` + +Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all) + +### 1.3 Prevent Waterfall Chains in API Routes + +**Impact: CRITICAL (2-10ร— improvement)** + +In API routes and Server Actions, start independent operations immediately, even if you don't await them yet. + +**Incorrect: config waits for auth, data waits for both** + +```typescript +export async function GET(request: Request) { + const session = await auth() + const config = await fetchConfig() + const data = await fetchData(session.user.id) + return Response.json({ data, config }) +} +``` + +**Correct: auth and config start immediately** + +```typescript +export async function GET(request: Request) { + const sessionPromise = auth() + const configPromise = fetchConfig() + const session = await sessionPromise + const [config, data] = await Promise.all([ + configPromise, + fetchData(session.user.id) + ]) + return Response.json({ data, config }) +} +``` + +For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization). + +### 1.4 Promise.all() for Independent Operations + +**Impact: CRITICAL (2-10ร— improvement)** + +When async operations have no interdependencies, execute them concurrently using `Promise.all()`. + +**Incorrect: sequential execution, 3 round trips** + +```typescript +const user = await fetchUser() +const posts = await fetchPosts() +const comments = await fetchComments() +``` + +**Correct: parallel execution, 1 round trip** + +```typescript +const [user, posts, comments] = await Promise.all([ + fetchUser(), + fetchPosts(), + fetchComments() +]) +``` + +### 1.5 Strategic Suspense Boundaries + +**Impact: HIGH (faster initial paint)** + +Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads. + +**Incorrect: wrapper blocked by data fetching** + +```tsx +async function Page() { + const data = await fetchData() // Blocks entire page + + return ( +
                      +
                      Sidebar
                      +
                      Header
                      +
                      + +
                      +
                      Footer
                      +
                      + ) +} +``` + +The entire layout waits for data even though only the middle section needs it. + +**Correct: wrapper shows immediately, data streams in** + +```tsx +function Page() { + return ( +
                      +
                      Sidebar
                      +
                      Header
                      +
                      + }> + + +
                      +
                      Footer
                      +
                      + ) +} + +async function DataDisplay() { + const data = await fetchData() // Only blocks this component + return
                      {data.content}
                      +} +``` + +Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data. + +**Alternative: share promise across components** + +```tsx +function Page() { + // Start fetch immediately, but don't await + const dataPromise = fetchData() + + return ( +
                      +
                      Sidebar
                      +
                      Header
                      + }> + + + +
                      Footer
                      +
                      + ) +} + +function DataDisplay({ dataPromise }: { dataPromise: Promise }) { + const data = use(dataPromise) // Unwraps the promise + return
                      {data.content}
                      +} + +function DataSummary({ dataPromise }: { dataPromise: Promise }) { + const data = use(dataPromise) // Reuses the same promise + return
                      {data.summary}
                      +} +``` + +Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together. + +**When NOT to use this pattern:** + +- Critical data needed for layout decisions (affects positioning) + +- SEO-critical content above the fold + +- Small, fast queries where suspense overhead isn't worth it + +- When you want to avoid layout shift (loading โ†’ content jump) + +**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities. + +--- + +## 2. Bundle Size Optimization + +**Impact: CRITICAL** + +Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint. + +### 2.1 Avoid Barrel File Imports + +**Impact: CRITICAL (200-800ms import cost, slow builds)** + +Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`). + +Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts. + +**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph. + +**Incorrect: imports entire library** + +```tsx +import { Check, X, Menu } from 'lucide-react' +// Loads 1,583 modules, takes ~2.8s extra in dev +// Runtime cost: 200-800ms on every cold start + +import { Button, TextField } from '@mui/material' +// Loads 2,225 modules, takes ~4.2s extra in dev +``` + +**Correct: imports only what you need** + +```tsx +import Check from 'lucide-react/dist/esm/icons/check' +import X from 'lucide-react/dist/esm/icons/x' +import Menu from 'lucide-react/dist/esm/icons/menu' +// Loads only 3 modules (~2KB vs ~1MB) + +import Button from '@mui/material/Button' +import TextField from '@mui/material/TextField' +// Loads only what you use +``` + +**Alternative: Next.js 13.5+** + +```js +// next.config.js - use optimizePackageImports +module.exports = { + experimental: { + optimizePackageImports: ['lucide-react', '@mui/material'] + } +} + +// Then you can keep the ergonomic barrel imports: +import { Check, X, Menu } from 'lucide-react' +// Automatically transformed to direct imports at build time +``` + +Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR. + +Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`. + +Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) + +### 2.2 Conditional Module Loading + +**Impact: HIGH (loads large data only when needed)** + +Load large data or modules only when a feature is activated. + +**Example: lazy-load animation frames** + +```tsx +function AnimationPlayer({ enabled }: { enabled: boolean }) { + const [frames, setFrames] = useState(null) + + useEffect(() => { + if (enabled && !frames && typeof window !== 'undefined') { + import('./animation-frames.js') + .then(mod => setFrames(mod.frames)) + .catch(() => setEnabled(false)) + } + }, [enabled, frames]) + + if (!frames) return + return +} +``` + +The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed. + +### 2.3 Defer Non-Critical Third-Party Libraries + +**Impact: MEDIUM (loads after hydration)** + +Analytics, logging, and error tracking don't block user interaction. Load them after hydration. + +**Incorrect: blocks initial bundle** + +```tsx +import { Analytics } from '@vercel/analytics/react' + +export default function RootLayout({ children }) { + return ( + + + {children} + + + + ) +} +``` + +**Correct: loads after hydration** + +```tsx +import dynamic from 'next/dynamic' + +const Analytics = dynamic( + () => import('@vercel/analytics/react').then(m => m.Analytics), + { ssr: false } +) + +export default function RootLayout({ children }) { + return ( + + + {children} + + + + ) +} +``` + +### 2.4 Dynamic Imports for Heavy Components + +**Impact: CRITICAL (directly affects TTI and LCP)** + +Use `next/dynamic` to lazy-load large components not needed on initial render. + +**Incorrect: Monaco bundles with main chunk ~300KB** + +```tsx +import { MonacoEditor } from './monaco-editor' + +function CodePanel({ code }: { code: string }) { + return +} +``` + +**Correct: Monaco loads on demand** + +```tsx +import dynamic from 'next/dynamic' + +const MonacoEditor = dynamic( + () => import('./monaco-editor').then(m => m.MonacoEditor), + { ssr: false } +) + +function CodePanel({ code }: { code: string }) { + return +} +``` + +### 2.5 Preload Based on User Intent + +**Impact: MEDIUM (reduces perceived latency)** + +Preload heavy bundles before they're needed to reduce perceived latency. + +**Example: preload on hover/focus** + +```tsx +function EditorButton({ onClick }: { onClick: () => void }) { + const preload = () => { + if (typeof window !== 'undefined') { + void import('./monaco-editor') + } + } + + return ( + + ) +} +``` + +**Example: preload when feature flag is enabled** + +```tsx +function FlagsProvider({ children, flags }: Props) { + useEffect(() => { + if (flags.editorEnabled && typeof window !== 'undefined') { + void import('./monaco-editor').then(mod => mod.init()) + } + }, [flags.editorEnabled]) + + return + {children} + +} +``` + +The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed. + +--- + +## 3. Server-Side Performance + +**Impact: HIGH** + +Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times. + +### 3.1 Cross-Request LRU Caching + +**Impact: HIGH (caches across requests)** + +`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache. + +**Implementation:** + +```typescript +import { LRUCache } from 'lru-cache' + +const cache = new LRUCache({ + max: 1000, + ttl: 5 * 60 * 1000 // 5 minutes +}) + +export async function getUser(id: string) { + const cached = cache.get(id) + if (cached) return cached + + const user = await db.user.findUnique({ where: { id } }) + cache.set(id, user) + return user +} + +// Request 1: DB query, result cached +// Request 2: cache hit, no DB query +``` + +Use when sequential user actions hit multiple endpoints needing the same data within seconds. + +**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis. + +**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching. + +Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache) + +### 3.2 Minimize Serialization at RSC Boundaries + +**Impact: HIGH (reduces data transfer size)** + +The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses. + +**Incorrect: serializes all 50 fields** + +```tsx +async function Page() { + const user = await fetchUser() // 50 fields + return +} + +'use client' +function Profile({ user }: { user: User }) { + return
                      {user.name}
                      // uses 1 field +} +``` + +**Correct: serializes only 1 field** + +```tsx +async function Page() { + const user = await fetchUser() + return +} + +'use client' +function Profile({ name }: { name: string }) { + return
                      {name}
                      +} +``` + +### 3.3 Parallel Data Fetching with Component Composition + +**Impact: CRITICAL (eliminates server-side waterfalls)** + +React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching. + +**Incorrect: Sidebar waits for Page's fetch to complete** + +```tsx +export default async function Page() { + const header = await fetchHeader() + return ( +
                      +
                      {header}
                      + +
                      + ) +} + +async function Sidebar() { + const items = await fetchSidebarItems() + return +} +``` + +**Correct: both fetch simultaneously** + +```tsx +async function Header() { + const data = await fetchHeader() + return
                      {data}
                      +} + +async function Sidebar() { + const items = await fetchSidebarItems() + return +} + +export default function Page() { + return ( +
                      +
                      + +
                      + ) +} +``` + +**Alternative with children prop:** + +```tsx +async function Layout({ children }: { children: ReactNode }) { + const header = await fetchHeader() + return ( +
                      +
                      {header}
                      + {children} +
                      + ) +} + +async function Sidebar() { + const items = await fetchSidebarItems() + return +} + +export default function Page() { + return ( + + + + ) +} +``` + +### 3.4 Per-Request Deduplication with React.cache() + +**Impact: MEDIUM (deduplicates within request)** + +Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most. + +**Usage:** + +```typescript +import { cache } from 'react' + +export const getCurrentUser = cache(async () => { + const session = await auth() + if (!session?.user?.id) return null + return await db.user.findUnique({ + where: { id: session.user.id } + }) +}) +``` + +Within a single request, multiple calls to `getCurrentUser()` execute the query only once. + +### 3.5 Use after() for Non-Blocking Operations + +**Impact: MEDIUM (faster response times)** + +Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response. + +**Incorrect: blocks response** + +```tsx +import { logUserAction } from '@/app/utils' + +export async function POST(request: Request) { + // Perform mutation + await updateDatabase(request) + + // Logging blocks the response + const userAgent = request.headers.get('user-agent') || 'unknown' + await logUserAction({ userAgent }) + + return new Response(JSON.stringify({ status: 'success' }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }) +} +``` + +**Correct: non-blocking** + +```tsx +import { after } from 'next/server' +import { headers, cookies } from 'next/headers' +import { logUserAction } from '@/app/utils' + +export async function POST(request: Request) { + // Perform mutation + await updateDatabase(request) + + // Log after response is sent + after(async () => { + const userAgent = (await headers()).get('user-agent') || 'unknown' + const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous' + + logUserAction({ sessionCookie, userAgent }) + }) + + return new Response(JSON.stringify({ status: 'success' }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }) +} +``` + +The response is sent immediately while logging happens in the background. + +**Common use cases:** + +- Analytics tracking + +- Audit logging + +- Sending notifications + +- Cache invalidation + +- Cleanup tasks + +**Important notes:** + +- `after()` runs even if the response fails or redirects + +- Works in Server Actions, Route Handlers, and Server Components + +Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after) + +--- + +## 4. Client-Side Data Fetching + +**Impact: MEDIUM-HIGH** + +Automatic deduplication and efficient data fetching patterns reduce redundant network requests. + +### 4.1 Deduplicate Global Event Listeners + +**Impact: LOW (single listener for N components)** + +Use `useSWRSubscription()` to share global event listeners across component instances. + +**Incorrect: N instances = N listeners** + +```tsx +function useKeyboardShortcut(key: string, callback: () => void) { + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.metaKey && e.key === key) { + callback() + } + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }, [key, callback]) +} +``` + +When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener. + +**Correct: N instances = 1 listener** + +```tsx +import useSWRSubscription from 'swr/subscription' + +// Module-level Map to track callbacks per key +const keyCallbacks = new Map void>>() + +function useKeyboardShortcut(key: string, callback: () => void) { + // Register this callback in the Map + useEffect(() => { + if (!keyCallbacks.has(key)) { + keyCallbacks.set(key, new Set()) + } + keyCallbacks.get(key)!.add(callback) + + return () => { + const set = keyCallbacks.get(key) + if (set) { + set.delete(callback) + if (set.size === 0) { + keyCallbacks.delete(key) + } + } + } + }, [key, callback]) + + useSWRSubscription('global-keydown', () => { + const handler = (e: KeyboardEvent) => { + if (e.metaKey && keyCallbacks.has(e.key)) { + keyCallbacks.get(e.key)!.forEach(cb => cb()) + } + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }) +} + +function Profile() { + // Multiple shortcuts will share the same listener + useKeyboardShortcut('p', () => { /* ... */ }) + useKeyboardShortcut('k', () => { /* ... */ }) + // ... +} +``` + +### 4.2 Use SWR for Automatic Deduplication + +**Impact: MEDIUM-HIGH (automatic deduplication)** + +SWR enables request deduplication, caching, and revalidation across component instances. + +**Incorrect: no deduplication, each instance fetches** + +```tsx +function UserList() { + const [users, setUsers] = useState([]) + useEffect(() => { + fetch('/api/users') + .then(r => r.json()) + .then(setUsers) + }, []) +} +``` + +**Correct: multiple instances share one request** + +```tsx +import useSWR from 'swr' + +function UserList() { + const { data: users } = useSWR('/api/users', fetcher) +} +``` + +**For immutable data:** + +```tsx +import { useImmutableSWR } from '@/lib/swr' + +function StaticContent() { + const { data } = useImmutableSWR('/api/config', fetcher) +} +``` + +**For mutations:** + +```tsx +import { useSWRMutation } from 'swr/mutation' + +function UpdateButton() { + const { trigger } = useSWRMutation('/api/user', updateUser) + return +} +``` + +Reference: [https://swr.vercel.app](https://swr.vercel.app) + +--- + +## 5. Re-render Optimization + +**Impact: MEDIUM** + +Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness. + +### 5.1 Defer State Reads to Usage Point + +**Impact: MEDIUM (avoids unnecessary subscriptions)** + +Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks. + +**Incorrect: subscribes to all searchParams changes** + +```tsx +function ShareButton({ chatId }: { chatId: string }) { + const searchParams = useSearchParams() + + const handleShare = () => { + const ref = searchParams.get('ref') + shareChat(chatId, { ref }) + } + + return +} +``` + +**Correct: reads on demand, no subscription** + +```tsx +function ShareButton({ chatId }: { chatId: string }) { + const handleShare = () => { + const params = new URLSearchParams(window.location.search) + const ref = params.get('ref') + shareChat(chatId, { ref }) + } + + return +} +``` + +### 5.2 Extract to Memoized Components + +**Impact: MEDIUM (enables early returns)** + +Extract expensive work into memoized components to enable early returns before computation. + +**Incorrect: computes avatar even when loading** + +```tsx +function Profile({ user, loading }: Props) { + const avatar = useMemo(() => { + const id = computeAvatarId(user) + return + }, [user]) + + if (loading) return + return
                      {avatar}
                      +} +``` + +**Correct: skips computation when loading** + +```tsx +const UserAvatar = memo(function UserAvatar({ user }: { user: User }) { + const id = useMemo(() => computeAvatarId(user), [user]) + return +}) + +function Profile({ user, loading }: Props) { + if (loading) return + return ( +
                      + +
                      + ) +} +``` + +**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders. + +### 5.3 Narrow Effect Dependencies + +**Impact: LOW (minimizes effect re-runs)** + +Specify primitive dependencies instead of objects to minimize effect re-runs. + +**Incorrect: re-runs on any user field change** + +```tsx +useEffect(() => { + console.log(user.id) +}, [user]) +``` + +**Correct: re-runs only when id changes** + +```tsx +useEffect(() => { + console.log(user.id) +}, [user.id]) +``` + +**For derived state, compute outside effect:** + +```tsx +// Incorrect: runs on width=767, 766, 765... +useEffect(() => { + if (width < 768) { + enableMobileMode() + } +}, [width]) + +// Correct: runs only on boolean transition +const isMobile = width < 768 +useEffect(() => { + if (isMobile) { + enableMobileMode() + } +}, [isMobile]) +``` + +### 5.4 Subscribe to Derived State + +**Impact: MEDIUM (reduces re-render frequency)** + +Subscribe to derived boolean state instead of continuous values to reduce re-render frequency. + +**Incorrect: re-renders on every pixel change** + +```tsx +function Sidebar() { + const width = useWindowWidth() // updates continuously + const isMobile = width < 768 + return