mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-20 02:15:12 +00:00
Merge branch 'main' into feature/mom_2026
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
# categor - TypeScript template literal in website/src/scripts/pages/skills.ts:70 (categor${...length > 1 ? "ies" : "y"})
|
||||
# aline - proper name (Aline Ávila, contributor)
|
||||
# ative - part of "Declarative Agents" in TypeSpec M365 Copilot documentation (collections/typespec-m365-copilot.collection.md)
|
||||
ignore-words-list = numer,wit,aks,edn,ser,ois,gir,rouge,categor,aline,ative,afterall,deques
|
||||
# dateA, dateB - variable names used in sorting comparison functions
|
||||
ignore-words-list = numer,wit,aks,edn,ser,ois,gir,rouge,categor,aline,ative,afterall,deques,dateA,dateB
|
||||
# Skip certain files and directories
|
||||
skip = .git,node_modules,package-lock.json,*.lock,website/build,website/.docusaurus
|
||||
|
||||
2
.github/workflows/deploy-website.yml
vendored
2
.github/workflows/deploy-website.yml
vendored
@@ -40,6 +40,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Full history needed for git-based last updated dates
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
@@ -122,7 +122,7 @@ dotnet msbuild <ProjectName>.csproj /t:GenerateRestoreGraphFile /p:RestoreGraphO
|
||||
## Classification Rules
|
||||
- `TargetFramework` starts with `netcoreapp`, `net5.0+`, `net6.0+`, etc. → **Modern .NET**
|
||||
- `netstandard*` → **.NET Standard** (migrate to current .NET version)
|
||||
- `net4*` → **.NET Framework** (migrate via intermediate step to .NET 6+)
|
||||
- `net4*` → **.NET Framework** (migrate via intermediate step to .NET 8+)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ You want to understand how long PRs have been open in a repository. This tool de
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
pip install copilot-sdk
|
||||
pip install github-copilot-sdk
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -98,7 +98,7 @@ Ready-to-use prompt templates for specific development scenarios and tasks, defi
|
||||
| [Microsoft 365 Declarative Agents Development Kit](../prompts/declarative-agents.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fdeclarative-agents.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode-insiders%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fdeclarative-agents.prompt.md) | Complete development kit for Microsoft 365 Copilot declarative agents with three comprehensive workflows (basic, advanced, validation), TypeSpec support, and Microsoft 365 Agents Toolkit integration |
|
||||
| [Migration and Code Evolution Instructions Generator](../prompts/generate-custom-instructions-from-codebase.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fgenerate-custom-instructions-from-codebase.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode-insiders%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fgenerate-custom-instructions-from-codebase.prompt.md) | Migration and code evolution instructions generator for GitHub Copilot. Analyzes differences between two project versions (branches, commits, or releases) to create precise instructions allowing Copilot to maintain consistency during technology migrations, major refactoring, or framework version upgrades. |
|
||||
| [MkDocs AI Translator](../prompts/mkdocs-translations.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fmkdocs-translations.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode-insiders%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fmkdocs-translations.prompt.md) | Generate a language translation for a mkdocs documentation stack. |
|
||||
| [MSTest Best Practices](../prompts/csharp-mstest.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fcsharp-mstest.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode-insiders%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fcsharp-mstest.prompt.md) | Get best practices for MSTest unit testing, including data-driven tests |
|
||||
| [MSTest Best Practices (MSTest 3.x/4.x)](../prompts/csharp-mstest.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fcsharp-mstest.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode-insiders%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fcsharp-mstest.prompt.md) | Get best practices for MSTest 3.x/4.x unit testing, including modern assertion APIs and data-driven tests |
|
||||
| [Multi Stage Dockerfile](../prompts/multi-stage-dockerfile.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fmulti-stage-dockerfile.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode-insiders%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fmulti-stage-dockerfile.prompt.md) | Create optimized multi-stage Dockerfiles for any language or framework |
|
||||
| [My Issues](../prompts/my-issues.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fmy-issues.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode-insiders%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fmy-issues.prompt.md) | List my issues in the current repository |
|
||||
| [My Pull Requests](../prompts/my-pull-requests.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fmy-pull-requests.prompt.md)<br />[](https://aka.ms/awesome-copilot/install/prompt?url=vscode-insiders%3Achat-prompt%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fprompts%2Fmy-pull-requests.prompt.md) | List my pull requests in the current repository |
|
||||
|
||||
@@ -31,6 +31,7 @@ Skills differ from other primitives by supporting bundled assets (scripts, code
|
||||
| [azure-static-web-apps](../skills/azure-static-web-apps/SKILL.md) | Helps create, configure, and deploy Azure Static Web Apps using the SWA CLI. Use when deploying static sites to Azure, setting up SWA local development, configuring staticwebapp.config.json, adding Azure Functions APIs to SWA, or setting up GitHub Actions CI/CD for Static Web Apps. | None |
|
||||
| [chrome-devtools](../skills/chrome-devtools/SKILL.md) | Expert-level browser automation, debugging, and performance analysis using Chrome DevTools MCP. Use for interacting with web pages, capturing screenshots, analyzing network traffic, and profiling performance. | None |
|
||||
| [copilot-sdk](../skills/copilot-sdk/SKILL.md) | Build agentic applications with GitHub Copilot SDK. Use when embedding AI agents in apps, creating custom tools, implementing streaming responses, managing sessions, connecting to MCP servers, or creating custom agents. Triggers on Copilot SDK, GitHub SDK, agentic app, embed Copilot, programmable agent, MCP server, custom agent. | None |
|
||||
| [excalidraw-diagram-generator](../skills/excalidraw-diagram-generator/SKILL.md) | Generate Excalidraw diagrams from natural language descriptions. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", or "generate an Excalidraw file". Supports flowcharts, relationship diagrams, mind maps, and system architecture diagrams. Outputs .excalidraw JSON files that can be opened directly in Excalidraw. | `references/element-types.md`<br />`references/excalidraw-schema.md`<br />`scripts/.gitignore`<br />`scripts/README.md`<br />`scripts/add-arrow.py`<br />`scripts/add-icon-to-diagram.py`<br />`scripts/split-excalidraw-library.py`<br />`templates/business-flow-swimlane-template.excalidraw`<br />`templates/class-diagram-template.excalidraw`<br />`templates/data-flow-diagram-template.excalidraw`<br />`templates/er-diagram-template.excalidraw`<br />`templates/flowchart-template.excalidraw`<br />`templates/mindmap-template.excalidraw`<br />`templates/relationship-template.excalidraw`<br />`templates/sequence-diagram-template.excalidraw` |
|
||||
| [gh-cli](../skills/gh-cli/SKILL.md) | GitHub CLI (gh) comprehensive reference for repositories, issues, pull requests, Actions, projects, releases, gists, codespaces, organizations, extensions, and all GitHub operations from the command line. | None |
|
||||
| [git-commit](../skills/git-commit/SKILL.md) | Execute git commit with conventional commit message analysis, intelligent staging, and message generation. Use when user asks to commit changes, create a git commit, or mentions "/commit". Supports: (1) Auto-detecting type and scope from changes, (2) Generating conventional commit messages from diff, (3) Interactive commit with optional type/scope/description overrides, (4) Intelligent file staging for logical grouping | None |
|
||||
| [github-issues](../skills/github-issues/SKILL.md) | Create, update, and manage GitHub issues using MCP tools. Use this skill when users want to create bug reports, feature requests, or task issues, update existing issues, add labels/assignees/milestones, or manage issue workflows. Triggers on requests like "create an issue", "file a bug", "request a feature", "update issue X", or any GitHub issue management task. | `references/templates.md` |
|
||||
@@ -43,7 +44,9 @@ Skills differ from other primitives by supporting bundled assets (scripts, code
|
||||
| [microsoft-code-reference](../skills/microsoft-code-reference/SKILL.md) | Look up Microsoft API references, find working code samples, and verify SDK code is correct. Use when working with Azure SDKs, .NET libraries, or Microsoft APIs—to find the right method, check parameters, get working examples, or troubleshoot errors. Catches hallucinated methods, wrong signatures, and deprecated patterns by querying official docs. | None |
|
||||
| [microsoft-docs](../skills/microsoft-docs/SKILL.md) | Query official Microsoft documentation to understand concepts, find tutorials, and learn how services work. Use for Azure, .NET, Microsoft 365, Windows, Power Platform, and all Microsoft technologies. Get accurate, current information from learn.microsoft.com and other official Microsoft websites—architecture overviews, quickstarts, configuration guides, limits, and best practices. | None |
|
||||
| [nuget-manager](../skills/nuget-manager/SKILL.md) | Manage NuGet packages in .NET projects/solutions. Use this skill when adding, removing, or updating NuGet package versions. It enforces using `dotnet` CLI for package management and provides strict procedures for direct file edits only when updating versions. | None |
|
||||
| [penpot-uiux-design](../skills/penpot-uiux-design/SKILL.md) | Comprehensive guide for creating professional UI/UX designs in Penpot using MCP tools. Use this skill when: (1) Creating new UI/UX designs for web, mobile, or desktop applications, (2) Building design systems with components and tokens, (3) Designing dashboards, forms, navigation, or landing pages, (4) Applying accessibility standards and best practices, (5) Following platform guidelines (iOS, Android, Material Design), (6) Reviewing or improving existing Penpot designs for usability. Triggers: "design a UI", "create interface", "build layout", "design dashboard", "create form", "design landing page", "make it accessible", "design system", "component library". | `references/accessibility.md`<br />`references/component-patterns.md`<br />`references/platform-guidelines.md`<br />`references/setup-troubleshooting.md` |
|
||||
| [plantuml-ascii](../skills/plantuml-ascii/SKILL.md) | Generate ASCII art diagrams using PlantUML text mode. Use when user asks to create ASCII diagrams, text-based diagrams, terminal-friendly diagrams, or mentions plantuml ascii, text diagram, ascii art diagram. Supports: Converting PlantUML diagrams to ASCII art, Creating sequence diagrams, class diagrams, flowcharts in ASCII format, Generating Unicode-enhanced ASCII art with -utxt flag | None |
|
||||
| [powerbi-modeling](../skills/powerbi-modeling/SKILL.md) | Power BI semantic modeling assistant for building optimized data models. Use when working with Power BI semantic models, creating measures, designing star schemas, configuring relationships, implementing RLS, or optimizing model performance. Triggers on queries about DAX calculations, table relationships, dimension/fact table design, naming conventions, model documentation, cardinality, cross-filter direction, calculation groups, and data model best practices. Always connects to the active model first using power-bi-modeling MCP tools to understand the data structure before providing guidance. | `references/MEASURES-DAX.md`<br />`references/PERFORMANCE.md`<br />`references/RELATIONSHIPS.md`<br />`references/RLS.md`<br />`references/STAR-SCHEMA.md` |
|
||||
| [prd](../skills/prd/SKILL.md) | Generate high-quality Product Requirements Documents (PRDs) for software systems and AI-powered features. Includes executive summaries, user stories, technical specifications, and risk analysis. | None |
|
||||
| [refactor](../skills/refactor/SKILL.md) | Surgical code refactoring to improve maintainability without changing behavior. Covers extracting functions, renaming variables, breaking down god functions, improving type safety, eliminating code smells, and applying design patterns. Less drastic than repo-rebuilder; use for gradual improvements. | None |
|
||||
| [scoutqa-test](../skills/scoutqa-test/SKILL.md) | This skill should be used when the user asks to "test this website", "run exploratory testing", "check for accessibility issues", "verify the login flow works", "find bugs on this page", or requests automated QA testing. Triggers on web application testing scenarios including smoke tests, accessibility audits, e-commerce flows, and user flow validation using ScoutQA CLI. IMPORTANT: Use this skill proactively after implementing web application features to verify they work correctly - don't wait for the user to ask for testing. | None |
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
parseSkillMetadata,
|
||||
parseYamlFile,
|
||||
} from "./yaml-parser.mjs";
|
||||
import { getGitFileDates } from "./utils/git-dates.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
@@ -64,7 +65,7 @@ function extractTitle(filePath, frontmatter) {
|
||||
/**
|
||||
* Generate agents metadata
|
||||
*/
|
||||
function generateAgentsData() {
|
||||
function generateAgentsData(gitDates) {
|
||||
const agents = [];
|
||||
const files = fs
|
||||
.readdirSync(AGENTS_DIR)
|
||||
@@ -105,6 +106,7 @@ function generateAgentsData() {
|
||||
: [],
|
||||
path: relativePath,
|
||||
filename: file,
|
||||
lastUpdated: gitDates.get(relativePath) || null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,7 +125,7 @@ function generateAgentsData() {
|
||||
/**
|
||||
* Generate prompts metadata
|
||||
*/
|
||||
function generatePromptsData() {
|
||||
function generatePromptsData(gitDates) {
|
||||
const prompts = [];
|
||||
const files = fs
|
||||
.readdirSync(PROMPTS_DIR)
|
||||
@@ -151,6 +153,7 @@ function generatePromptsData() {
|
||||
tools: tools,
|
||||
path: relativePath,
|
||||
filename: file,
|
||||
lastUpdated: gitDates.get(relativePath) || null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -206,7 +209,7 @@ function extractExtensionFromPattern(pattern) {
|
||||
/**
|
||||
* Generate instructions metadata
|
||||
*/
|
||||
function generateInstructionsData() {
|
||||
function generateInstructionsData(gitDates) {
|
||||
const instructions = [];
|
||||
const files = fs
|
||||
.readdirSync(INSTRUCTIONS_DIR)
|
||||
@@ -253,6 +256,7 @@ function generateInstructionsData() {
|
||||
extensions: [...new Set(extensions)],
|
||||
path: relativePath,
|
||||
filename: file,
|
||||
lastUpdated: gitDates.get(relativePath) || null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -316,7 +320,7 @@ function categorizeSkill(name, description) {
|
||||
/**
|
||||
* Generate skills metadata
|
||||
*/
|
||||
function generateSkillsData() {
|
||||
function generateSkillsData(gitDates) {
|
||||
const skills = [];
|
||||
|
||||
if (!fs.existsSync(SKILLS_DIR)) {
|
||||
@@ -343,6 +347,9 @@ function generateSkillsData() {
|
||||
// Get all files in the skill folder recursively
|
||||
const files = getSkillFiles(skillPath, relativePath);
|
||||
|
||||
// Get last updated from SKILL.md file
|
||||
const skillFilePath = `${relativePath}/SKILL.md`;
|
||||
|
||||
skills.push({
|
||||
id: folder,
|
||||
name: metadata.name,
|
||||
@@ -356,8 +363,9 @@ function generateSkillsData() {
|
||||
assetCount: metadata.assets.length,
|
||||
category: category,
|
||||
path: relativePath,
|
||||
skillFile: `${relativePath}/SKILL.md`,
|
||||
skillFile: skillFilePath,
|
||||
files: files,
|
||||
lastUpdated: gitDates.get(skillFilePath) || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -406,7 +414,7 @@ function getSkillFiles(skillPath, relativePath) {
|
||||
/**
|
||||
* Generate collections metadata
|
||||
*/
|
||||
function generateCollectionsData() {
|
||||
function generateCollectionsData(gitDates) {
|
||||
const collections = [];
|
||||
|
||||
if (!fs.existsSync(COLLECTIONS_DIR)) {
|
||||
@@ -447,6 +455,7 @@ function generateCollectionsData() {
|
||||
})),
|
||||
path: relativePath,
|
||||
filename: file,
|
||||
lastUpdated: gitDates.get(relativePath) || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -542,6 +551,7 @@ function generateSearchIndex(
|
||||
title: agent.title,
|
||||
description: agent.description,
|
||||
path: agent.path,
|
||||
lastUpdated: agent.lastUpdated,
|
||||
searchText: `${agent.title} ${agent.description} ${agent.tools.join(
|
||||
" "
|
||||
)}`.toLowerCase(),
|
||||
@@ -555,6 +565,7 @@ function generateSearchIndex(
|
||||
title: prompt.title,
|
||||
description: prompt.description,
|
||||
path: prompt.path,
|
||||
lastUpdated: prompt.lastUpdated,
|
||||
searchText: `${prompt.title} ${prompt.description}`.toLowerCase(),
|
||||
});
|
||||
}
|
||||
@@ -566,6 +577,7 @@ function generateSearchIndex(
|
||||
title: instruction.title,
|
||||
description: instruction.description,
|
||||
path: instruction.path,
|
||||
lastUpdated: instruction.lastUpdated,
|
||||
searchText: `${instruction.title} ${instruction.description} ${
|
||||
instruction.applyTo || ""
|
||||
}`.toLowerCase(),
|
||||
@@ -579,6 +591,7 @@ function generateSearchIndex(
|
||||
title: skill.title,
|
||||
description: skill.description,
|
||||
path: skill.skillFile,
|
||||
lastUpdated: skill.lastUpdated,
|
||||
searchText: `${skill.title} ${skill.description}`.toLowerCase(),
|
||||
});
|
||||
}
|
||||
@@ -591,6 +604,7 @@ function generateSearchIndex(
|
||||
description: collection.description,
|
||||
path: collection.path,
|
||||
tags: collection.tags,
|
||||
lastUpdated: collection.lastUpdated,
|
||||
searchText: `${collection.name} ${
|
||||
collection.description
|
||||
} ${collection.tags.join(" ")}`.toLowerCase(),
|
||||
@@ -703,32 +717,40 @@ async function main() {
|
||||
|
||||
ensureDataDir();
|
||||
|
||||
// Load git dates for all resource files (single efficient git command)
|
||||
console.log("Loading git history for last updated dates...");
|
||||
const gitDates = getGitFileDates(
|
||||
["agents/", "prompts/", "instructions/", "skills/", "collections/"],
|
||||
ROOT_FOLDER
|
||||
);
|
||||
console.log(`✓ Loaded dates for ${gitDates.size} files\n`);
|
||||
|
||||
// Generate all data
|
||||
const agentsData = generateAgentsData();
|
||||
const agentsData = generateAgentsData(gitDates);
|
||||
const agents = agentsData.items;
|
||||
console.log(
|
||||
`✓ Generated ${agents.length} agents (${agentsData.filters.models.length} models, ${agentsData.filters.tools.length} tools)`
|
||||
);
|
||||
|
||||
const promptsData = generatePromptsData();
|
||||
const promptsData = generatePromptsData(gitDates);
|
||||
const prompts = promptsData.items;
|
||||
console.log(
|
||||
`✓ Generated ${prompts.length} prompts (${promptsData.filters.tools.length} tools)`
|
||||
);
|
||||
|
||||
const instructionsData = generateInstructionsData();
|
||||
const instructionsData = generateInstructionsData(gitDates);
|
||||
const instructions = instructionsData.items;
|
||||
console.log(
|
||||
`✓ Generated ${instructions.length} instructions (${instructionsData.filters.extensions.length} extensions)`
|
||||
);
|
||||
|
||||
const skillsData = generateSkillsData();
|
||||
const skillsData = generateSkillsData(gitDates);
|
||||
const skills = skillsData.items;
|
||||
console.log(
|
||||
`✓ Generated ${skills.length} skills (${skillsData.filters.categories.length} categories)`
|
||||
);
|
||||
|
||||
const collectionsData = generateCollectionsData();
|
||||
const collectionsData = generateCollectionsData(gitDates);
|
||||
const collections = collectionsData.items;
|
||||
console.log(
|
||||
`✓ Generated ${collections.length} collections (${collectionsData.filters.tags.length} tags)`
|
||||
|
||||
103
eng/utils/git-dates.mjs
Normal file
103
eng/utils/git-dates.mjs
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Utility to extract last modification dates from git history.
|
||||
* Uses a single git log command for efficiency.
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* Get the last modification date for all tracked files in specified directories.
|
||||
* Returns a Map of file path -> ISO date string.
|
||||
*
|
||||
* @param {string[]} directories - Array of directory paths to scan
|
||||
* @param {string} rootDir - Root directory for relative paths
|
||||
* @returns {Map<string, string>} Map of relative file path to ISO date string
|
||||
*/
|
||||
export function getGitFileDates(directories, rootDir) {
|
||||
const fileDates = new Map();
|
||||
|
||||
try {
|
||||
// Get git log with file names for all specified directories
|
||||
// Format: ISO date, then file names that were modified in that commit
|
||||
const gitArgs = [
|
||||
"--no-pager",
|
||||
"log",
|
||||
"--format=%aI", // Author date in ISO 8601 format
|
||||
"--name-only",
|
||||
"--diff-filter=ACMR", // Added, Copied, Modified, Renamed
|
||||
"--",
|
||||
...directories,
|
||||
];
|
||||
|
||||
const output = execSync(`git ${gitArgs.join(" ")}`, {
|
||||
encoding: "utf8",
|
||||
cwd: rootDir,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
// Parse the output: alternating date lines and file name lines
|
||||
// Format is:
|
||||
// 2026-01-15T10:30:00+00:00
|
||||
//
|
||||
// file1.md
|
||||
// file2.md
|
||||
//
|
||||
// 2026-01-14T09:00:00+00:00
|
||||
// ...
|
||||
|
||||
let currentDate = null;
|
||||
const lines = output.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is a date line (ISO 8601 format)
|
||||
if (/^\d{4}-\d{2}-\d{2}T/.test(trimmed)) {
|
||||
currentDate = trimmed;
|
||||
} else if (currentDate && trimmed) {
|
||||
// This is a file path - only set if we haven't seen this file yet
|
||||
// (first occurrence is the most recent modification)
|
||||
if (!fileDates.has(trimmed)) {
|
||||
fileDates.set(trimmed, currentDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Git command failed - might not be a git repo or no history
|
||||
console.warn("Warning: Could not get git dates:", error.message);
|
||||
}
|
||||
|
||||
return fileDates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last modification date for a single file.
|
||||
*
|
||||
* @param {string} filePath - Path to the file (relative to git root)
|
||||
* @param {string} rootDir - Root directory
|
||||
* @returns {string|null} ISO date string or null if not found
|
||||
*/
|
||||
export function getGitFileDate(filePath, rootDir) {
|
||||
try {
|
||||
const output = execSync(
|
||||
`git --no-pager log -1 --format="%aI" -- "${filePath}"`,
|
||||
{
|
||||
encoding: "utf8",
|
||||
cwd: rootDir,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
}
|
||||
);
|
||||
|
||||
const date = output.trim();
|
||||
return date || null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -18,11 +18,11 @@ name: "GitHub Copilot SDK Python Instructions"
|
||||
Always install via pip:
|
||||
|
||||
```bash
|
||||
pip install copilot-sdk
|
||||
pip install github-copilot-sdk
|
||||
# or with poetry
|
||||
poetry add copilot-sdk
|
||||
poetry add github-copilot-sdk
|
||||
# or with uv
|
||||
uv add copilot-sdk
|
||||
uv add github-copilot-sdk
|
||||
```
|
||||
|
||||
## Client Initialization
|
||||
|
||||
@@ -23,9 +23,9 @@ Follow the steps **sequentially** and **do not attempt to upgrade all projects a
|
||||
- Note the current target and SDK.
|
||||
|
||||
2. **Select Target Version**
|
||||
- **.NET (Core/Modern)**: Upgrade to the latest LTS (e.g., `net8.0`).
|
||||
- **.NET Standard**: Prefer migrating to **.NET 6+** if possible. If staying, target `netstandard2.1`.
|
||||
- **.NET Framework**: Upgrade to at least **4.8**, or migrate to .NET 6+ if feasible.
|
||||
- **.NET (Core/Modern)**: Upgrade to the latest LTS (e.g., `net10.0`).
|
||||
- **.NET Standard**: Prefer migrating to **.NET 8+** if possible. If staying, target `netstandard2.1`.
|
||||
- **.NET Framework**: Upgrade to at least **4.8**, or migrate to .NET 8+ if feasible.
|
||||
|
||||
3. **Review Release Notes & Breaking Changes**
|
||||
- [.NET Core/.NET Upgrade Docs](https://learn.microsoft.com/dotnet/core/whats-new/)
|
||||
@@ -76,7 +76,7 @@ For each project:
|
||||
```
|
||||
|
||||
2. Check for:
|
||||
- `TargetFramework` → Change to the desired version (e.g., `net8.0`).
|
||||
- `TargetFramework` → Change to the desired version (e.g., `net10.0`).
|
||||
- `PackageReference` → Verify if each NuGet package supports the new framework.
|
||||
- Run:
|
||||
```bash
|
||||
@@ -148,7 +148,7 @@ BlobServiceClient client = new BlobServiceClient(connectionString);
|
||||
- Common issues:
|
||||
- Deprecated APIs → Replace with supported alternatives.
|
||||
- Package incompatibility → Find updated NuGet or migrate to Microsoft-supported library.
|
||||
- Configuration differences (e.g., `Startup.cs` → `Program.cs` in .NET 6+).
|
||||
- Configuration differences (e.g., `Startup.cs` → `Program.cs` in .NET 8+).
|
||||
|
||||
|
||||
---
|
||||
@@ -239,9 +239,9 @@ Use this table as a sample to track the progress of the upgrade across all proje
|
||||
|
||||
| Project Name | Target Framework | Dependencies Updated | Builds Successfully | Tests Passing | Deployment Verified | Notes |
|
||||
|--------------|------------------|-----------------------|---------------------|---------------|---------------------|-------|
|
||||
| Project A | ☐ net8.0 | ☐ | ☐ | ☐ | ☐ | |
|
||||
| Project B | ☐ net8.0 | ☐ | ☐ | ☐ | ☐ | |
|
||||
| Project C | ☐ net8.0 | ☐ | ☐ | ☐ | ☐ | |
|
||||
| Project A | ☐ net10.0 | ☐ | ☐ | ☐ | ☐ | |
|
||||
| Project B | ☐ net10.0 | ☐ | ☐ | ☐ | ☐ | |
|
||||
| Project C | ☐ net10.0 | ☐ | ☐ | ☐ | ☐ | |
|
||||
|
||||
> ✅ Mark each column as you complete the step for every project.
|
||||
|
||||
@@ -272,7 +272,7 @@ For organizations with multiple repositories:
|
||||
## 🔑 Notes & Best Practices
|
||||
|
||||
- **Prefer Migration to Modern .NET**
|
||||
If on .NET Framework or .NET Standard, evaluate moving to .NET 6/8 for long-term support.
|
||||
If on .NET Framework or .NET Standard, evaluate moving to .NET 8/10 for long-term support.
|
||||
- **Automate Tests Early**
|
||||
CI/CD should block merges if tests fail.
|
||||
- **Incremental Upgrades**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
description: 'Prompt and workflow for generating conventional commit messages using a structured XML format. Guides users to create standardized, descriptive commit messages in line with the Conventional Commits specification, including instructions, examples, and validation.'
|
||||
tools: ['runCommands/runInTerminal', 'runCommands/getTerminalOutput']
|
||||
tools: ['execute/runInTerminal', 'execute/getTerminalOutput']
|
||||
---
|
||||
|
||||
### Instructions
|
||||
|
||||
@@ -1,67 +1,479 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
tools: ['changes', 'search/codebase', 'edit/editFiles', 'problems', 'search']
|
||||
description: 'Get best practices for MSTest unit testing, including data-driven tests'
|
||||
description: 'Get best practices for MSTest 3.x/4.x unit testing, including modern assertion APIs and data-driven tests'
|
||||
---
|
||||
|
||||
# MSTest Best Practices
|
||||
# MSTest Best Practices (MSTest 3.x/4.x)
|
||||
|
||||
Your goal is to help me write effective unit tests with MSTest, covering both standard and data-driven testing approaches.
|
||||
Your goal is to help me write effective unit tests with modern MSTest, using current APIs and best practices.
|
||||
|
||||
## Project Setup
|
||||
|
||||
- Use a separate test project with naming convention `[ProjectName].Tests`
|
||||
- Reference MSTest package
|
||||
- Create test classes that match the classes being tested (e.g., `CalculatorTests` for `Calculator`)
|
||||
- Use .NET SDK test commands: `dotnet test` for running tests
|
||||
- Reference MSTest 3.x+ NuGet packages (includes analyzers)
|
||||
- Consider using MSTest.Sdk for simplified project setup
|
||||
- Run tests with `dotnet test`
|
||||
|
||||
## Test Structure
|
||||
## Test Class Structure
|
||||
|
||||
- Use `[TestClass]` attribute for test classes
|
||||
- Use `[TestMethod]` attribute for test methods
|
||||
- Follow the Arrange-Act-Assert (AAA) pattern
|
||||
- Name tests using the pattern `MethodName_Scenario_ExpectedBehavior`
|
||||
- Use `[TestInitialize]` and `[TestCleanup]` for per-test setup and teardown
|
||||
- Use `[ClassInitialize]` and `[ClassCleanup]` for per-class setup and teardown
|
||||
- Use `[AssemblyInitialize]` and `[AssemblyCleanup]` for assembly-level setup and teardown
|
||||
- **Seal test classes by default** for performance and design clarity
|
||||
- Use `[TestMethod]` for test methods (prefer over `[DataTestMethod]`)
|
||||
- Follow Arrange-Act-Assert (AAA) pattern
|
||||
- Name tests using pattern `MethodName_Scenario_ExpectedBehavior`
|
||||
|
||||
## Standard Tests
|
||||
```csharp
|
||||
[TestClass]
|
||||
public sealed class CalculatorTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Add_TwoPositiveNumbers_ReturnsSum()
|
||||
{
|
||||
// Arrange
|
||||
var calculator = new Calculator();
|
||||
|
||||
- Keep tests focused on a single behavior
|
||||
- Avoid testing multiple behaviors in one test method
|
||||
- Use clear assertions that express intent
|
||||
- Include only the assertions needed to verify the test case
|
||||
- Make tests independent and idempotent (can run in any order)
|
||||
- Avoid test interdependencies
|
||||
// Act
|
||||
var result = calculator.Add(2, 3);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(5, result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test Lifecycle
|
||||
|
||||
- **Prefer constructors over `[TestInitialize]`** - enables `readonly` fields and follows standard C# patterns
|
||||
- Use `[TestCleanup]` for cleanup that must run even if test fails
|
||||
- Combine constructor with async `[TestInitialize]` when async setup is needed
|
||||
|
||||
```csharp
|
||||
[TestClass]
|
||||
public sealed class ServiceTests
|
||||
{
|
||||
private readonly MyService _service; // readonly enabled by constructor
|
||||
|
||||
public ServiceTests()
|
||||
{
|
||||
_service = new MyService();
|
||||
}
|
||||
|
||||
[TestInitialize]
|
||||
public async Task InitAsync()
|
||||
{
|
||||
// Use for async initialization only
|
||||
await _service.WarmupAsync();
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Cleanup() => _service.Reset();
|
||||
}
|
||||
```
|
||||
|
||||
### Execution Order
|
||||
|
||||
1. **Assembly Initialization** - `[AssemblyInitialize]` (once per test assembly)
|
||||
2. **Class Initialization** - `[ClassInitialize]` (once per test class)
|
||||
3. **Test Initialization** (for every test method):
|
||||
1. Constructor
|
||||
2. Set `TestContext` property
|
||||
3. `[TestInitialize]`
|
||||
4. **Test Execution** - test method runs
|
||||
5. **Test Cleanup** (for every test method):
|
||||
1. `[TestCleanup]`
|
||||
2. `DisposeAsync` (if implemented)
|
||||
3. `Dispose` (if implemented)
|
||||
6. **Class Cleanup** - `[ClassCleanup]` (once per test class)
|
||||
7. **Assembly Cleanup** - `[AssemblyCleanup]` (once per test assembly)
|
||||
|
||||
## Modern Assertion APIs
|
||||
|
||||
MSTest provides three assertion classes: `Assert`, `StringAssert`, and `CollectionAssert`.
|
||||
|
||||
### Assert Class - Core Assertions
|
||||
|
||||
```csharp
|
||||
// Equality
|
||||
Assert.AreEqual(expected, actual);
|
||||
Assert.AreNotEqual(notExpected, actual);
|
||||
Assert.AreSame(expectedObject, actualObject); // Reference equality
|
||||
Assert.AreNotSame(notExpectedObject, actualObject);
|
||||
|
||||
// Null checks
|
||||
Assert.IsNull(value);
|
||||
Assert.IsNotNull(value);
|
||||
|
||||
// Boolean
|
||||
Assert.IsTrue(condition);
|
||||
Assert.IsFalse(condition);
|
||||
|
||||
// Fail/Inconclusive
|
||||
Assert.Fail("Test failed due to...");
|
||||
Assert.Inconclusive("Test cannot be completed because...");
|
||||
```
|
||||
|
||||
### Exception Testing (Prefer over `[ExpectedException]`)
|
||||
|
||||
```csharp
|
||||
// Assert.Throws - matches TException or derived types
|
||||
var ex = Assert.Throws<ArgumentException>(() => Method(null));
|
||||
Assert.AreEqual("Value cannot be null.", ex.Message);
|
||||
|
||||
// Assert.ThrowsExactly - matches exact type only
|
||||
var ex = Assert.ThrowsExactly<InvalidOperationException>(() => Method());
|
||||
|
||||
// Async versions
|
||||
var ex = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetAsync(url));
|
||||
var ex = await Assert.ThrowsExactlyAsync<InvalidOperationException>(async () => await Method());
|
||||
```
|
||||
|
||||
### Collection Assertions (Assert class)
|
||||
|
||||
```csharp
|
||||
Assert.Contains(expectedItem, collection);
|
||||
Assert.DoesNotContain(unexpectedItem, collection);
|
||||
Assert.ContainsSingle(collection); // exactly one element
|
||||
Assert.HasCount(5, collection);
|
||||
Assert.IsEmpty(collection);
|
||||
Assert.IsNotEmpty(collection);
|
||||
```
|
||||
|
||||
### String Assertions (Assert class)
|
||||
|
||||
```csharp
|
||||
Assert.Contains("expected", actualString);
|
||||
Assert.StartsWith("prefix", actualString);
|
||||
Assert.EndsWith("suffix", actualString);
|
||||
Assert.DoesNotStartWith("prefix", actualString);
|
||||
Assert.DoesNotEndWith("suffix", actualString);
|
||||
Assert.MatchesRegex(@"\d{3}-\d{4}", phoneNumber);
|
||||
Assert.DoesNotMatchRegex(@"\d+", textOnly);
|
||||
```
|
||||
|
||||
### Comparison Assertions
|
||||
|
||||
```csharp
|
||||
Assert.IsGreaterThan(lowerBound, actual);
|
||||
Assert.IsGreaterThanOrEqualTo(lowerBound, actual);
|
||||
Assert.IsLessThan(upperBound, actual);
|
||||
Assert.IsLessThanOrEqualTo(upperBound, actual);
|
||||
Assert.IsInRange(actual, low, high);
|
||||
Assert.IsPositive(number);
|
||||
Assert.IsNegative(number);
|
||||
```
|
||||
|
||||
### Type Assertions
|
||||
|
||||
```csharp
|
||||
// MSTest 3.x - uses out parameter
|
||||
Assert.IsInstanceOfType<MyClass>(obj, out var typed);
|
||||
typed.DoSomething();
|
||||
|
||||
// MSTest 4.x - returns typed result directly
|
||||
var typed = Assert.IsInstanceOfType<MyClass>(obj);
|
||||
typed.DoSomething();
|
||||
|
||||
Assert.IsNotInstanceOfType<WrongType>(obj);
|
||||
```
|
||||
|
||||
### Assert.That (MSTest 4.0+)
|
||||
|
||||
```csharp
|
||||
Assert.That(result.Count > 0); // Auto-captures expression in failure message
|
||||
```
|
||||
|
||||
### StringAssert Class
|
||||
|
||||
> **Note:** Prefer `Assert` class equivalents when available (e.g., `Assert.Contains("expected", actual)` over `StringAssert.Contains(actual, "expected")`).
|
||||
|
||||
```csharp
|
||||
StringAssert.Contains(actualString, "expected");
|
||||
StringAssert.StartsWith(actualString, "prefix");
|
||||
StringAssert.EndsWith(actualString, "suffix");
|
||||
StringAssert.Matches(actualString, new Regex(@"\d{3}-\d{4}"));
|
||||
StringAssert.DoesNotMatch(actualString, new Regex(@"\d+"));
|
||||
```
|
||||
|
||||
### CollectionAssert Class
|
||||
|
||||
> **Note:** Prefer `Assert` class equivalents when available (e.g., `Assert.Contains`).
|
||||
|
||||
```csharp
|
||||
// Containment
|
||||
CollectionAssert.Contains(collection, expectedItem);
|
||||
CollectionAssert.DoesNotContain(collection, unexpectedItem);
|
||||
|
||||
// Equality (same elements, same order)
|
||||
CollectionAssert.AreEqual(expectedCollection, actualCollection);
|
||||
CollectionAssert.AreNotEqual(unexpectedCollection, actualCollection);
|
||||
|
||||
// Equivalence (same elements, any order)
|
||||
CollectionAssert.AreEquivalent(expectedCollection, actualCollection);
|
||||
CollectionAssert.AreNotEquivalent(unexpectedCollection, actualCollection);
|
||||
|
||||
// Subset checks
|
||||
CollectionAssert.IsSubsetOf(subset, superset);
|
||||
CollectionAssert.IsNotSubsetOf(notSubset, collection);
|
||||
|
||||
// Element validation
|
||||
CollectionAssert.AllItemsAreInstancesOfType(collection, typeof(MyClass));
|
||||
CollectionAssert.AllItemsAreNotNull(collection);
|
||||
CollectionAssert.AllItemsAreUnique(collection);
|
||||
```
|
||||
|
||||
## Data-Driven Tests
|
||||
|
||||
- Use `[TestMethod]` combined with data source attributes
|
||||
- Use `[DataRow]` for inline test data
|
||||
- Use `[DynamicData]` for programmatically generated test data
|
||||
- Use `[TestProperty]` to add metadata to tests
|
||||
- Use meaningful parameter names in data-driven tests
|
||||
### DataRow
|
||||
|
||||
## Assertions
|
||||
```csharp
|
||||
[TestMethod]
|
||||
[DataRow(1, 2, 3)]
|
||||
[DataRow(0, 0, 0, DisplayName = "Zeros")]
|
||||
[DataRow(-1, 1, 0, IgnoreMessage = "Known issue #123")] // MSTest 3.8+
|
||||
public void Add_ReturnsSum(int a, int b, int expected)
|
||||
{
|
||||
Assert.AreEqual(expected, Calculator.Add(a, b));
|
||||
}
|
||||
```
|
||||
|
||||
- Use `Assert.AreEqual` for value equality
|
||||
- Use `Assert.AreSame` for reference equality
|
||||
- Use `Assert.IsTrue`/`Assert.IsFalse` for boolean conditions
|
||||
- Use `CollectionAssert` for collection comparisons
|
||||
- Use `StringAssert` for string-specific assertions
|
||||
- Use `Assert.Throws<T>` to test exceptions
|
||||
- Ensure assertions are simple in nature and have a message provided for clarity on failure
|
||||
### DynamicData
|
||||
|
||||
## Mocking and Isolation
|
||||
The data source can return any of the following types:
|
||||
|
||||
- Consider using Moq or NSubstitute alongside MSTest
|
||||
- Mock dependencies to isolate units under test
|
||||
- Use interfaces to facilitate mocking
|
||||
- Consider using a DI container for complex test setups
|
||||
- `IEnumerable<(T1, T2, ...)>` (ValueTuple) - **preferred**, provides type safety (MSTest 3.7+)
|
||||
- `IEnumerable<Tuple<T1, T2, ...>>` - provides type safety
|
||||
- `IEnumerable<TestDataRow>` - provides type safety plus control over test metadata (display name, categories)
|
||||
- `IEnumerable<object[]>` - **least preferred**, no type safety
|
||||
|
||||
> **Note:** When creating new test data methods, prefer `ValueTuple` or `TestDataRow` over `IEnumerable<object[]>`. The `object[]` approach provides no compile-time type checking and can lead to runtime errors from type mismatches.
|
||||
|
||||
```csharp
|
||||
[TestMethod]
|
||||
[DynamicData(nameof(TestData))]
|
||||
public void DynamicTest(int a, int b, int expected)
|
||||
{
|
||||
Assert.AreEqual(expected, Calculator.Add(a, b));
|
||||
}
|
||||
|
||||
// ValueTuple - preferred (MSTest 3.7+)
|
||||
public static IEnumerable<(int a, int b, int expected)> TestData =>
|
||||
[
|
||||
(1, 2, 3),
|
||||
(0, 0, 0),
|
||||
];
|
||||
|
||||
// TestDataRow - when you need custom display names or metadata
|
||||
public static IEnumerable<TestDataRow<(int a, int b, int expected)>> TestDataWithMetadata =>
|
||||
[
|
||||
new((1, 2, 3)) { DisplayName = "Positive numbers" },
|
||||
new((0, 0, 0)) { DisplayName = "Zeros" },
|
||||
new((-1, 1, 0)) { DisplayName = "Mixed signs", IgnoreMessage = "Known issue #123" },
|
||||
];
|
||||
|
||||
// IEnumerable<object[]> - avoid for new code (no type safety)
|
||||
public static IEnumerable<object[]> LegacyTestData =>
|
||||
[
|
||||
[1, 2, 3],
|
||||
[0, 0, 0],
|
||||
];
|
||||
```
|
||||
|
||||
## TestContext
|
||||
|
||||
The `TestContext` class provides test run information, cancellation support, and output methods.
|
||||
See [TestContext documentation](https://learn.microsoft.com/dotnet/core/testing/unit-testing-mstest-writing-tests-testcontext) for complete reference.
|
||||
|
||||
### Accessing TestContext
|
||||
|
||||
```csharp
|
||||
// Property (MSTest suppresses CS8618 - don't use nullable or = null!)
|
||||
public TestContext TestContext { get; set; }
|
||||
|
||||
// Constructor injection (MSTest 3.6+) - preferred for immutability
|
||||
[TestClass]
|
||||
public sealed class MyTests
|
||||
{
|
||||
private readonly TestContext _testContext;
|
||||
|
||||
public MyTests(TestContext testContext)
|
||||
{
|
||||
_testContext = testContext;
|
||||
}
|
||||
}
|
||||
|
||||
// Static methods receive it as parameter
|
||||
[ClassInitialize]
|
||||
public static void ClassInit(TestContext context) { }
|
||||
|
||||
// Optional for cleanup methods (MSTest 3.6+)
|
||||
[ClassCleanup]
|
||||
public static void ClassCleanup(TestContext context) { }
|
||||
|
||||
[AssemblyCleanup]
|
||||
public static void AssemblyCleanup(TestContext context) { }
|
||||
```
|
||||
|
||||
### Cancellation Token
|
||||
|
||||
Always use `TestContext.CancellationToken` for cooperative cancellation with `[Timeout]`:
|
||||
|
||||
```csharp
|
||||
[TestMethod]
|
||||
[Timeout(5000)]
|
||||
public async Task LongRunningTest()
|
||||
{
|
||||
await _httpClient.GetAsync(url, TestContext.CancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
### Test Run Properties
|
||||
|
||||
```csharp
|
||||
TestContext.TestName // Current test method name
|
||||
TestContext.TestDisplayName // Display name (3.7+)
|
||||
TestContext.CurrentTestOutcome // Pass/Fail/InProgress
|
||||
TestContext.TestData // Parameterized test data (3.7+, in TestInitialize/Cleanup)
|
||||
TestContext.TestException // Exception if test failed (3.7+, in TestCleanup)
|
||||
TestContext.DeploymentDirectory // Directory with deployment items
|
||||
```
|
||||
|
||||
### Output and Result Files
|
||||
|
||||
```csharp
|
||||
// Write to test output (useful for debugging)
|
||||
TestContext.WriteLine("Processing item {0}", itemId);
|
||||
|
||||
// Attach files to test results (logs, screenshots)
|
||||
TestContext.AddResultFile(screenshotPath);
|
||||
|
||||
// Store/retrieve data across test methods
|
||||
TestContext.Properties["SharedKey"] = computedValue;
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Retry for Flaky Tests (MSTest 3.9+)
|
||||
|
||||
```csharp
|
||||
[TestMethod]
|
||||
[Retry(3)]
|
||||
public void FlakyTest() { }
|
||||
```
|
||||
|
||||
### Conditional Execution (MSTest 3.10+)
|
||||
|
||||
Skip or run tests based on OS or CI environment:
|
||||
|
||||
```csharp
|
||||
// OS-specific tests
|
||||
[TestMethod]
|
||||
[OSCondition(OperatingSystems.Windows)]
|
||||
public void WindowsOnlyTest() { }
|
||||
|
||||
[TestMethod]
|
||||
[OSCondition(OperatingSystems.Linux | OperatingSystems.MacOS)]
|
||||
public void UnixOnlyTest() { }
|
||||
|
||||
[TestMethod]
|
||||
[OSCondition(ConditionMode.Exclude, OperatingSystems.Windows)]
|
||||
public void SkipOnWindowsTest() { }
|
||||
|
||||
// CI environment tests
|
||||
[TestMethod]
|
||||
[CICondition] // Runs only in CI (default: ConditionMode.Include)
|
||||
public void CIOnlyTest() { }
|
||||
|
||||
[TestMethod]
|
||||
[CICondition(ConditionMode.Exclude)] // Skips in CI, runs locally
|
||||
public void LocalOnlyTest() { }
|
||||
```
|
||||
|
||||
### Parallelization
|
||||
|
||||
```csharp
|
||||
// Assembly level
|
||||
[assembly: Parallelize(Workers = 4, Scope = ExecutionScope.MethodLevel)]
|
||||
|
||||
// Disable for specific class
|
||||
[TestClass]
|
||||
[DoNotParallelize]
|
||||
public sealed class SequentialTests { }
|
||||
```
|
||||
|
||||
### Work Item Traceability (MSTest 3.8+)
|
||||
|
||||
Link tests to work items for traceability in test reports:
|
||||
|
||||
```csharp
|
||||
// Azure DevOps work items
|
||||
[TestMethod]
|
||||
[WorkItem(12345)] // Links to work item #12345
|
||||
public void Feature_Scenario_ExpectedBehavior() { }
|
||||
|
||||
// Multiple work items
|
||||
[TestMethod]
|
||||
[WorkItem(12345)]
|
||||
[WorkItem(67890)]
|
||||
public void Feature_CoversMultipleRequirements() { }
|
||||
|
||||
// GitHub issues (MSTest 3.8+)
|
||||
[TestMethod]
|
||||
[GitHubWorkItem("https://github.com/owner/repo/issues/42")]
|
||||
public void BugFix_Issue42_IsResolved() { }
|
||||
```
|
||||
|
||||
Work item associations appear in test results and can be used for:
|
||||
- Tracing test coverage to requirements
|
||||
- Linking bug fixes to regression tests
|
||||
- Generating traceability reports in CI/CD pipelines
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
```csharp
|
||||
// ❌ Wrong argument order
|
||||
Assert.AreEqual(actual, expected);
|
||||
// ✅ Correct
|
||||
Assert.AreEqual(expected, actual);
|
||||
|
||||
// ❌ Using ExpectedException (obsolete)
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
// ✅ Use Assert.Throws
|
||||
Assert.Throws<ArgumentException>(() => Method());
|
||||
|
||||
// ❌ Using LINQ Single() - unclear exception
|
||||
var item = items.Single();
|
||||
// ✅ Use ContainsSingle - better failure message
|
||||
var item = Assert.ContainsSingle(items);
|
||||
|
||||
// ❌ Hard cast - unclear exception
|
||||
var handler = (MyHandler)result;
|
||||
// ✅ Type assertion - shows actual type on failure
|
||||
var handler = Assert.IsInstanceOfType<MyHandler>(result);
|
||||
|
||||
// ❌ Ignoring cancellation token
|
||||
await client.GetAsync(url, CancellationToken.None);
|
||||
// ✅ Flow test cancellation
|
||||
await client.GetAsync(url, TestContext.CancellationToken);
|
||||
|
||||
// ❌ Making TestContext nullable - leads to unnecessary null checks
|
||||
public TestContext? TestContext { get; set; }
|
||||
// ❌ Using null! - MSTest already suppresses CS8618 for this property
|
||||
public TestContext TestContext { get; set; } = null!;
|
||||
// ✅ Declare without nullable or initializer - MSTest handles the warning
|
||||
public TestContext TestContext { get; set; }
|
||||
```
|
||||
|
||||
## Test Organization
|
||||
|
||||
- Group tests by feature or component
|
||||
- Use test categories with `[TestCategory("Category")]`
|
||||
- Use test priorities with `[Priority(1)]` for critical tests
|
||||
- Use `[Owner("DeveloperName")]` to indicate ownership
|
||||
- Use `[TestCategory("Category")]` for filtering
|
||||
- Use `[TestProperty("Name", "Value")]` for custom metadata (e.g., `[TestProperty("Bug", "12345")]`)
|
||||
- Use `[Priority(1)]` for critical tests
|
||||
- Enable relevant MSTest analyzers (MSTEST0020 for constructor preference)
|
||||
|
||||
## Mocking and Isolation
|
||||
|
||||
- Use Moq or NSubstitute for mocking dependencies
|
||||
- Use interfaces to facilitate mocking
|
||||
- Mock dependencies to isolate units under test
|
||||
|
||||
613
skills/excalidraw-diagram-generator/SKILL.md
Normal file
613
skills/excalidraw-diagram-generator/SKILL.md
Normal file
@@ -0,0 +1,613 @@
|
||||
---
|
||||
name: excalidraw-diagram-generator
|
||||
description: 'Generate Excalidraw diagrams from natural language descriptions. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", or "generate an Excalidraw file". Supports flowcharts, relationship diagrams, mind maps, and system architecture diagrams. Outputs .excalidraw JSON files that can be opened directly in Excalidraw.'
|
||||
---
|
||||
|
||||
# Excalidraw Diagram Generator
|
||||
|
||||
A skill for generating Excalidraw-format diagrams from natural language descriptions. This skill helps create visual representations of processes, systems, relationships, and ideas without manual drawing.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when users request:
|
||||
|
||||
- "Create a diagram showing..."
|
||||
- "Make a flowchart for..."
|
||||
- "Visualize the process of..."
|
||||
- "Draw the system architecture of..."
|
||||
- "Generate a mind map about..."
|
||||
- "Create an Excalidraw file for..."
|
||||
- "Show the relationship between..."
|
||||
- "Diagram the workflow of..."
|
||||
|
||||
**Supported diagram types:**
|
||||
- 📊 **Flowcharts**: Sequential processes, workflows, decision trees
|
||||
- 🔗 **Relationship Diagrams**: Entity relationships, system components, dependencies
|
||||
- 🧠 **Mind Maps**: Concept hierarchies, brainstorming results, topic organization
|
||||
- 🏗️ **Architecture Diagrams**: System design, module interactions, data flow
|
||||
- 📈 **Data Flow Diagrams (DFD)**: Data flow visualization, data transformation processes
|
||||
- 🏊 **Business Flow (Swimlane)**: Cross-functional workflows, actor-based process flows
|
||||
- 📦 **Class Diagrams**: Object-oriented design, class structures and relationships
|
||||
- 🔄 **Sequence Diagrams**: Object interactions over time, message flows
|
||||
- 🗃️ **ER Diagrams**: Database entity relationships, data models
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Clear description of what should be visualized
|
||||
- Identification of key entities, steps, or concepts
|
||||
- Understanding of relationships or flow between elements
|
||||
|
||||
## Step-by-Step Workflow
|
||||
|
||||
### Step 1: Understand the Request
|
||||
|
||||
Analyze the user's description to determine:
|
||||
1. **Diagram type** (flowchart, relationship, mind map, architecture)
|
||||
2. **Key elements** (entities, steps, concepts)
|
||||
3. **Relationships** (flow, connections, hierarchy)
|
||||
4. **Complexity** (number of elements)
|
||||
|
||||
### Step 2: Choose the Appropriate Diagram Type
|
||||
|
||||
| User Intent | Diagram Type | Example Keywords |
|
||||
|-------------|--------------|------------------|
|
||||
| Process flow, steps, procedures | **Flowchart** | "workflow", "process", "steps", "procedure" |
|
||||
| Connections, dependencies, associations | **Relationship Diagram** | "relationship", "connections", "dependencies", "structure" |
|
||||
| Concept hierarchy, brainstorming | **Mind Map** | "mind map", "concepts", "ideas", "breakdown" |
|
||||
| System design, components | **Architecture Diagram** | "architecture", "system", "components", "modules" |
|
||||
| Data flow, transformation processes | **Data Flow Diagram (DFD)** | "data flow", "data processing", "data transformation" |
|
||||
| Cross-functional processes, actor responsibilities | **Business Flow (Swimlane)** | "business process", "swimlane", "actors", "responsibilities" |
|
||||
| Object-oriented design, class structures | **Class Diagram** | "class", "inheritance", "OOP", "object model" |
|
||||
| Interaction sequences, message flows | **Sequence Diagram** | "sequence", "interaction", "messages", "timeline" |
|
||||
| Database design, entity relationships | **ER Diagram** | "database", "entity", "relationship", "data model" |
|
||||
|
||||
### Step 3: Extract Structured Information
|
||||
|
||||
**For Flowcharts:**
|
||||
- List of sequential steps
|
||||
- Decision points (if any)
|
||||
- Start and end points
|
||||
|
||||
**For Relationship Diagrams:**
|
||||
- Entities/nodes (name + optional description)
|
||||
- Relationships between entities (from → to, with label)
|
||||
|
||||
**For Mind Maps:**
|
||||
- Central topic
|
||||
- Main branches (3-6 recommended)
|
||||
- Sub-topics for each branch (optional)
|
||||
|
||||
**For Data Flow Diagrams (DFD):**
|
||||
- Data sources and destinations (external entities)
|
||||
- Processes (data transformations)
|
||||
- Data stores (databases, files)
|
||||
- Data flows (arrows showing data movement from left-to-right or from top-left to bottom-right)
|
||||
- **Important**: Do not represent process order, only data flow
|
||||
|
||||
**For Business Flow (Swimlane):**
|
||||
- Actors/roles (departments, systems, people) - displayed as header columns
|
||||
- Process lanes (vertical lanes under each actor)
|
||||
- Process boxes (activities within each lane)
|
||||
- Flow arrows (connecting process boxes, including cross-lane handoffs)
|
||||
|
||||
**For Class Diagrams:**
|
||||
- Classes with names
|
||||
- Attributes with visibility (+, -, #)
|
||||
- Methods with visibility and parameters
|
||||
- Relationships: inheritance (solid line + white triangle), implementation (dashed line + white triangle), association (solid line), dependency (dashed line), aggregation (solid line + white diamond), composition (solid line + filled diamond)
|
||||
- Multiplicity notations (1, 0..1, 1..*, *)
|
||||
|
||||
**For Sequence Diagrams:**
|
||||
- Objects/actors (arranged horizontally at top)
|
||||
- Lifelines (vertical lines from each object)
|
||||
- Messages (horizontal arrows between lifelines)
|
||||
- Synchronous messages (solid arrow), asynchronous messages (dashed arrow)
|
||||
- Return values (dashed arrows)
|
||||
- Activation boxes (rectangles on lifelines during execution)
|
||||
- Time flows from top to bottom
|
||||
|
||||
**For ER Diagrams:**
|
||||
- Entities (rectangles with entity names)
|
||||
- Attributes (listed inside entities)
|
||||
- Primary keys (underlined or marked with PK)
|
||||
- Foreign keys (marked with FK)
|
||||
- Relationships (lines connecting entities)
|
||||
- Cardinality: 1:1 (one-to-one), 1:N (one-to-many), N:M (many-to-many)
|
||||
- Junction/associative entities for many-to-many relationships (dashed rectangles)
|
||||
|
||||
### Step 4: Generate the Excalidraw JSON
|
||||
|
||||
Create the `.excalidraw` file with appropriate elements:
|
||||
|
||||
**Available element types:**
|
||||
- `rectangle`: Boxes for entities, steps, concepts
|
||||
- `ellipse`: Alternative shapes for emphasis
|
||||
- `diamond`: Decision points
|
||||
- `arrow`: Directional connections
|
||||
- `text`: Labels and annotations
|
||||
|
||||
**Key properties to set:**
|
||||
- **Position**: `x`, `y` coordinates
|
||||
- **Size**: `width`, `height`
|
||||
- **Style**: `strokeColor`, `backgroundColor`, `fillStyle`
|
||||
- **Font**: `fontFamily: 5` (Excalifont - **required for all text elements**)
|
||||
- **Text**: Embedded text for labels
|
||||
- **Connections**: `points` array for arrows
|
||||
|
||||
**Important**: All text elements must use `fontFamily: 5` (Excalifont) for consistent visual appearance.
|
||||
|
||||
### Step 5: Format the Output
|
||||
|
||||
Structure the complete Excalidraw file:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
// Array of diagram elements
|
||||
],
|
||||
"appState": {
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"gridSize": 20
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: Save and Provide Instructions
|
||||
|
||||
1. Save as `<descriptive-name>.excalidraw`
|
||||
2. Inform user how to open:
|
||||
- Visit https://excalidraw.com
|
||||
- Click "Open" or drag-and-drop the file
|
||||
- Or use Excalidraw VS Code extension
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Element Count Guidelines
|
||||
|
||||
| Diagram Type | Recommended Count | Maximum |
|
||||
|--------------|-------------------|---------|
|
||||
| Flowchart steps | 3-10 | 15 |
|
||||
| Relationship entities | 3-8 | 12 |
|
||||
| Mind map branches | 4-6 | 8 |
|
||||
| Mind map sub-topics per branch | 2-4 | 6 |
|
||||
|
||||
### Layout Tips
|
||||
|
||||
1. **Start positions**: Center important elements, use consistent spacing
|
||||
2. **Spacing**:
|
||||
- Horizontal gap: 200-300px between elements
|
||||
- Vertical gap: 100-150px between rows
|
||||
3. **Colors**: Use consistent color scheme
|
||||
- Primary elements: Light blue (`#a5d8ff`)
|
||||
- Secondary elements: Light green (`#b2f2bb`)
|
||||
- Important/Central: Yellow (`#ffd43b`)
|
||||
- Alerts/Warnings: Light red (`#ffc9c9`)
|
||||
4. **Text sizing**: 16-24px for readability
|
||||
5. **Font**: Always use `fontFamily: 5` (Excalifont) for all text elements
|
||||
6. **Arrow style**: Use straight arrows for simple flows, curved for complex relationships
|
||||
|
||||
### Complexity Management
|
||||
|
||||
**If user request has too many elements:**
|
||||
- Suggest breaking into multiple diagrams
|
||||
- Focus on main elements first
|
||||
- Offer to create detailed sub-diagrams
|
||||
|
||||
**Example response:**
|
||||
```
|
||||
"Your request includes 15 components. For clarity, I recommend:
|
||||
1. High-level architecture diagram (6 main components)
|
||||
2. Detailed diagram for each subsystem
|
||||
|
||||
Would you like me to start with the high-level view?"
|
||||
```
|
||||
|
||||
## Example Prompts and Responses
|
||||
|
||||
### Example 1: Simple Flowchart
|
||||
|
||||
**User:** "Create a flowchart for user registration"
|
||||
|
||||
**Agent generates:**
|
||||
1. Extract steps: "Enter email" → "Verify email" → "Set password" → "Complete"
|
||||
2. Create flowchart with 4 rectangles + 3 arrows
|
||||
3. Save as `user-registration-flow.excalidraw`
|
||||
|
||||
### Example 2: Relationship Diagram
|
||||
|
||||
**User:** "Diagram the relationship between User, Post, and Comment entities"
|
||||
|
||||
**Agent generates:**
|
||||
1. Entities: User, Post, Comment
|
||||
2. Relationships: User → Post ("creates"), User → Comment ("writes"), Post → Comment ("contains")
|
||||
3. Save as `user-content-relationships.excalidraw`
|
||||
|
||||
### Example 3: Mind Map
|
||||
|
||||
**User:** "Mind map about machine learning concepts"
|
||||
|
||||
**Agent generates:**
|
||||
1. Center: "Machine Learning"
|
||||
2. Branches: Supervised Learning, Unsupervised Learning, Reinforcement Learning, Deep Learning
|
||||
3. Sub-topics under each branch
|
||||
4. Save as `machine-learning-mindmap.excalidraw`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Elements overlap | Increase spacing between coordinates |
|
||||
| Text doesn't fit in boxes | Increase box width or reduce font size |
|
||||
| Too many elements | Break into multiple diagrams |
|
||||
| Unclear layout | Use grid layout (rows/columns) or radial layout (mind maps) |
|
||||
| Colors inconsistent | Define color palette upfront based on element types |
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### Grid Layout (for Relationship Diagrams)
|
||||
```javascript
|
||||
const columns = Math.ceil(Math.sqrt(entityCount));
|
||||
const x = startX + (index % columns) * horizontalGap;
|
||||
const y = startY + Math.floor(index / columns) * verticalGap;
|
||||
```
|
||||
|
||||
### Radial Layout (for Mind Maps)
|
||||
```javascript
|
||||
const angle = (2 * Math.PI * index) / branchCount;
|
||||
const x = centerX + radius * Math.cos(angle);
|
||||
const y = centerY + radius * Math.sin(angle);
|
||||
```
|
||||
|
||||
### Auto-generated IDs
|
||||
Use timestamp + random string for unique IDs:
|
||||
```javascript
|
||||
const id = Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
Always provide:
|
||||
1. ✅ Complete `.excalidraw` JSON file
|
||||
2. 📊 Summary of what was created
|
||||
3. 📝 Element count
|
||||
4. 💡 Instructions for opening/editing
|
||||
|
||||
**Example summary:**
|
||||
```
|
||||
Created: user-workflow.excalidraw
|
||||
Type: Flowchart
|
||||
Elements: 7 rectangles, 6 arrows, 1 title text
|
||||
Total: 14 elements
|
||||
|
||||
To view:
|
||||
1. Visit https://excalidraw.com
|
||||
2. Drag and drop user-workflow.excalidraw
|
||||
3. Or use File → Open in Excalidraw VS Code extension
|
||||
```
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before delivering the diagram:
|
||||
- [ ] All elements have unique IDs
|
||||
- [ ] Coordinates prevent overlapping
|
||||
- [ ] Text is readable (font size 16+)
|
||||
- [ ] **All text elements use `fontFamily: 5` (Excalifont)**
|
||||
- [ ] Arrows connect logically
|
||||
- [ ] Colors follow consistent scheme
|
||||
- [ ] File is valid JSON
|
||||
- [ ] Element count is reasonable (<20 for clarity)
|
||||
|
||||
## Icon Libraries (Optional Enhancement)
|
||||
|
||||
For specialized diagrams (e.g., AWS/GCP/Azure architecture diagrams), you can use pre-made icon libraries from Excalidraw. This provides professional, standardized icons instead of basic shapes.
|
||||
|
||||
### When User Requests Icons
|
||||
|
||||
**If user asks for AWS/cloud architecture diagrams or mentions wanting to use specific icons:**
|
||||
|
||||
1. **Check if library exists**: Look for `libraries/<library-name>/reference.md`
|
||||
2. **If library exists**: Proceed to use icons (see AI Assistant Workflow below)
|
||||
3. **If library does NOT exist**: Respond with setup instructions:
|
||||
|
||||
```
|
||||
To use [AWS/GCP/Azure/etc.] architecture icons, please follow these steps:
|
||||
|
||||
1. Visit https://libraries.excalidraw.com/
|
||||
2. Search for "[AWS Architecture Icons/etc.]" and download the .excalidrawlib file
|
||||
3. Create directory: skills/excalidraw-diagram-generator/libraries/[icon-set-name]/
|
||||
4. Place the downloaded file in that directory
|
||||
5. Run the splitter script:
|
||||
python skills/excalidraw-diagram-generator/scripts/split-excalidraw-library.py skills/excalidraw-diagram-generator/libraries/[icon-set-name]/
|
||||
|
||||
This will split the library into individual icon files for efficient use.
|
||||
After setup is complete, I can create your diagram using the actual AWS/cloud icons.
|
||||
|
||||
Alternatively, I can create the diagram now using simple shapes (rectangles, ellipses)
|
||||
which you can later replace with icons manually in Excalidraw.
|
||||
```
|
||||
|
||||
### User Setup Instructions (Detailed)
|
||||
|
||||
**Step 1: Create Library Directory**
|
||||
```bash
|
||||
mkdir -p skills/excalidraw-diagram-generator/libraries/aws-architecture-icons
|
||||
```
|
||||
|
||||
**Step 2: Download Library**
|
||||
- Visit: https://libraries.excalidraw.com/
|
||||
- Search for your desired icon set (e.g., "AWS Architecture Icons")
|
||||
- Click download to get the `.excalidrawlib` file
|
||||
- Example categories (availability varies; confirm on the site):
|
||||
- Cloud service icons
|
||||
- UI/Material icons
|
||||
- Flowchart symbols
|
||||
|
||||
**Step 3: Place Library File**
|
||||
- Rename the downloaded file to match the directory name (e.g., `aws-architecture-icons.excalidrawlib`)
|
||||
- Move it to the directory created in Step 1
|
||||
|
||||
**Step 4: Run Splitter Script**
|
||||
```bash
|
||||
python skills/excalidraw-diagram-generator/scripts/split-excalidraw-library.py skills/excalidraw-diagram-generator/libraries/aws-architecture-icons/
|
||||
```
|
||||
|
||||
**Step 5: Verify Setup**
|
||||
After running the script, verify the following structure exists:
|
||||
```
|
||||
skills/excalidraw-diagram-generator/libraries/aws-architecture-icons/
|
||||
aws-architecture-icons.excalidrawlib (original)
|
||||
reference.md (generated - icon lookup table)
|
||||
icons/ (generated - individual icon files)
|
||||
API-Gateway.json
|
||||
CloudFront.json
|
||||
EC2.json
|
||||
Lambda.json
|
||||
RDS.json
|
||||
S3.json
|
||||
...
|
||||
```
|
||||
|
||||
### AI Assistant Workflow
|
||||
|
||||
**When icon libraries are available in `libraries/`:**
|
||||
|
||||
**RECOMMENDED APPROACH: Use Python Scripts (Efficient & Reliable)**
|
||||
|
||||
The repository includes Python scripts that handle icon integration automatically:
|
||||
|
||||
1. **Create base diagram structure**:
|
||||
- Create `.excalidraw` file with basic layout (title, boxes, regions)
|
||||
- This establishes the canvas and overall structure
|
||||
|
||||
2. **Add icons using Python script**:
|
||||
```bash
|
||||
python skills/excalidraw-diagram-generator/scripts/add-icon-to-diagram.py \
|
||||
<diagram-path> <icon-name> <x> <y> [--label "Text"] [--library-path PATH]
|
||||
```
|
||||
- Edit via `.excalidraw.edit` is enabled by default to avoid overwrite issues; pass `--no-use-edit-suffix` to disable.
|
||||
|
||||
**Examples**:
|
||||
```bash
|
||||
# Add EC2 icon at position (400, 300) with label
|
||||
python scripts/add-icon-to-diagram.py diagram.excalidraw EC2 400 300 --label "Web Server"
|
||||
|
||||
# Add VPC icon at position (200, 150)
|
||||
python scripts/add-icon-to-diagram.py diagram.excalidraw VPC 200 150
|
||||
|
||||
# Add icon from different library
|
||||
python scripts/add-icon-to-diagram.py diagram.excalidraw Compute-Engine 500 200 \
|
||||
--library-path libraries/gcp-icons --label "API Server"
|
||||
```
|
||||
|
||||
3. **Add connecting arrows**:
|
||||
```bash
|
||||
python skills/excalidraw-diagram-generator/scripts/add-arrow.py \
|
||||
<diagram-path> <from-x> <from-y> <to-x> <to-y> [--label "Text"] [--style solid|dashed|dotted] [--color HEX]
|
||||
```
|
||||
- Edit via `.excalidraw.edit` is enabled by default to avoid overwrite issues; pass `--no-use-edit-suffix` to disable.
|
||||
|
||||
**Examples**:
|
||||
```bash
|
||||
# Simple arrow from (300, 250) to (500, 300)
|
||||
python scripts/add-arrow.py diagram.excalidraw 300 250 500 300
|
||||
|
||||
# Arrow with label
|
||||
python scripts/add-arrow.py diagram.excalidraw 300 250 500 300 --label "HTTPS"
|
||||
|
||||
# Dashed arrow with custom color
|
||||
python scripts/add-arrow.py diagram.excalidraw 400 350 600 400 --style dashed --color "#7950f2"
|
||||
```
|
||||
|
||||
4. **Workflow summary**:
|
||||
```bash
|
||||
# Step 1: Create base diagram with title and structure
|
||||
# (Create .excalidraw file with initial elements)
|
||||
|
||||
# Step 2: Add icons with labels
|
||||
python scripts/add-icon-to-diagram.py my-diagram.excalidraw "Internet-gateway" 200 150 --label "Internet Gateway"
|
||||
python scripts/add-icon-to-diagram.py my-diagram.excalidraw VPC 250 250
|
||||
python scripts/add-icon-to-diagram.py my-diagram.excalidraw ELB 350 300 --label "Load Balancer"
|
||||
python scripts/add-icon-to-diagram.py my-diagram.excalidraw EC2 450 350 --label "EC2 Instance"
|
||||
python scripts/add-icon-to-diagram.py my-diagram.excalidraw RDS 550 400 --label "Database"
|
||||
|
||||
# Step 3: Add connecting arrows
|
||||
python scripts/add-arrow.py my-diagram.excalidraw 250 200 300 250 # Internet → VPC
|
||||
python scripts/add-arrow.py my-diagram.excalidraw 300 300 400 300 # VPC → ELB
|
||||
python scripts/add-arrow.py my-diagram.excalidraw 400 330 500 350 # ELB → EC2
|
||||
python scripts/add-arrow.py my-diagram.excalidraw 500 380 600 400 # EC2 → RDS
|
||||
```
|
||||
|
||||
**Benefits of Python Script Approach**:
|
||||
- ✅ **No token consumption**: Icon JSON data (200-1000 lines each) never enters AI context
|
||||
- ✅ **Accurate transformations**: Coordinate calculations handled deterministically
|
||||
- ✅ **ID management**: Automatic UUID generation prevents conflicts
|
||||
- ✅ **Reliable**: No risk of coordinate miscalculation or ID collision
|
||||
- ✅ **Fast**: Direct file manipulation, no parsing overhead
|
||||
- ✅ **Reusable**: Works with any Excalidraw library you provide
|
||||
|
||||
**ALTERNATIVE: Manual Icon Integration (Not Recommended)**
|
||||
|
||||
Only use this if Python scripts are unavailable:
|
||||
|
||||
1. **Check for libraries**:
|
||||
```
|
||||
List directory: skills/excalidraw-diagram-generator/libraries/
|
||||
Look for subdirectories containing reference.md files
|
||||
```
|
||||
|
||||
2. **Read reference.md**:
|
||||
```
|
||||
Open: libraries/<library-name>/reference.md
|
||||
This is lightweight (typically <300 lines) and lists all available icons
|
||||
```
|
||||
|
||||
3. **Find relevant icons**:
|
||||
```
|
||||
Search the reference.md table for icon names matching diagram needs
|
||||
Example: For AWS diagram with EC2, S3, Lambda → Find "EC2", "S3", "Lambda" in table
|
||||
```
|
||||
|
||||
4. **Load specific icon data** (WARNING: Large files):
|
||||
```
|
||||
Read ONLY the needed icon files:
|
||||
- libraries/aws-architecture-icons/icons/EC2.json (200-300 lines)
|
||||
- libraries/aws-architecture-icons/icons/S3.json (200-300 lines)
|
||||
- libraries/aws-architecture-icons/icons/Lambda.json (200-300 lines)
|
||||
Note: Each icon file is 200-1000 lines - this consumes significant tokens
|
||||
```
|
||||
|
||||
5. **Extract and transform elements**:
|
||||
```
|
||||
Each icon JSON contains an "elements" array
|
||||
Calculate bounding box (min_x, min_y, max_x, max_y)
|
||||
Apply offset to all x/y coordinates
|
||||
Generate new unique IDs for all elements
|
||||
Update groupIds references
|
||||
Copy transformed elements into your diagram
|
||||
```
|
||||
|
||||
6. **Position icons and add connections**:
|
||||
```
|
||||
Adjust x/y coordinates to position icons correctly in the diagram
|
||||
Update IDs to ensure uniqueness across diagram
|
||||
Add connecting arrows and labels as needed
|
||||
```
|
||||
|
||||
**Manual Integration Challenges**:
|
||||
- ⚠️ High token consumption (200-1000 lines per icon × number of icons)
|
||||
- ⚠️ Complex coordinate transformation calculations
|
||||
- ⚠️ Risk of ID collision if not handled carefully
|
||||
- ⚠️ Time-consuming for diagrams with many icons
|
||||
|
||||
### Example: Creating AWS Diagram with Icons
|
||||
|
||||
**Request**: "Create an AWS architecture diagram with Internet Gateway, VPC, ELB, EC2, and RDS"
|
||||
|
||||
**Recommended Workflow (using Python scripts)**:
|
||||
**Request**: "Create an AWS architecture diagram with Internet Gateway, VPC, ELB, EC2, and RDS"
|
||||
|
||||
**Recommended Workflow (using Python scripts)**:
|
||||
|
||||
```bash
|
||||
# Step 1: Create base diagram file with title
|
||||
# Create my-aws-diagram.excalidraw with basic structure (title, etc.)
|
||||
|
||||
# Step 2: Check icon availability
|
||||
# Read: libraries/aws-architecture-icons/reference.md
|
||||
# Confirm icons exist: Internet-gateway, VPC, ELB, EC2, RDS
|
||||
|
||||
# Step 3: Add icons with Python script
|
||||
python scripts/add-icon-to-diagram.py my-aws-diagram.excalidraw "Internet-gateway" 150 100 --label "Internet Gateway"
|
||||
python scripts/add-icon-to-diagram.py my-aws-diagram.excalidraw VPC 200 200
|
||||
python scripts/add-icon-to-diagram.py my-aws-diagram.excalidraw ELB 350 250 --label "Load Balancer"
|
||||
python scripts/add-icon-to-diagram.py my-aws-diagram.excalidraw EC2 500 300 --label "Web Server"
|
||||
python scripts/add-icon-to-diagram.py my-aws-diagram.excalidraw RDS 650 350 --label "Database"
|
||||
|
||||
# Step 4: Add connecting arrows
|
||||
python scripts/add-arrow.py my-aws-diagram.excalidraw 200 150 250 200 # Internet → VPC
|
||||
python scripts/add-arrow.py my-aws-diagram.excalidraw 265 230 350 250 # VPC → ELB
|
||||
python scripts/add-arrow.py my-aws-diagram.excalidraw 415 280 500 300 # ELB → EC2
|
||||
python scripts/add-arrow.py my-aws-diagram.excalidraw 565 330 650 350 --label "SQL" --style dashed
|
||||
|
||||
# Result: Complete diagram with professional AWS icons, labels, and connections
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- No manual coordinate calculation
|
||||
- No token consumption for icon data
|
||||
- Deterministic, reliable results
|
||||
- Easy to iterate and adjust positions
|
||||
|
||||
**Alternative Workflow (manual, if scripts unavailable)**:
|
||||
1. Check: `libraries/aws-architecture-icons/reference.md` exists → Yes
|
||||
2. Read reference.md → Find entries for Internet-gateway, VPC, ELB, EC2, RDS
|
||||
3. Load:
|
||||
- `icons/Internet-gateway.json` (298 lines)
|
||||
- `icons/VPC.json` (550 lines)
|
||||
- `icons/ELB.json` (363 lines)
|
||||
- `icons/EC2.json` (231 lines)
|
||||
- `icons/RDS.json` (similar size)
|
||||
**Total: ~2000+ lines of JSON to process**
|
||||
4. Extract elements from each JSON
|
||||
5. Calculate bounding boxes and offsets for each icon
|
||||
6. Transform all coordinates (x, y) for positioning
|
||||
7. Generate unique IDs for all elements
|
||||
8. Add arrows showing data flow
|
||||
9. Add text labels
|
||||
10. Generate final `.excalidraw` file
|
||||
|
||||
**Challenges with manual approach**:
|
||||
- High token consumption (~2000-5000 lines)
|
||||
- Complex coordinate math
|
||||
- Risk of ID conflicts
|
||||
|
||||
### Supported Icon Libraries (Examples — verify availability)
|
||||
|
||||
- This workflow works with any valid `.excalidrawlib` file you provide.
|
||||
- Examples of library categories you may find on https://libraries.excalidraw.com/:
|
||||
- Cloud service icons
|
||||
- Kubernetes / infrastructure icons
|
||||
- UI / Material icons
|
||||
- Flowchart / diagram symbols
|
||||
- Network diagram icons
|
||||
- Availability and naming can change; verify exact library names on the site before use.
|
||||
|
||||
### Fallback: No Icons Available
|
||||
|
||||
**If no icon libraries are set up:**
|
||||
- Create diagrams using basic shapes (rectangles, ellipses, arrows)
|
||||
- Use color coding and text labels to distinguish components
|
||||
- Inform user they can add icons later or set up libraries for future diagrams
|
||||
- The diagram will still be functional and clear, just less visually polished
|
||||
|
||||
## References
|
||||
|
||||
See bundled references for:
|
||||
- `references/excalidraw-schema.md` - Complete Excalidraw JSON schema
|
||||
- `references/element-types.md` - Detailed element type specifications
|
||||
- `templates/flowchart-template.json` - Basic flowchart starter
|
||||
- `templates/relationship-template.json` - Relationship diagram starter
|
||||
- `templates/mindmap-template.json` - Mind map starter
|
||||
- `scripts/split-excalidraw-library.py` - Tool to split `.excalidrawlib` files
|
||||
- `scripts/README.md` - Documentation for library tools
|
||||
- `scripts/.gitignore` - Prevents local Python artifacts from being committed
|
||||
|
||||
## Limitations
|
||||
|
||||
- Complex curves are simplified to straight/basic curved lines
|
||||
- Hand-drawn roughness is set to default (1)
|
||||
- No embedded images support in auto-generation
|
||||
- Maximum recommended elements: 20 per diagram
|
||||
- No automatic collision detection (use spacing guidelines)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
- Auto-layout optimization algorithms
|
||||
- Import from Mermaid/PlantUML syntax
|
||||
- Template library expansion
|
||||
- Interactive editing after generation
|
||||
497
skills/excalidraw-diagram-generator/references/element-types.md
Normal file
497
skills/excalidraw-diagram-generator/references/element-types.md
Normal file
@@ -0,0 +1,497 @@
|
||||
# Excalidraw Element Types Guide
|
||||
|
||||
Detailed specifications for each Excalidraw element type with visual examples and use cases.
|
||||
|
||||
## Element Type Overview
|
||||
|
||||
| Type | Visual | Primary Use | Text Support |
|
||||
|------|--------|-------------|--------------|
|
||||
| `rectangle` | □ | Boxes, containers, process steps | ✅ Yes |
|
||||
| `ellipse` | ○ | Emphasis, terminals, states | ✅ Yes |
|
||||
| `diamond` | ◇ | Decision points, choices | ✅ Yes |
|
||||
| `arrow` | → | Directional flow, relationships | ❌ No (use separate text) |
|
||||
| `line` | — | Connections, dividers | ❌ No |
|
||||
| `text` | A | Labels, annotations, titles | ✅ (Its purpose) |
|
||||
|
||||
---
|
||||
|
||||
## Rectangle
|
||||
|
||||
**Best for:** Process steps, entities, data stores, components
|
||||
|
||||
### Properties
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "rectangle",
|
||||
roundness: { type: 3 }, // Rounded corners
|
||||
text: "Step Name", // Optional embedded text
|
||||
fontSize: 20,
|
||||
textAlign: "center",
|
||||
verticalAlign: "middle"
|
||||
}
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
| Scenario | Configuration |
|
||||
|----------|---------------|
|
||||
| **Process step** | Green background (`#b2f2bb`), centered text |
|
||||
| **Entity/Object** | Blue background (`#a5d8ff`), medium size |
|
||||
| **System component** | Light color, descriptive text |
|
||||
| **Data store** | Gray/white, database-like label |
|
||||
|
||||
### Size Guidelines
|
||||
|
||||
| Content | Width | Height |
|
||||
|---------|-------|--------|
|
||||
| Single word | 120-150px | 60-80px |
|
||||
| Short phrase (2-4 words) | 180-220px | 80-100px |
|
||||
| Sentence | 250-300px | 100-120px |
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 200,
|
||||
"height": 80,
|
||||
"backgroundColor": "#b2f2bb",
|
||||
"text": "Validate Input",
|
||||
"fontSize": 20,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"roundness": { "type": 3 }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ellipse
|
||||
|
||||
**Best for:** Start/end points, states, emphasis circles
|
||||
|
||||
### Properties
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "ellipse",
|
||||
text: "Start",
|
||||
fontSize: 18,
|
||||
textAlign: "center",
|
||||
verticalAlign: "middle"
|
||||
}
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
| Scenario | Configuration |
|
||||
|----------|---------------|
|
||||
| **Flow start** | Light green, "Start" text |
|
||||
| **Flow end** | Light red, "End" text |
|
||||
| **State** | Soft color, state name |
|
||||
| **Highlight** | Bright color, emphasis text |
|
||||
|
||||
### Size Guidelines
|
||||
|
||||
For circular shapes, use `width === height`:
|
||||
|
||||
| Content | Diameter |
|
||||
|---------|----------|
|
||||
| Icon/Symbol | 60-80px |
|
||||
| Short text | 100-120px |
|
||||
| Longer text | 150-180px |
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ellipse",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 120,
|
||||
"height": 120,
|
||||
"backgroundColor": "#d0f0c0",
|
||||
"text": "Start",
|
||||
"fontSize": 18,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diamond
|
||||
|
||||
**Best for:** Decision points, conditional branches
|
||||
|
||||
### Properties
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "diamond",
|
||||
text: "Valid?",
|
||||
fontSize: 18,
|
||||
textAlign: "center",
|
||||
verticalAlign": "middle"
|
||||
}
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
| Scenario | Text Example |
|
||||
|----------|--------------|
|
||||
| **Yes/No decision** | "Is Valid?", "Exists?" |
|
||||
| **Multiple choice** | "Type?", "Status?" |
|
||||
| **Conditional** | "Score > 50?" |
|
||||
|
||||
### Size Guidelines
|
||||
|
||||
Diamonds need more space than rectangles for the same text:
|
||||
|
||||
| Content | Width | Height |
|
||||
|---------|-------|--------|
|
||||
| Yes/No | 120-140px | 120-140px |
|
||||
| Short question | 160-180px | 160-180px |
|
||||
| Longer question | 200-220px | 200-220px |
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "diamond",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 150,
|
||||
"height": 150,
|
||||
"backgroundColor": "#ffe4a3",
|
||||
"text": "Valid?",
|
||||
"fontSize": 18,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Arrow
|
||||
|
||||
**Best for:** Flow direction, relationships, dependencies
|
||||
|
||||
### Properties
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "arrow",
|
||||
points: [[0, 0], [endX, endY]], // Relative coordinates
|
||||
roundness: { type: 2 }, // Curved
|
||||
startBinding: null, // Or { elementId, focus, gap }
|
||||
endBinding: null
|
||||
}
|
||||
```
|
||||
|
||||
### Arrow Directions
|
||||
|
||||
#### Horizontal (Left to Right)
|
||||
|
||||
```json
|
||||
{
|
||||
"x": 100,
|
||||
"y": 150,
|
||||
"width": 200,
|
||||
"height": 0,
|
||||
"points": [[0, 0], [200, 0]]
|
||||
}
|
||||
```
|
||||
|
||||
#### Vertical (Top to Bottom)
|
||||
|
||||
```json
|
||||
{
|
||||
"x": 200,
|
||||
"y": 100,
|
||||
"width": 0,
|
||||
"height": 150,
|
||||
"points": [[0, 0], [0, 150]]
|
||||
}
|
||||
```
|
||||
|
||||
#### Diagonal
|
||||
|
||||
```json
|
||||
{
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 200,
|
||||
"height": 150,
|
||||
"points": [[0, 0], [200, 150]]
|
||||
}
|
||||
```
|
||||
|
||||
### Arrow Styles
|
||||
|
||||
| Style | `strokeStyle` | `strokeWidth` | Use Case |
|
||||
|-------|---------------|---------------|----------|
|
||||
| **Normal flow** | `"solid"` | 2 | Standard connections |
|
||||
| **Optional/Weak** | `"dashed"` | 2 | Optional paths |
|
||||
| **Important** | `"solid"` | 3-4 | Emphasized flow |
|
||||
| **Dotted** | `"dotted"` | 2 | Indirect relationships |
|
||||
|
||||
### Adding Arrow Labels
|
||||
|
||||
Use separate text elements positioned near arrow midpoint:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "arrow",
|
||||
"id": "arrow1",
|
||||
"x": 100,
|
||||
"y": 150,
|
||||
"points": [[0, 0], [200, 0]]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 180, // Near midpoint
|
||||
"y": 130, // Above arrow
|
||||
"text": "sends",
|
||||
"fontSize": 14
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Line
|
||||
|
||||
**Best for:** Non-directional connections, dividers, borders
|
||||
|
||||
### Properties
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "line",
|
||||
points: [[0, 0], [x2, y2], [x3, y3], ...],
|
||||
roundness: null // Or { type: 2 } for curved
|
||||
}
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
| Scenario | Configuration |
|
||||
|----------|---------------|
|
||||
| **Divider** | Horizontal, thin stroke |
|
||||
| **Border** | Closed path (polygon) |
|
||||
| **Connection** | Multi-point path |
|
||||
| **Underline** | Short horizontal line |
|
||||
|
||||
### Multi-Point Line Example
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "line",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[100, 50],
|
||||
[200, 0]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Text
|
||||
|
||||
**Best for:** Labels, titles, annotations, standalone text
|
||||
|
||||
### Properties
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "text",
|
||||
text: "Label text",
|
||||
fontSize: 20,
|
||||
fontFamily: 1, // 1=Virgil, 2=Helvetica, 3=Cascadia
|
||||
textAlign: "left",
|
||||
verticalAlign: "top"
|
||||
}
|
||||
```
|
||||
|
||||
### Font Sizes by Purpose
|
||||
|
||||
| Purpose | Font Size |
|
||||
|---------|-----------|
|
||||
| **Main title** | 28-36 |
|
||||
| **Section header** | 24-28 |
|
||||
| **Element label** | 18-22 |
|
||||
| **Annotation** | 14-16 |
|
||||
| **Small note** | 12-14 |
|
||||
|
||||
### Width/Height Calculation
|
||||
|
||||
```javascript
|
||||
// Approximate width
|
||||
const width = text.length * fontSize * 0.6;
|
||||
|
||||
// Approximate height (single line)
|
||||
const height = fontSize * 1.2;
|
||||
|
||||
// Multi-line
|
||||
const lines = text.split('\n').length;
|
||||
const height = fontSize * 1.2 * lines;
|
||||
```
|
||||
|
||||
### Text Positioning
|
||||
|
||||
| Position | textAlign | verticalAlign | Use Case |
|
||||
|----------|-----------|---------------|----------|
|
||||
| **Top-left** | `"left"` | `"top"` | Default labels |
|
||||
| **Centered** | `"center"` | `"middle"` | Titles |
|
||||
| **Bottom-right** | `"right"` | `"bottom"` | Footnotes |
|
||||
|
||||
### Example: Title
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "text",
|
||||
"x": 100,
|
||||
"y": 50,
|
||||
"width": 400,
|
||||
"height": 40,
|
||||
"text": "System Architecture",
|
||||
"fontSize": 32,
|
||||
"fontFamily": 2,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Annotation
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "text",
|
||||
"x": 150,
|
||||
"y": 200,
|
||||
"width": 100,
|
||||
"height": 20,
|
||||
"text": "User input",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Combining Elements
|
||||
|
||||
### Pattern: Labeled Box
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "rectangle",
|
||||
"id": "box1",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 200,
|
||||
"height": 100,
|
||||
"text": "Component",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Pattern: Connected Boxes
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "rectangle",
|
||||
"id": "box1",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 150,
|
||||
"height": 80,
|
||||
"text": "Step 1"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"id": "arrow1",
|
||||
"x": 250,
|
||||
"y": 140,
|
||||
"points": [[0, 0], [100, 0]]
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"id": "box2",
|
||||
"x": 350,
|
||||
"y": 100,
|
||||
"width": 150,
|
||||
"height": 80,
|
||||
"text": "Step 2"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Pattern: Decision Tree
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "diamond",
|
||||
"id": "decision",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 140,
|
||||
"height": 140,
|
||||
"text": "Valid?"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"id": "yes-arrow",
|
||||
"x": 240,
|
||||
"y": 170,
|
||||
"points": [[0, 0], [60, 0]]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "yes-label",
|
||||
"x": 250,
|
||||
"y": 150,
|
||||
"text": "Yes",
|
||||
"fontSize": 14
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"id": "yes-box",
|
||||
"x": 300,
|
||||
"y": 140,
|
||||
"width": 120,
|
||||
"height": 60,
|
||||
"text": "Process"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| When you need... | Use this element |
|
||||
|------------------|------------------|
|
||||
| Process box | `rectangle` with text |
|
||||
| Decision point | `diamond` with question |
|
||||
| Flow direction | `arrow` |
|
||||
| Start/End | `ellipse` |
|
||||
| Title/Header | `text` (large font) |
|
||||
| Annotation | `text` (small font) |
|
||||
| Non-directional link | `line` |
|
||||
| Divider | `line` (horizontal) |
|
||||
@@ -0,0 +1,350 @@
|
||||
# Excalidraw JSON Schema Reference
|
||||
|
||||
This document describes the structure of Excalidraw `.excalidraw` files for diagram generation.
|
||||
|
||||
## Top-Level Structure
|
||||
|
||||
```typescript
|
||||
interface ExcalidrawFile {
|
||||
type: "excalidraw";
|
||||
version: number; // Always 2
|
||||
source: string; // "https://excalidraw.com"
|
||||
elements: ExcalidrawElement[];
|
||||
appState: AppState;
|
||||
files: Record<string, any>; // Usually empty {}
|
||||
}
|
||||
```
|
||||
|
||||
## AppState
|
||||
|
||||
```typescript
|
||||
interface AppState {
|
||||
viewBackgroundColor: string; // Hex color, e.g., "#ffffff"
|
||||
gridSize: number; // Typically 20
|
||||
}
|
||||
```
|
||||
|
||||
## ExcalidrawElement Base Properties
|
||||
|
||||
All elements share these common properties:
|
||||
|
||||
```typescript
|
||||
interface BaseElement {
|
||||
id: string; // Unique identifier
|
||||
type: ElementType; // See Element Types below
|
||||
x: number; // X coordinate (pixels from top-left)
|
||||
y: number; // Y coordinate (pixels from top-left)
|
||||
width: number; // Width in pixels
|
||||
height: number; // Height in pixels
|
||||
angle: number; // Rotation angle in radians (usually 0)
|
||||
strokeColor: string; // Hex color, e.g., "#1e1e1e"
|
||||
backgroundColor: string; // Hex color or "transparent"
|
||||
fillStyle: "solid" | "hachure" | "cross-hatch";
|
||||
strokeWidth: number; // 1-4 typically
|
||||
strokeStyle: "solid" | "dashed" | "dotted";
|
||||
roughness: number; // 0-2, controls hand-drawn effect (1 = default)
|
||||
opacity: number; // 0-100
|
||||
groupIds: string[]; // IDs of groups this element belongs to
|
||||
frameId: null; // Usually null
|
||||
index: string; // Stacking order identifier
|
||||
roundness: Roundness | null;
|
||||
seed: number; // Random seed for deterministic rendering
|
||||
version: number; // Element version (increment on edit)
|
||||
versionNonce: number; // Random number changed on edit
|
||||
isDeleted: boolean; // Should be false
|
||||
boundElements: any; // Usually null
|
||||
updated: number; // Timestamp in milliseconds
|
||||
link: null; // External link (usually null)
|
||||
locked: boolean; // Whether element is locked
|
||||
}
|
||||
```
|
||||
|
||||
## Element Types
|
||||
|
||||
### Rectangle
|
||||
|
||||
```typescript
|
||||
interface RectangleElement extends BaseElement {
|
||||
type: "rectangle";
|
||||
roundness: { type: 3 }; // 3 = rounded corners
|
||||
text?: string; // Optional text inside
|
||||
fontSize?: number; // Font size (16-32 typical)
|
||||
fontFamily?: number; // 1 = Virgil, 2 = Helvetica, 3 = Cascadia
|
||||
textAlign?: "left" | "center" | "right";
|
||||
verticalAlign?: "top" | "middle" | "bottom";
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"id": "rect1",
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 200,
|
||||
"height": 100,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#a5d8ff",
|
||||
"text": "My Box",
|
||||
"fontSize": 20,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"roundness": { "type": 3 }
|
||||
}
|
||||
```
|
||||
|
||||
### Ellipse
|
||||
|
||||
```typescript
|
||||
interface EllipseElement extends BaseElement {
|
||||
type: "ellipse";
|
||||
text?: string;
|
||||
fontSize?: number;
|
||||
fontFamily?: number;
|
||||
textAlign?: "left" | "center" | "right";
|
||||
verticalAlign?: "top" | "middle" | "bottom";
|
||||
}
|
||||
```
|
||||
|
||||
### Diamond
|
||||
|
||||
```typescript
|
||||
interface DiamondElement extends BaseElement {
|
||||
type: "diamond";
|
||||
text?: string;
|
||||
fontSize?: number;
|
||||
fontFamily?: number;
|
||||
textAlign?: "left" | "center" | "right";
|
||||
verticalAlign?: "top" | "middle" | "bottom";
|
||||
}
|
||||
```
|
||||
|
||||
### Arrow
|
||||
|
||||
```typescript
|
||||
interface ArrowElement extends BaseElement {
|
||||
type: "arrow";
|
||||
points: [number, number][]; // Array of [x, y] coordinates relative to element
|
||||
startBinding: Binding | null;
|
||||
endBinding: Binding | null;
|
||||
roundness: { type: 2 }; // 2 = curved arrow
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"id": "arrow1",
|
||||
"type": "arrow",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 200,
|
||||
"height": 0,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[200, 0]
|
||||
],
|
||||
"roundness": { "type": 2 },
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
}
|
||||
```
|
||||
|
||||
**Points explanation:**
|
||||
- First point `[0, 0]` is relative to `(x, y)`
|
||||
- Subsequent points are relative to the first point
|
||||
- For straight horizontal arrow: `[[0, 0], [width, 0]]`
|
||||
- For straight vertical arrow: `[[0, 0], [0, height]]`
|
||||
|
||||
### Line
|
||||
|
||||
```typescript
|
||||
interface LineElement extends BaseElement {
|
||||
type: "line";
|
||||
points: [number, number][];
|
||||
startBinding: Binding | null;
|
||||
endBinding: Binding | null;
|
||||
roundness: { type: 2 } | null;
|
||||
}
|
||||
```
|
||||
|
||||
### Text
|
||||
|
||||
```typescript
|
||||
interface TextElement extends BaseElement {
|
||||
type: "text";
|
||||
text: string;
|
||||
fontSize: number;
|
||||
fontFamily: number; // 1-3
|
||||
textAlign: "left" | "center" | "right";
|
||||
verticalAlign: "top" | "middle" | "bottom";
|
||||
roundness: null; // Text has no roundness
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"id": "text1",
|
||||
"type": "text",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 150,
|
||||
"height": 25,
|
||||
"text": "Hello World",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"roundness": null
|
||||
}
|
||||
```
|
||||
|
||||
**Width/Height calculation:**
|
||||
- Width ≈ `text.length * fontSize * 0.6`
|
||||
- Height ≈ `fontSize * 1.2 * numberOfLines`
|
||||
|
||||
## Bindings
|
||||
|
||||
Bindings connect arrows to shapes:
|
||||
|
||||
```typescript
|
||||
interface Binding {
|
||||
elementId: string; // ID of bound element
|
||||
focus: number; // -1 to 1, position along edge
|
||||
gap: number; // Distance from element edge
|
||||
}
|
||||
```
|
||||
|
||||
## Common Colors
|
||||
|
||||
| Color Name | Hex Code | Use Case |
|
||||
|------------|----------|----------|
|
||||
| Black | `#1e1e1e` | Default stroke |
|
||||
| Light Blue | `#a5d8ff` | Primary entities |
|
||||
| Light Green | `#b2f2bb` | Process steps |
|
||||
| Yellow | `#ffd43b` | Important/Central |
|
||||
| Light Red | `#ffc9c9` | Warnings/Errors |
|
||||
| Cyan | `#96f2d7` | Secondary items |
|
||||
| Transparent | `transparent` | No fill |
|
||||
| White | `#ffffff` | Background |
|
||||
|
||||
## ID Generation
|
||||
|
||||
IDs should be unique strings. Common patterns:
|
||||
|
||||
```javascript
|
||||
// Timestamp-based
|
||||
const id = Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
|
||||
// Sequential
|
||||
const id = "element-" + counter++;
|
||||
|
||||
// Descriptive
|
||||
const id = "step-1", "entity-user", "arrow-1-to-2";
|
||||
```
|
||||
|
||||
## Seed Generation
|
||||
|
||||
Seeds are used for deterministic randomness in hand-drawn effect:
|
||||
|
||||
```javascript
|
||||
const seed = Math.floor(Math.random() * 2147483647);
|
||||
```
|
||||
|
||||
## Version and VersionNonce
|
||||
|
||||
```javascript
|
||||
const version = 1; // Increment when element is edited
|
||||
const versionNonce = Math.floor(Math.random() * 2147483647);
|
||||
```
|
||||
|
||||
## Coordinate System
|
||||
|
||||
- Origin `(0, 0)` is top-left corner
|
||||
- X increases to the right
|
||||
- Y increases downward
|
||||
- All units are in pixels
|
||||
|
||||
## Recommended Spacing
|
||||
|
||||
| Context | Spacing |
|
||||
|---------|---------|
|
||||
| Horizontal gap between elements | 200-300px |
|
||||
| Vertical gap between rows | 100-150px |
|
||||
| Minimum margin from edge | 50px |
|
||||
| Arrow-to-box clearance | 20-30px |
|
||||
|
||||
## Font Families
|
||||
|
||||
| ID | Name | Description |
|
||||
|----|------|-------------|
|
||||
| 1 | Virgil | Hand-drawn style (default) |
|
||||
| 2 | Helvetica | Clean sans-serif |
|
||||
| 3 | Cascadia | Monospace |
|
||||
|
||||
## Validation Rules
|
||||
|
||||
✅ **Required:**
|
||||
- All IDs must be unique
|
||||
- `type` must match actual element type
|
||||
- `version` must be an integer ≥ 1
|
||||
- `opacity` must be 0-100
|
||||
|
||||
⚠️ **Recommended:**
|
||||
- Keep `roughness` at 1 for consistency
|
||||
- Use `strokeWidth` of 2 for clarity
|
||||
- Set `isDeleted` to `false`
|
||||
- Set `locked` to `false`
|
||||
- Keep `frameId`, `boundElements`, `link` as `null`
|
||||
|
||||
## Complete Minimal Example
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"id": "box1",
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 200,
|
||||
"height": 100,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#a5d8ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a0",
|
||||
"roundness": { "type": 3 },
|
||||
"seed": 1234567890,
|
||||
"version": 1,
|
||||
"versionNonce": 987654321,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Hello",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"gridSize": 20
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
```
|
||||
34
skills/excalidraw-diagram-generator/scripts/.gitignore
vendored
Normal file
34
skills/excalidraw-diagram-generator/scripts/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# Avoid accidentally committing local Python artifacts produced when running these scripts.
|
||||
|
||||
# Byte-compiled / cache files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Virtual environments (people often create these next to scripts)
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# Tool caches
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.tox/
|
||||
.nox/
|
||||
|
||||
# Coverage outputs
|
||||
.coverage
|
||||
.coverage.*
|
||||
htmlcov/
|
||||
|
||||
# Packaging/build outputs (rare here, but harmless)
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
|
||||
# OS cruft
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
193
skills/excalidraw-diagram-generator/scripts/README.md
Normal file
193
skills/excalidraw-diagram-generator/scripts/README.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# Excalidraw Library Tools
|
||||
|
||||
This directory contains scripts for working with Excalidraw libraries.
|
||||
|
||||
## split-excalidraw-library.py
|
||||
|
||||
Splits an Excalidraw library file (`*.excalidrawlib`) into individual icon JSON files for efficient token usage by AI assistants.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.6 or higher
|
||||
- No additional dependencies required (uses only standard library)
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
python split-excalidraw-library.py <path-to-library-directory>
|
||||
```
|
||||
|
||||
### Step-by-Step Workflow
|
||||
|
||||
1. **Create library directory**:
|
||||
```bash
|
||||
mkdir -p skills/excalidraw-diagram-generator/libraries/aws-architecture-icons
|
||||
```
|
||||
|
||||
2. **Download and place library file**:
|
||||
- Visit: https://libraries.excalidraw.com/
|
||||
- Search for "AWS Architecture Icons" and download the `.excalidrawlib` file
|
||||
- Rename it to match the directory name: `aws-architecture-icons.excalidrawlib`
|
||||
- Place it in the directory created in step 1
|
||||
|
||||
3. **Run the script**:
|
||||
```bash
|
||||
python skills/excalidraw-diagram-generator/scripts/split-excalidraw-library.py skills/excalidraw-diagram-generator/libraries/aws-architecture-icons/
|
||||
```
|
||||
|
||||
### Output Structure
|
||||
|
||||
The script creates the following structure in the library directory:
|
||||
|
||||
```
|
||||
skills/excalidraw-diagram-generator/libraries/aws-architecture-icons/
|
||||
aws-architecture-icons.excalidrawlib # Original file (kept)
|
||||
reference.md # Generated: Quick reference table
|
||||
icons/ # Generated: Individual icon files
|
||||
API-Gateway.json
|
||||
CloudFront.json
|
||||
EC2.json
|
||||
S3.json
|
||||
...
|
||||
```
|
||||
|
||||
### What the Script Does
|
||||
|
||||
1. **Reads** the `.excalidrawlib` file
|
||||
2. **Extracts** each icon from the `libraryItems` array
|
||||
3. **Sanitizes** icon names to create valid filenames (spaces → hyphens, removes special characters)
|
||||
4. **Saves** each icon as a separate JSON file in the `icons/` directory
|
||||
5. **Generates** a `reference.md` file with a table mapping icon names to filenames
|
||||
|
||||
### Benefits
|
||||
|
||||
- **Token Efficiency**: AI can first read the lightweight `reference.md` to find relevant icons, then load only the specific icon files needed
|
||||
- **Organization**: Icons are organized in a clear directory structure
|
||||
- **Extensibility**: Users can add multiple library sets side-by-side
|
||||
|
||||
### Recommended Workflow
|
||||
|
||||
1. Download desired Excalidraw libraries from https://libraries.excalidraw.com/
|
||||
2. Run this script on each library file
|
||||
3. Move the generated folders to `../libraries/`
|
||||
4. The AI assistant will use `reference.md` files to locate and use icons efficiently
|
||||
|
||||
### Library Sources (Examples — verify availability)
|
||||
|
||||
- Examples found on https://libraries.excalidraw.com/ may include cloud/service icon sets.
|
||||
- Availability changes over time; verify the exact library names on the site before use.
|
||||
- This script works with any valid `.excalidrawlib` file you provide.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Error: File not found**
|
||||
- Check that the file path is correct
|
||||
- Make sure the file has a `.excalidrawlib` extension
|
||||
|
||||
**Error: Invalid library file format**
|
||||
- Ensure the file is a valid Excalidraw library file
|
||||
- Check that it contains a `libraryItems` array
|
||||
|
||||
### License Considerations
|
||||
|
||||
When using third-party icon libraries:
|
||||
- **AWS Architecture Icons**: Subject to AWS Content License
|
||||
- **GCP Icons**: Subject to Google's terms
|
||||
- **Other libraries**: Check each library's license
|
||||
|
||||
This script is for personal/organizational use. Redistribution of split icon files should comply with the original library's license terms.
|
||||
|
||||
## add-icon-to-diagram.py
|
||||
|
||||
Adds a specific icon from a split Excalidraw library into an existing `.excalidraw` diagram. The script handles coordinate translation and ID collision avoidance, and can optionally add a label under the icon.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.6 or higher
|
||||
- A diagram file (`.excalidraw`)
|
||||
- A split icon library directory (created by `split-excalidraw-library.py`)
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
python add-icon-to-diagram.py <diagram-path> <icon-name> <x> <y> [OPTIONS]
|
||||
```
|
||||
|
||||
**Options**
|
||||
- `--library-path PATH` : Path to the icon library directory (default: `aws-architecture-icons`)
|
||||
- `--label TEXT` : Add a text label below the icon
|
||||
-- `--use-edit-suffix` : Edit via `.excalidraw.edit` to avoid editor overwrite issues (enabled by default; pass `--no-use-edit-suffix` to disable)
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Add EC2 icon at position (400, 300)
|
||||
python add-icon-to-diagram.py diagram.excalidraw EC2 400 300
|
||||
|
||||
# Add VPC icon with label
|
||||
python add-icon-to-diagram.py diagram.excalidraw VPC 200 150 --label "VPC"
|
||||
|
||||
# Safe edit mode is enabled by default (avoids editor overwrite issues)
|
||||
# Use `--no-use-edit-suffix` to disable
|
||||
python add-icon-to-diagram.py diagram.excalidraw EC2 500 300
|
||||
|
||||
# Add icon from another library
|
||||
python add-icon-to-diagram.py diagram.excalidraw Compute-Engine 500 200 \
|
||||
--library-path libraries/gcp-icons --label "API Server"
|
||||
```
|
||||
|
||||
### What the Script Does
|
||||
|
||||
1. **Loads** the icon JSON from the library’s `icons/` directory
|
||||
2. **Calculates** the icon’s bounding box
|
||||
3. **Offsets** all coordinates to the target position
|
||||
4. **Generates** unique IDs for all elements and groups
|
||||
5. **Appends** the transformed elements to the diagram
|
||||
6. **(Optional)** Adds a label beneath the icon
|
||||
|
||||
---
|
||||
|
||||
## add-arrow.py
|
||||
|
||||
Adds a straight arrow between two points in an existing `.excalidraw` diagram. Supports optional labels and line styles.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.6 or higher
|
||||
- A diagram file (`.excalidraw`)
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
python add-arrow.py <diagram-path> <from-x> <from-y> <to-x> <to-y> [OPTIONS]
|
||||
```
|
||||
|
||||
**Options**
|
||||
- `--style {solid|dashed|dotted}` : Line style (default: `solid`)
|
||||
- `--color HEX` : Arrow color (default: `#1e1e1e`)
|
||||
- `--label TEXT` : Add a text label on the arrow
|
||||
-- `--use-edit-suffix` : Edit via `.excalidraw.edit` to avoid editor overwrite issues (enabled by default; pass `--no-use-edit-suffix` to disable)
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Simple arrow
|
||||
python add-arrow.py diagram.excalidraw 300 200 500 300
|
||||
|
||||
# Arrow with label
|
||||
python add-arrow.py diagram.excalidraw 300 200 500 300 --label "HTTPS"
|
||||
|
||||
# Dashed arrow with custom color
|
||||
python add-arrow.py diagram.excalidraw 400 350 600 400 --style dashed --color "#7950f2"
|
||||
|
||||
# Safe edit mode is enabled by default (avoids editor overwrite issues)
|
||||
# Use `--no-use-edit-suffix` to disable
|
||||
python add-arrow.py diagram.excalidraw 300 200 500 300
|
||||
```
|
||||
|
||||
### What the Script Does
|
||||
|
||||
1. **Creates** an arrow element from the given coordinates
|
||||
2. **(Optional)** Adds a label near the arrow midpoint
|
||||
3. **Appends** elements to the diagram
|
||||
4. **Saves** the updated file
|
||||
312
skills/excalidraw-diagram-generator/scripts/add-arrow.py
Normal file
312
skills/excalidraw-diagram-generator/scripts/add-arrow.py
Normal file
@@ -0,0 +1,312 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Add arrows (connections) between elements in Excalidraw diagrams.
|
||||
|
||||
Usage:
|
||||
python add-arrow.py <diagram_path> <from_x> <from_y> <to_x> <to_y> [OPTIONS]
|
||||
|
||||
Options:
|
||||
--style {solid|dashed|dotted} Arrow line style (default: solid)
|
||||
--color HEX Arrow color (default: #1e1e1e)
|
||||
--label TEXT Add text label on the arrow
|
||||
--use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)
|
||||
|
||||
Examples:
|
||||
python add-arrow.py diagram.excalidraw 300 200 500 300
|
||||
python add-arrow.py diagram.excalidraw 300 200 500 300 --label "HTTP"
|
||||
python add-arrow.py diagram.excalidraw 300 200 500 300 --style dashed --color "#7950f2"
|
||||
python add-arrow.py diagram.excalidraw 300 200 500 300 --use-edit-suffix
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def generate_unique_id() -> str:
|
||||
"""Generate a unique ID for Excalidraw elements."""
|
||||
return str(uuid.uuid4()).replace('-', '')[:16]
|
||||
|
||||
|
||||
def prepare_edit_path(diagram_path: Path, use_edit_suffix: bool) -> tuple[Path, Path | None]:
|
||||
"""
|
||||
Prepare a safe edit path to avoid editor overwrite issues.
|
||||
|
||||
Returns:
|
||||
(work_path, final_path)
|
||||
- work_path: file path to read/write during edit
|
||||
- final_path: file path to rename back to (or None if not used)
|
||||
"""
|
||||
if not use_edit_suffix:
|
||||
return diagram_path, None
|
||||
|
||||
if diagram_path.suffix != ".excalidraw":
|
||||
return diagram_path, None
|
||||
|
||||
edit_path = diagram_path.with_suffix(diagram_path.suffix + ".edit")
|
||||
|
||||
if diagram_path.exists():
|
||||
if edit_path.exists():
|
||||
raise FileExistsError(f"Edit file already exists: {edit_path}")
|
||||
diagram_path.rename(edit_path)
|
||||
|
||||
return edit_path, diagram_path
|
||||
|
||||
|
||||
def finalize_edit_path(work_path: Path, final_path: Path | None) -> None:
|
||||
"""Finalize edit by renaming .edit back to .excalidraw if needed."""
|
||||
if final_path is None:
|
||||
return
|
||||
|
||||
if final_path.exists():
|
||||
final_path.unlink()
|
||||
|
||||
work_path.rename(final_path)
|
||||
|
||||
|
||||
def create_arrow(
|
||||
from_x: float,
|
||||
from_y: float,
|
||||
to_x: float,
|
||||
to_y: float,
|
||||
style: str = "solid",
|
||||
color: str = "#1e1e1e",
|
||||
label: str = None
|
||||
) -> list:
|
||||
"""
|
||||
Create an arrow element.
|
||||
|
||||
Args:
|
||||
from_x: Starting X coordinate
|
||||
from_y: Starting Y coordinate
|
||||
to_x: Ending X coordinate
|
||||
to_y: Ending Y coordinate
|
||||
style: Line style (solid, dashed, dotted)
|
||||
color: Arrow color
|
||||
label: Optional text label on the arrow
|
||||
|
||||
Returns:
|
||||
List of elements (arrow and optional label)
|
||||
"""
|
||||
elements = []
|
||||
|
||||
# Arrow element
|
||||
arrow = {
|
||||
"id": generate_unique_id(),
|
||||
"type": "arrow",
|
||||
"x": from_x,
|
||||
"y": from_y,
|
||||
"width": to_x - from_x,
|
||||
"height": to_y - from_y,
|
||||
"angle": 0,
|
||||
"strokeColor": color,
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": style,
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": None,
|
||||
"index": "a0",
|
||||
"roundness": {
|
||||
"type": 2
|
||||
},
|
||||
"seed": 1000000000 + hash(f"{from_x}{from_y}{to_x}{to_y}") % 1000000000,
|
||||
"version": 1,
|
||||
"versionNonce": 2000000000 + hash(f"{from_x}{from_y}{to_x}{to_y}") % 1000000000,
|
||||
"isDeleted": False,
|
||||
"boundElements": [],
|
||||
"updated": 1738195200000,
|
||||
"link": None,
|
||||
"locked": False,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[to_x - from_x, to_y - from_y]
|
||||
],
|
||||
"startBinding": None,
|
||||
"endBinding": None,
|
||||
"startArrowhead": None,
|
||||
"endArrowhead": "arrow",
|
||||
"lastCommittedPoint": None
|
||||
}
|
||||
elements.append(arrow)
|
||||
|
||||
# Optional label
|
||||
if label:
|
||||
mid_x = (from_x + to_x) / 2 - (len(label) * 5)
|
||||
mid_y = (from_y + to_y) / 2 - 10
|
||||
|
||||
label_element = {
|
||||
"id": generate_unique_id(),
|
||||
"type": "text",
|
||||
"x": mid_x,
|
||||
"y": mid_y,
|
||||
"width": len(label) * 10,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": color,
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": None,
|
||||
"index": "a0",
|
||||
"roundness": None,
|
||||
"seed": 1000000000 + hash(label) % 1000000000,
|
||||
"version": 1,
|
||||
"versionNonce": 2000000000 + hash(label) % 1000000000,
|
||||
"isDeleted": False,
|
||||
"boundElements": [],
|
||||
"updated": 1738195200000,
|
||||
"link": None,
|
||||
"locked": False,
|
||||
"text": label,
|
||||
"fontSize": 14,
|
||||
"fontFamily": 5,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top",
|
||||
"containerId": None,
|
||||
"originalText": label,
|
||||
"autoResize": True,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
elements.append(label_element)
|
||||
|
||||
return elements
|
||||
|
||||
|
||||
def add_arrow_to_diagram(
|
||||
diagram_path: Path,
|
||||
from_x: float,
|
||||
from_y: float,
|
||||
to_x: float,
|
||||
to_y: float,
|
||||
style: str = "solid",
|
||||
color: str = "#1e1e1e",
|
||||
label: str = None
|
||||
) -> None:
|
||||
"""
|
||||
Add an arrow to an Excalidraw diagram.
|
||||
|
||||
Args:
|
||||
diagram_path: Path to the Excalidraw diagram file
|
||||
from_x: Starting X coordinate
|
||||
from_y: Starting Y coordinate
|
||||
to_x: Ending X coordinate
|
||||
to_y: Ending Y coordinate
|
||||
style: Line style (solid, dashed, dotted)
|
||||
color: Arrow color
|
||||
label: Optional text label
|
||||
"""
|
||||
print(f"Creating arrow from ({from_x}, {from_y}) to ({to_x}, {to_y})")
|
||||
arrow_elements = create_arrow(from_x, from_y, to_x, to_y, style, color, label)
|
||||
|
||||
if label:
|
||||
print(f" With label: '{label}'")
|
||||
|
||||
# Load diagram
|
||||
print(f"Loading diagram: {diagram_path}")
|
||||
with open(diagram_path, 'r', encoding='utf-8') as f:
|
||||
diagram = json.load(f)
|
||||
|
||||
# Add arrow elements
|
||||
if 'elements' not in diagram:
|
||||
diagram['elements'] = []
|
||||
|
||||
original_count = len(diagram['elements'])
|
||||
diagram['elements'].extend(arrow_elements)
|
||||
print(f" Added {len(arrow_elements)} elements (total: {original_count} -> {len(diagram['elements'])})")
|
||||
|
||||
# Save diagram
|
||||
print(f"Saving diagram")
|
||||
with open(diagram_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(diagram, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"✓ Successfully added arrow to diagram")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if len(sys.argv) < 6:
|
||||
print("Usage: python add-arrow.py <diagram_path> <from_x> <from_y> <to_x> <to_y> [OPTIONS]")
|
||||
print("\nOptions:")
|
||||
print(" --style {solid|dashed|dotted} Line style (default: solid)")
|
||||
print(" --color HEX Color (default: #1e1e1e)")
|
||||
print(" --label TEXT Text label on arrow")
|
||||
print(" --use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)")
|
||||
print("\nExamples:")
|
||||
print(" python add-arrow.py diagram.excalidraw 300 200 500 300")
|
||||
print(" python add-arrow.py diagram.excalidraw 300 200 500 300 --label 'HTTP'")
|
||||
sys.exit(1)
|
||||
|
||||
diagram_path = Path(sys.argv[1])
|
||||
from_x = float(sys.argv[2])
|
||||
from_y = float(sys.argv[3])
|
||||
to_x = float(sys.argv[4])
|
||||
to_y = float(sys.argv[5])
|
||||
|
||||
# Parse optional arguments
|
||||
style = "solid"
|
||||
color = "#1e1e1e"
|
||||
label = None
|
||||
# Default: use edit suffix to avoid editor overwrite issues
|
||||
use_edit_suffix = True
|
||||
|
||||
i = 6
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i] == '--style':
|
||||
if i + 1 < len(sys.argv):
|
||||
style = sys.argv[i + 1]
|
||||
if style not in ['solid', 'dashed', 'dotted']:
|
||||
print(f"Error: Invalid style '{style}'. Must be: solid, dashed, or dotted")
|
||||
sys.exit(1)
|
||||
i += 2
|
||||
else:
|
||||
print("Error: --style requires an argument")
|
||||
sys.exit(1)
|
||||
elif sys.argv[i] == '--color':
|
||||
if i + 1 < len(sys.argv):
|
||||
color = sys.argv[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
print("Error: --color requires an argument")
|
||||
sys.exit(1)
|
||||
elif sys.argv[i] == '--label':
|
||||
if i + 1 < len(sys.argv):
|
||||
label = sys.argv[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
print("Error: --label requires a text argument")
|
||||
sys.exit(1)
|
||||
elif sys.argv[i] == '--use-edit-suffix':
|
||||
use_edit_suffix = True
|
||||
i += 1
|
||||
elif sys.argv[i] == '--no-use-edit-suffix':
|
||||
use_edit_suffix = False
|
||||
i += 1
|
||||
else:
|
||||
print(f"Error: Unknown option: {sys.argv[i]}")
|
||||
sys.exit(1)
|
||||
|
||||
# Validate inputs
|
||||
if not diagram_path.exists():
|
||||
print(f"Error: Diagram file not found: {diagram_path}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
work_path, final_path = prepare_edit_path(diagram_path, use_edit_suffix)
|
||||
add_arrow_to_diagram(work_path, from_x, from_y, to_x, to_y, style, color, label)
|
||||
finalize_edit_path(work_path, final_path)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,404 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Add icons from Excalidraw libraries to diagrams.
|
||||
|
||||
This script reads an icon JSON file from an Excalidraw library, transforms its coordinates
|
||||
to a target position, generates unique IDs, and adds it to an existing Excalidraw diagram.
|
||||
Works with any Excalidraw library (AWS, GCP, Azure, Kubernetes, etc.).
|
||||
|
||||
Usage:
|
||||
python add-icon-to-diagram.py <diagram_path> <icon_name> <x> <y> [OPTIONS]
|
||||
|
||||
Options:
|
||||
--library-path PATH Path to the icon library directory (default: aws-architecture-icons)
|
||||
--label TEXT Add a text label below the icon
|
||||
--use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)
|
||||
|
||||
Examples:
|
||||
python add-icon-to-diagram.py diagram.excalidraw EC2 500 300
|
||||
python add-icon-to-diagram.py diagram.excalidraw EC2 500 300 --label "Web Server"
|
||||
python add-icon-to-diagram.py diagram.excalidraw VPC 200 150 --library-path libraries/gcp-icons
|
||||
python add-icon-to-diagram.py diagram.excalidraw EC2 500 300 --use-edit-suffix
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Tuple
|
||||
|
||||
|
||||
def generate_unique_id() -> str:
|
||||
"""Generate a unique ID for Excalidraw elements."""
|
||||
return str(uuid.uuid4()).replace('-', '')[:16]
|
||||
|
||||
|
||||
def calculate_bounding_box(elements: List[Dict[str, Any]]) -> Tuple[float, float, float, float]:
|
||||
"""Calculate the bounding box (min_x, min_y, max_x, max_y) of icon elements."""
|
||||
if not elements:
|
||||
return (0, 0, 0, 0)
|
||||
|
||||
min_x = float('inf')
|
||||
min_y = float('inf')
|
||||
max_x = float('-inf')
|
||||
max_y = float('-inf')
|
||||
|
||||
for element in elements:
|
||||
if 'x' in element and 'y' in element:
|
||||
x = element['x']
|
||||
y = element['y']
|
||||
width = element.get('width', 0)
|
||||
height = element.get('height', 0)
|
||||
|
||||
min_x = min(min_x, x)
|
||||
min_y = min(min_y, y)
|
||||
max_x = max(max_x, x + width)
|
||||
max_y = max(max_y, y + height)
|
||||
|
||||
return (min_x, min_y, max_x, max_y)
|
||||
|
||||
|
||||
def transform_icon_elements(
|
||||
elements: List[Dict[str, Any]],
|
||||
target_x: float,
|
||||
target_y: float
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Transform icon elements to target coordinates with unique IDs.
|
||||
|
||||
Args:
|
||||
elements: Icon elements from JSON file
|
||||
target_x: Target X coordinate (top-left position)
|
||||
target_y: Target Y coordinate (top-left position)
|
||||
|
||||
Returns:
|
||||
Transformed elements with new coordinates and IDs
|
||||
"""
|
||||
if not elements:
|
||||
return []
|
||||
|
||||
# Calculate bounding box
|
||||
min_x, min_y, max_x, max_y = calculate_bounding_box(elements)
|
||||
|
||||
# Calculate offset
|
||||
offset_x = target_x - min_x
|
||||
offset_y = target_y - min_y
|
||||
|
||||
# Create ID mapping: old_id -> new_id
|
||||
id_mapping = {}
|
||||
for element in elements:
|
||||
if 'id' in element:
|
||||
old_id = element['id']
|
||||
id_mapping[old_id] = generate_unique_id()
|
||||
|
||||
# Create group ID mapping
|
||||
group_id_mapping = {}
|
||||
for element in elements:
|
||||
if 'groupIds' in element:
|
||||
for old_group_id in element['groupIds']:
|
||||
if old_group_id not in group_id_mapping:
|
||||
group_id_mapping[old_group_id] = generate_unique_id()
|
||||
|
||||
# Transform elements
|
||||
transformed = []
|
||||
for element in elements:
|
||||
new_element = element.copy()
|
||||
|
||||
# Update coordinates
|
||||
if 'x' in new_element:
|
||||
new_element['x'] = new_element['x'] + offset_x
|
||||
if 'y' in new_element:
|
||||
new_element['y'] = new_element['y'] + offset_y
|
||||
|
||||
# Update ID
|
||||
if 'id' in new_element:
|
||||
new_element['id'] = id_mapping[new_element['id']]
|
||||
|
||||
# Update group IDs
|
||||
if 'groupIds' in new_element:
|
||||
new_element['groupIds'] = [
|
||||
group_id_mapping[gid] for gid in new_element['groupIds']
|
||||
]
|
||||
|
||||
# Update binding references if they exist
|
||||
if 'startBinding' in new_element and new_element['startBinding']:
|
||||
if 'elementId' in new_element['startBinding']:
|
||||
old_id = new_element['startBinding']['elementId']
|
||||
if old_id in id_mapping:
|
||||
new_element['startBinding']['elementId'] = id_mapping[old_id]
|
||||
|
||||
if 'endBinding' in new_element and new_element['endBinding']:
|
||||
if 'elementId' in new_element['endBinding']:
|
||||
old_id = new_element['endBinding']['elementId']
|
||||
if old_id in id_mapping:
|
||||
new_element['endBinding']['elementId'] = id_mapping[old_id]
|
||||
|
||||
# Update containerId if it exists
|
||||
if 'containerId' in new_element and new_element['containerId']:
|
||||
old_id = new_element['containerId']
|
||||
if old_id in id_mapping:
|
||||
new_element['containerId'] = id_mapping[old_id]
|
||||
|
||||
# Update boundElements if they exist
|
||||
if 'boundElements' in new_element and new_element['boundElements']:
|
||||
new_bound_elements = []
|
||||
for bound_elem in new_element['boundElements']:
|
||||
if isinstance(bound_elem, dict) and 'id' in bound_elem:
|
||||
old_id = bound_elem['id']
|
||||
if old_id in id_mapping:
|
||||
bound_elem['id'] = id_mapping[old_id]
|
||||
new_bound_elements.append(bound_elem)
|
||||
new_element['boundElements'] = new_bound_elements
|
||||
|
||||
transformed.append(new_element)
|
||||
|
||||
return transformed
|
||||
|
||||
|
||||
def load_icon(icon_name: str, library_path: Path) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Load icon elements from library.
|
||||
|
||||
Args:
|
||||
icon_name: Name of the icon (e.g., "EC2", "VPC")
|
||||
library_path: Path to the icon library directory
|
||||
|
||||
Returns:
|
||||
List of icon elements
|
||||
"""
|
||||
icon_file = library_path / "icons" / f"{icon_name}.json"
|
||||
|
||||
if not icon_file.exists():
|
||||
raise FileNotFoundError(f"Icon file not found: {icon_file}")
|
||||
|
||||
with open(icon_file, 'r', encoding='utf-8') as f:
|
||||
icon_data = json.load(f)
|
||||
|
||||
return icon_data.get('elements', [])
|
||||
|
||||
|
||||
def prepare_edit_path(diagram_path: Path, use_edit_suffix: bool) -> tuple[Path, Path | None]:
|
||||
"""
|
||||
Prepare a safe edit path to avoid editor overwrite issues.
|
||||
|
||||
Returns:
|
||||
(work_path, final_path)
|
||||
- work_path: file path to read/write during edit
|
||||
- final_path: file path to rename back to (or None if not used)
|
||||
"""
|
||||
if not use_edit_suffix:
|
||||
return diagram_path, None
|
||||
|
||||
if diagram_path.suffix != ".excalidraw":
|
||||
return diagram_path, None
|
||||
|
||||
edit_path = diagram_path.with_suffix(diagram_path.suffix + ".edit")
|
||||
|
||||
if diagram_path.exists():
|
||||
if edit_path.exists():
|
||||
raise FileExistsError(f"Edit file already exists: {edit_path}")
|
||||
diagram_path.rename(edit_path)
|
||||
|
||||
return edit_path, diagram_path
|
||||
|
||||
|
||||
def finalize_edit_path(work_path: Path, final_path: Path | None) -> None:
|
||||
"""Finalize edit by renaming .edit back to .excalidraw if needed."""
|
||||
if final_path is None:
|
||||
return
|
||||
|
||||
if final_path.exists():
|
||||
final_path.unlink()
|
||||
|
||||
work_path.rename(final_path)
|
||||
|
||||
|
||||
def create_text_label(text: str, x: float, y: float) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a text label element.
|
||||
|
||||
Args:
|
||||
text: Label text
|
||||
x: X coordinate
|
||||
y: Y coordinate
|
||||
|
||||
Returns:
|
||||
Text element dictionary
|
||||
"""
|
||||
return {
|
||||
"id": generate_unique_id(),
|
||||
"type": "text",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"width": len(text) * 10, # Approximate width
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": None,
|
||||
"index": "a0",
|
||||
"roundness": None,
|
||||
"seed": 1000000000 + hash(text) % 1000000000,
|
||||
"version": 1,
|
||||
"versionNonce": 2000000000 + hash(text) % 1000000000,
|
||||
"isDeleted": False,
|
||||
"boundElements": [],
|
||||
"updated": 1738195200000,
|
||||
"link": None,
|
||||
"locked": False,
|
||||
"text": text,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 5, # Excalifont
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top",
|
||||
"containerId": None,
|
||||
"originalText": text,
|
||||
"autoResize": True,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
|
||||
|
||||
def add_icon_to_diagram(
|
||||
diagram_path: Path,
|
||||
icon_name: str,
|
||||
x: float,
|
||||
y: float,
|
||||
library_path: Path,
|
||||
label: str = None
|
||||
) -> None:
|
||||
"""
|
||||
Add an icon to an Excalidraw diagram.
|
||||
|
||||
Args:
|
||||
diagram_path: Path to the Excalidraw diagram file
|
||||
icon_name: Name of the icon to add
|
||||
x: Target X coordinate
|
||||
y: Target Y coordinate
|
||||
library_path: Path to the icon library directory
|
||||
label: Optional text label to add below the icon
|
||||
"""
|
||||
# Load icon elements
|
||||
print(f"Loading icon: {icon_name}")
|
||||
icon_elements = load_icon(icon_name, library_path)
|
||||
print(f" Loaded {len(icon_elements)} elements")
|
||||
|
||||
# Transform icon elements
|
||||
print(f"Transforming to position ({x}, {y})")
|
||||
transformed_elements = transform_icon_elements(icon_elements, x, y)
|
||||
|
||||
# Calculate icon bounding box for label positioning
|
||||
if label and transformed_elements:
|
||||
min_x, min_y, max_x, max_y = calculate_bounding_box(transformed_elements)
|
||||
icon_width = max_x - min_x
|
||||
icon_height = max_y - min_y
|
||||
|
||||
# Position label below icon, centered
|
||||
label_x = min_x + (icon_width / 2) - (len(label) * 5)
|
||||
label_y = max_y + 10
|
||||
|
||||
label_element = create_text_label(label, label_x, label_y)
|
||||
transformed_elements.append(label_element)
|
||||
print(f" Added label: '{label}'")
|
||||
|
||||
# Load diagram
|
||||
print(f"Loading diagram: {diagram_path}")
|
||||
with open(diagram_path, 'r', encoding='utf-8') as f:
|
||||
diagram = json.load(f)
|
||||
|
||||
# Add transformed elements
|
||||
if 'elements' not in diagram:
|
||||
diagram['elements'] = []
|
||||
|
||||
original_count = len(diagram['elements'])
|
||||
diagram['elements'].extend(transformed_elements)
|
||||
print(f" Added {len(transformed_elements)} elements (total: {original_count} -> {len(diagram['elements'])})")
|
||||
|
||||
# Save diagram
|
||||
print(f"Saving diagram")
|
||||
with open(diagram_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(diagram, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"✓ Successfully added '{icon_name}' icon to diagram")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if len(sys.argv) < 5:
|
||||
print("Usage: python add-icon-to-diagram.py <diagram_path> <icon_name> <x> <y> [OPTIONS]")
|
||||
print("\nOptions:")
|
||||
print(" --library-path PATH Path to icon library directory")
|
||||
print(" --label TEXT Add text label below icon")
|
||||
print(" --use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)")
|
||||
print("\nExamples:")
|
||||
print(" python add-icon-to-diagram.py diagram.excalidraw EC2 500 300")
|
||||
print(" python add-icon-to-diagram.py diagram.excalidraw EC2 500 300 --label 'Web Server'")
|
||||
sys.exit(1)
|
||||
|
||||
diagram_path = Path(sys.argv[1])
|
||||
icon_name = sys.argv[2]
|
||||
x = float(sys.argv[3])
|
||||
y = float(sys.argv[4])
|
||||
|
||||
# Default library path
|
||||
script_dir = Path(__file__).parent
|
||||
default_library_path = script_dir.parent / "libraries" / "aws-architecture-icons"
|
||||
|
||||
# Parse optional arguments
|
||||
library_path = default_library_path
|
||||
label = None
|
||||
# Default: use edit suffix to avoid editor overwrite issues
|
||||
use_edit_suffix = True
|
||||
|
||||
i = 5
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i] == '--library-path':
|
||||
if i + 1 < len(sys.argv):
|
||||
library_path = Path(sys.argv[i + 1])
|
||||
i += 2
|
||||
else:
|
||||
print("Error: --library-path requires a path argument")
|
||||
sys.exit(1)
|
||||
elif sys.argv[i] == '--label':
|
||||
if i + 1 < len(sys.argv):
|
||||
label = sys.argv[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
print("Error: --label requires a text argument")
|
||||
sys.exit(1)
|
||||
elif sys.argv[i] == '--use-edit-suffix':
|
||||
use_edit_suffix = True
|
||||
i += 1
|
||||
elif sys.argv[i] == '--no-use-edit-suffix':
|
||||
use_edit_suffix = False
|
||||
i += 1
|
||||
else:
|
||||
print(f"Error: Unknown option: {sys.argv[i]}")
|
||||
sys.exit(1)
|
||||
|
||||
# Validate inputs
|
||||
if not diagram_path.exists():
|
||||
print(f"Error: Diagram file not found: {diagram_path}")
|
||||
sys.exit(1)
|
||||
|
||||
if not library_path.exists():
|
||||
print(f"Error: Library path not found: {library_path}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
work_path, final_path = prepare_edit_path(diagram_path, use_edit_suffix)
|
||||
add_icon_to_diagram(work_path, icon_name, x, y, library_path, label)
|
||||
finalize_edit_path(work_path, final_path)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Excalidraw Library Splitter
|
||||
|
||||
This script splits an Excalidraw library file (*.excalidrawlib) into individual
|
||||
icon JSON files and generates a reference.md file for easy lookup.
|
||||
|
||||
The script expects the following structure:
|
||||
skills/excalidraw-diagram-generator/libraries/{icon-set-name}/
|
||||
{icon-set-name}.excalidrawlib (place this file first)
|
||||
|
||||
Usage:
|
||||
python split-excalidraw-library.py <path-to-library-directory>
|
||||
|
||||
Example:
|
||||
python split-excalidraw-library.py skills/excalidraw-diagram-generator/libraries/aws-architecture-icons/
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def sanitize_filename(name: str) -> str:
|
||||
"""
|
||||
Sanitize icon name to create a valid filename.
|
||||
|
||||
Args:
|
||||
name: Original icon name
|
||||
|
||||
Returns:
|
||||
Sanitized filename safe for all platforms
|
||||
"""
|
||||
# Replace spaces with hyphens
|
||||
filename = name.replace(' ', '-')
|
||||
|
||||
# Remove or replace special characters
|
||||
filename = re.sub(r'[^\w\-.]', '', filename)
|
||||
|
||||
# Remove multiple consecutive hyphens
|
||||
filename = re.sub(r'-+', '-', filename)
|
||||
|
||||
# Remove leading/trailing hyphens
|
||||
filename = filename.strip('-')
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def find_library_file(directory: Path) -> Path:
|
||||
"""
|
||||
Find the .excalidrawlib file in the given directory.
|
||||
|
||||
Args:
|
||||
directory: Directory to search
|
||||
|
||||
Returns:
|
||||
Path to the library file
|
||||
|
||||
Raises:
|
||||
SystemExit: If no library file or multiple library files found
|
||||
"""
|
||||
library_files = list(directory.glob('*.excalidrawlib'))
|
||||
|
||||
if len(library_files) == 0:
|
||||
print(f"Error: No .excalidrawlib file found in {directory}")
|
||||
print(f"Please place a .excalidrawlib file in {directory} first.")
|
||||
sys.exit(1)
|
||||
|
||||
if len(library_files) > 1:
|
||||
print(f"Error: Multiple .excalidrawlib files found in {directory}")
|
||||
print(f"Please keep only one library file in {directory}.")
|
||||
sys.exit(1)
|
||||
|
||||
return library_files[0]
|
||||
|
||||
|
||||
def split_library(library_dir: str) -> None:
|
||||
"""
|
||||
Split an Excalidraw library file into individual icon files.
|
||||
|
||||
Args:
|
||||
library_dir: Path to the directory containing the .excalidrawlib file
|
||||
"""
|
||||
library_dir = Path(library_dir)
|
||||
|
||||
if not library_dir.exists():
|
||||
print(f"Error: Directory not found: {library_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
if not library_dir.is_dir():
|
||||
print(f"Error: Path is not a directory: {library_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
# Find the library file
|
||||
library_path = find_library_file(library_dir)
|
||||
print(f"Found library: {library_path.name}")
|
||||
|
||||
# Load library file
|
||||
print(f"Loading library data...")
|
||||
with open(library_path, 'r', encoding='utf-8') as f:
|
||||
library_data = json.load(f)
|
||||
|
||||
# Validate library structure
|
||||
if 'libraryItems' not in library_data:
|
||||
print("Error: Invalid library file format (missing 'libraryItems')")
|
||||
sys.exit(1)
|
||||
|
||||
# Create icons directory
|
||||
icons_dir = library_dir / 'icons'
|
||||
icons_dir.mkdir(exist_ok=True)
|
||||
print(f"Output directory: {library_dir}")
|
||||
|
||||
# Process each library item (icon)
|
||||
library_items = library_data['libraryItems']
|
||||
icon_list = []
|
||||
|
||||
print(f"Processing {len(library_items)} icons...")
|
||||
|
||||
for item in library_items:
|
||||
# Get icon name
|
||||
icon_name = item.get('name', 'Unnamed')
|
||||
|
||||
# Create sanitized filename
|
||||
filename = sanitize_filename(icon_name) + '.json'
|
||||
|
||||
# Save icon data
|
||||
icon_path = icons_dir / filename
|
||||
with open(icon_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(item, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# Add to reference list
|
||||
icon_list.append({
|
||||
'name': icon_name,
|
||||
'filename': filename
|
||||
})
|
||||
|
||||
print(f" ✓ {icon_name} → {filename}")
|
||||
|
||||
# Sort icon list by name
|
||||
icon_list.sort(key=lambda x: x['name'])
|
||||
|
||||
# Generate reference.md
|
||||
library_name = library_path.stem
|
||||
reference_path = library_dir / 'reference.md'
|
||||
with open(reference_path, 'w', encoding='utf-8') as f:
|
||||
f.write(f"# {library_name} Reference\n\n")
|
||||
f.write(f"This directory contains {len(icon_list)} icons extracted from `{library_path.name}`.\n\n")
|
||||
f.write("## Available Icons\n\n")
|
||||
f.write("| Icon Name | Filename |\n")
|
||||
f.write("|-----------|----------|\n")
|
||||
|
||||
for icon in icon_list:
|
||||
f.write(f"| {icon['name']} | `icons/{icon['filename']}` |\n")
|
||||
|
||||
f.write("\n## Usage\n\n")
|
||||
f.write("Each icon JSON file contains the complete `elements` array needed to render that icon in Excalidraw.\n")
|
||||
f.write("You can copy the elements from these files into your Excalidraw diagrams.\n")
|
||||
|
||||
print(f"\n✅ Successfully split library into {len(icon_list)} icons")
|
||||
print(f"📄 Reference file created: {reference_path}")
|
||||
print(f"📁 Icons directory: {icons_dir}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
# Ensure consistent UTF-8 output on Windows consoles.
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python split-excalidraw-library.py <path-to-library-directory>")
|
||||
print("\nExample:")
|
||||
print(" python split-excalidraw-library.py skills/excalidraw-diagram-generator/libraries/aws-architecture-icons/")
|
||||
print("\nNote: The directory should contain a .excalidrawlib file.")
|
||||
sys.exit(1)
|
||||
|
||||
library_dir = sys.argv[1]
|
||||
split_library(library_dir)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,334 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"id": "title",
|
||||
"type": "text",
|
||||
"x": 200,
|
||||
"y": 50,
|
||||
"width": 300,
|
||||
"height": 30,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a0",
|
||||
"roundness": null,
|
||||
"seed": 2001001001,
|
||||
"version": 1,
|
||||
"versionNonce": 3002002001,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Business Process Flow",
|
||||
"fontSize": 24,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "lane-header-1",
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 120,
|
||||
"width": 200,
|
||||
"height": 50,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a1",
|
||||
"roundness": null,
|
||||
"seed": 2001001002,
|
||||
"version": 1,
|
||||
"versionNonce": 3002002002,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Customer",
|
||||
"fontSize": 18,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "lane-1",
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 170,
|
||||
"width": 200,
|
||||
"height": 250,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a2",
|
||||
"roundness": null,
|
||||
"seed": 2001001003,
|
||||
"version": 1,
|
||||
"versionNonce": 3002002003,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "process-1",
|
||||
"type": "rectangle",
|
||||
"x": 130,
|
||||
"y": 200,
|
||||
"width": 140,
|
||||
"height": 70,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#b2f2bb",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a3",
|
||||
"roundness": { "type": 3 },
|
||||
"seed": 2001001004,
|
||||
"version": 1,
|
||||
"versionNonce": 3002002004,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Submit\nRequest",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "lane-header-2",
|
||||
"type": "rectangle",
|
||||
"x": 300,
|
||||
"y": 120,
|
||||
"width": 200,
|
||||
"height": 50,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#fff3bf",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a4",
|
||||
"roundness": null,
|
||||
"seed": 2001001005,
|
||||
"version": 1,
|
||||
"versionNonce": 3002002005,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Sales Team",
|
||||
"fontSize": 18,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "lane-2",
|
||||
"type": "rectangle",
|
||||
"x": 300,
|
||||
"y": 170,
|
||||
"width": 200,
|
||||
"height": 250,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a5",
|
||||
"roundness": null,
|
||||
"seed": 2001001006,
|
||||
"version": 1,
|
||||
"versionNonce": 3002002006,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "process-2",
|
||||
"type": "rectangle",
|
||||
"x": 330,
|
||||
"y": 200,
|
||||
"width": 140,
|
||||
"height": 70,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffd43b",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a6",
|
||||
"roundness": { "type": 3 },
|
||||
"seed": 2001001007,
|
||||
"version": 1,
|
||||
"versionNonce": 3002002007,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Review\nRequest",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "cross-lane-arrow",
|
||||
"type": "arrow",
|
||||
"x": 270,
|
||||
"y": 235,
|
||||
"width": 60,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a7",
|
||||
"roundness": { "type": 2 },
|
||||
"seed": 2001001008,
|
||||
"version": 1,
|
||||
"versionNonce": 3002002008,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[60, 0]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "process-3",
|
||||
"type": "rectangle",
|
||||
"x": 330,
|
||||
"y": 310,
|
||||
"width": 140,
|
||||
"height": 70,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffd43b",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a8",
|
||||
"roundness": { "type": 3 },
|
||||
"seed": 2001001009,
|
||||
"version": 1,
|
||||
"versionNonce": 3002002009,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Approve",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "within-lane-arrow",
|
||||
"type": "arrow",
|
||||
"x": 400,
|
||||
"y": 270,
|
||||
"width": 0,
|
||||
"height": 40,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a9",
|
||||
"roundness": { "type": 2 },
|
||||
"seed": 2001001010,
|
||||
"version": 1,
|
||||
"versionNonce": 3002002010,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[0, 40]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"gridSize": 20
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor",
|
||||
"elements": [
|
||||
{
|
||||
"id": "class-1",
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 200,
|
||||
"height": 180,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a0",
|
||||
"roundness": null,
|
||||
"seed": 3001001001,
|
||||
"version": 1,
|
||||
"versionNonce": 4002002001,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "class-name-1",
|
||||
"type": "text",
|
||||
"x": 150,
|
||||
"y": 110,
|
||||
"width": 100,
|
||||
"height": 25,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a1",
|
||||
"roundness": null,
|
||||
"seed": 3001001002,
|
||||
"version": 1,
|
||||
"versionNonce": 4002002002,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "User",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top",
|
||||
"containerId": null,
|
||||
"originalText": "User",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "separator-1",
|
||||
"type": "line",
|
||||
"x": 100,
|
||||
"y": 145,
|
||||
"width": 200,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a2",
|
||||
"roundness": null,
|
||||
"seed": 3001001003,
|
||||
"version": 1,
|
||||
"versionNonce": 4002002003,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
200,
|
||||
0
|
||||
]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": null
|
||||
},
|
||||
{
|
||||
"id": "attributes-1",
|
||||
"type": "text",
|
||||
"x": 110,
|
||||
"y": 155,
|
||||
"width": 180,
|
||||
"height": 50,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a3",
|
||||
"roundness": null,
|
||||
"seed": 3001001004,
|
||||
"version": 1,
|
||||
"versionNonce": 4002002004,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "- id: number\n- name: string\n- email: string",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"containerId": null,
|
||||
"originalText": "- id: number\n- name: string\n- email: string",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.1904761904761905
|
||||
},
|
||||
{
|
||||
"id": "separator-2",
|
||||
"type": "line",
|
||||
"x": 100,
|
||||
"y": 215,
|
||||
"width": 200,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a4",
|
||||
"roundness": null,
|
||||
"seed": 3001001005,
|
||||
"version": 1,
|
||||
"versionNonce": 4002002005,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
200,
|
||||
0
|
||||
]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": null
|
||||
},
|
||||
{
|
||||
"id": "methods-1",
|
||||
"type": "text",
|
||||
"x": 110,
|
||||
"y": 225,
|
||||
"width": 180,
|
||||
"height": 45,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a5",
|
||||
"roundness": null,
|
||||
"seed": 3001001006,
|
||||
"version": 3,
|
||||
"versionNonce": 1660402375,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1769755991910,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "+ login(): void\n+ logout(): void\n+ updateProfile(): void",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"containerId": null,
|
||||
"originalText": "+ login(): void\n+ logout(): void\n+ updateProfile(): void",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.0714285714285714
|
||||
},
|
||||
{
|
||||
"id": "class-2",
|
||||
"type": "rectangle",
|
||||
"x": 400,
|
||||
"y": 100,
|
||||
"width": 200,
|
||||
"height": 180,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#fff3bf",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a6",
|
||||
"roundness": null,
|
||||
"seed": 3001001007,
|
||||
"version": 1,
|
||||
"versionNonce": 4002002007,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "class-name-2",
|
||||
"type": "text",
|
||||
"x": 430,
|
||||
"y": 110,
|
||||
"width": 140,
|
||||
"height": 25,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a7",
|
||||
"roundness": null,
|
||||
"seed": 3001001008,
|
||||
"version": 1,
|
||||
"versionNonce": 4002002008,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "AdminUser",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top",
|
||||
"containerId": null,
|
||||
"originalText": "AdminUser",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "separator-3",
|
||||
"type": "line",
|
||||
"x": 400,
|
||||
"y": 145,
|
||||
"width": 200,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a8",
|
||||
"roundness": null,
|
||||
"seed": 3001001009,
|
||||
"version": 1,
|
||||
"versionNonce": 4002002009,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
200,
|
||||
0
|
||||
]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": null
|
||||
},
|
||||
{
|
||||
"id": "attributes-2",
|
||||
"type": "text",
|
||||
"x": 410,
|
||||
"y": 155,
|
||||
"width": 180,
|
||||
"height": 35,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a9",
|
||||
"roundness": null,
|
||||
"seed": 3001001010,
|
||||
"version": 1,
|
||||
"versionNonce": 4002002010,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "- role: string\n- permissions: string[]",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"containerId": null,
|
||||
"originalText": "- role: string\n- permissions: string[]",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "separator-4",
|
||||
"type": "line",
|
||||
"x": 400,
|
||||
"y": 200,
|
||||
"width": 200,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "aA",
|
||||
"roundness": null,
|
||||
"seed": 3001001011,
|
||||
"version": 2,
|
||||
"versionNonce": 873024679,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1769755880046,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
200,
|
||||
0
|
||||
]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": null
|
||||
},
|
||||
{
|
||||
"id": "methods-2",
|
||||
"type": "text",
|
||||
"x": 410,
|
||||
"y": 210,
|
||||
"width": 180,
|
||||
"height": 60,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "aB",
|
||||
"roundness": null,
|
||||
"seed": 3001001012,
|
||||
"version": 2,
|
||||
"versionNonce": 1702655305,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1769755880046,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "+ manageUsers(): void\n+ assignRole(): void\n+ revokePermission(): void",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"containerId": null,
|
||||
"originalText": "+ manageUsers(): void\n+ assignRole(): void\n+ revokePermission(): void",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.4285714285714286
|
||||
},
|
||||
{
|
||||
"id": "inheritance-line",
|
||||
"type": "line",
|
||||
"x": 400,
|
||||
"y": 190,
|
||||
"width": 100,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "aC",
|
||||
"roundness": null,
|
||||
"seed": 3001001013,
|
||||
"version": 18,
|
||||
"versionNonce": 1139021225,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1769755989350,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-100,
|
||||
0
|
||||
]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": null
|
||||
},
|
||||
{
|
||||
"id": "inheritance-triangle",
|
||||
"type": "line",
|
||||
"x": 314.1999816894531,
|
||||
"y": 181.5,
|
||||
"width": 15,
|
||||
"height": 15,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "aD",
|
||||
"roundness": null,
|
||||
"seed": 3001001014,
|
||||
"version": 21,
|
||||
"versionNonce": 1468657767,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1769756005117,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-15,
|
||||
15
|
||||
],
|
||||
[
|
||||
0,
|
||||
15
|
||||
],
|
||||
[
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": null
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"gridModeEnabled": false,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"id": "external-entity-1",
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 200,
|
||||
"width": 120,
|
||||
"height": 80,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffc9c9",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a0",
|
||||
"roundness": { "type": 3 },
|
||||
"seed": 1001001001,
|
||||
"version": 1,
|
||||
"versionNonce": 2002002002,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "User",
|
||||
"fontSize": 18,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "data-flow-1",
|
||||
"type": "arrow",
|
||||
"x": 220,
|
||||
"y": 240,
|
||||
"width": 80,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a1",
|
||||
"roundness": { "type": 2 },
|
||||
"seed": 1001001002,
|
||||
"version": 1,
|
||||
"versionNonce": 2002002003,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[80, 0]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "flow-label-1",
|
||||
"type": "text",
|
||||
"x": 230,
|
||||
"y": 220,
|
||||
"width": 80,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a2",
|
||||
"roundness": null,
|
||||
"seed": 1001001003,
|
||||
"version": 1,
|
||||
"versionNonce": 2002002004,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "input data",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "process-1",
|
||||
"type": "ellipse",
|
||||
"x": 300,
|
||||
"y": 200,
|
||||
"width": 120,
|
||||
"height": 80,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#a5d8ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a3",
|
||||
"roundness": null,
|
||||
"seed": 1001001004,
|
||||
"version": 1,
|
||||
"versionNonce": 2002002005,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Process\nData",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "data-flow-2",
|
||||
"type": "arrow",
|
||||
"x": 420,
|
||||
"y": 240,
|
||||
"width": 80,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a4",
|
||||
"roundness": { "type": 2 },
|
||||
"seed": 1001001005,
|
||||
"version": 1,
|
||||
"versionNonce": 2002002006,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[80, 0]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "flow-label-2",
|
||||
"type": "text",
|
||||
"x": 425,
|
||||
"y": 220,
|
||||
"width": 100,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a5",
|
||||
"roundness": null,
|
||||
"seed": 1001001006,
|
||||
"version": 1,
|
||||
"versionNonce": 2002002007,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "processed data",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "data-store-1",
|
||||
"type": "rectangle",
|
||||
"x": 500,
|
||||
"y": 200,
|
||||
"width": 150,
|
||||
"height": 80,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#96f2d7",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a6",
|
||||
"roundness": null,
|
||||
"seed": 1001001007,
|
||||
"version": 1,
|
||||
"versionNonce": 2002002008,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Data Store\n(Database)",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "data-store-line",
|
||||
"type": "line",
|
||||
"x": 500,
|
||||
"y": 225,
|
||||
"width": 150,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a7",
|
||||
"roundness": null,
|
||||
"seed": 1001001008,
|
||||
"version": 1,
|
||||
"versionNonce": 2002002009,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[150, 0]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"gridSize": 20
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,662 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"id": "entity-1",
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 150,
|
||||
"width": 180,
|
||||
"height": 150,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a0",
|
||||
"roundness": null,
|
||||
"seed": 5001001001,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002001,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "entity-name-1",
|
||||
"type": "text",
|
||||
"x": 150,
|
||||
"y": 160,
|
||||
"width": 80,
|
||||
"height": 25,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a1",
|
||||
"roundness": null,
|
||||
"seed": 5001001002,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002002,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "User",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "entity-separator-1",
|
||||
"type": "line",
|
||||
"x": 100,
|
||||
"y": 195,
|
||||
"width": 180,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a2",
|
||||
"roundness": null,
|
||||
"seed": 5001001003,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002003,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[180, 0]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "attributes-1",
|
||||
"type": "text",
|
||||
"x": 110,
|
||||
"y": 205,
|
||||
"width": 160,
|
||||
"height": 80,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a3",
|
||||
"roundness": null,
|
||||
"seed": 5001001004,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002004,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "PK: user_id\nname\nemail\ncreated_at",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "entity-2",
|
||||
"type": "rectangle",
|
||||
"x": 450,
|
||||
"y": 150,
|
||||
"width": 180,
|
||||
"height": 150,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#fff3bf",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a4",
|
||||
"roundness": null,
|
||||
"seed": 5001001005,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002005,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "entity-name-2",
|
||||
"type": "text",
|
||||
"x": 500,
|
||||
"y": 160,
|
||||
"width": 80,
|
||||
"height": 25,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a5",
|
||||
"roundness": null,
|
||||
"seed": 5001001006,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002006,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Order",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "entity-separator-2",
|
||||
"type": "line",
|
||||
"x": 450,
|
||||
"y": 195,
|
||||
"width": 180,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a6",
|
||||
"roundness": null,
|
||||
"seed": 5001001007,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002007,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[180, 0]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "attributes-2",
|
||||
"type": "text",
|
||||
"x": 460,
|
||||
"y": 205,
|
||||
"width": 160,
|
||||
"height": 80,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a7",
|
||||
"roundness": null,
|
||||
"seed": 5001001008,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002008,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "PK: order_id\nFK: user_id\ntotal_amount\norder_date",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "relationship-line",
|
||||
"type": "line",
|
||||
"x": 280,
|
||||
"y": 225,
|
||||
"width": 170,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a8",
|
||||
"roundness": null,
|
||||
"seed": 5001001009,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002009,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[170, 0]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "cardinality-1",
|
||||
"type": "text",
|
||||
"x": 290,
|
||||
"y": 205,
|
||||
"width": 20,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a9",
|
||||
"roundness": null,
|
||||
"seed": 5001001010,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002010,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "1",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "cardinality-2",
|
||||
"type": "text",
|
||||
"x": 420,
|
||||
"y": 205,
|
||||
"width": 20,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a10",
|
||||
"roundness": null,
|
||||
"seed": 5001001011,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002011,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "N",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "relationship-label",
|
||||
"type": "text",
|
||||
"x": 330,
|
||||
"y": 200,
|
||||
"width": 80,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a11",
|
||||
"roundness": null,
|
||||
"seed": 5001001012,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002012,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "places",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "entity-3",
|
||||
"type": "rectangle",
|
||||
"x": 450,
|
||||
"y": 380,
|
||||
"width": 180,
|
||||
"height": 120,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#d0f0c0",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a12",
|
||||
"roundness": null,
|
||||
"seed": 5001001013,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002013,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "entity-name-3",
|
||||
"type": "text",
|
||||
"x": 480,
|
||||
"y": 390,
|
||||
"width": 120,
|
||||
"height": 25,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a13",
|
||||
"roundness": null,
|
||||
"seed": 5001001014,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002014,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Product",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "entity-separator-3",
|
||||
"type": "line",
|
||||
"x": 450,
|
||||
"y": 425,
|
||||
"width": 180,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a14",
|
||||
"roundness": null,
|
||||
"seed": 5001001015,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002015,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[180, 0]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "attributes-3",
|
||||
"type": "text",
|
||||
"x": 460,
|
||||
"y": 435,
|
||||
"width": 160,
|
||||
"height": 50,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a15",
|
||||
"roundness": null,
|
||||
"seed": 5001001016,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002016,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "PK: product_id\nname\nprice",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "relationship-line-2",
|
||||
"type": "line",
|
||||
"x": 540,
|
||||
"y": 300,
|
||||
"width": 0,
|
||||
"height": 80,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a16",
|
||||
"roundness": null,
|
||||
"seed": 5001001017,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002017,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[0, 80]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "cardinality-3",
|
||||
"type": "text",
|
||||
"x": 550,
|
||||
"y": 310,
|
||||
"width": 20,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a17",
|
||||
"roundness": null,
|
||||
"seed": 5001001018,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002018,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "N",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "cardinality-4",
|
||||
"type": "text",
|
||||
"x": 550,
|
||||
"y": 350,
|
||||
"width": 20,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a18",
|
||||
"roundness": null,
|
||||
"seed": 5001001019,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002019,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "M",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "relationship-label-2",
|
||||
"type": "text",
|
||||
"x": 490,
|
||||
"y": 330,
|
||||
"width": 80,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffffff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a19",
|
||||
"roundness": null,
|
||||
"seed": 5001001020,
|
||||
"version": 1,
|
||||
"versionNonce": 6002002020,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "contains",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "top"
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"gridSize": 20
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"id": "step1",
|
||||
"type": "rectangle",
|
||||
"x": 400,
|
||||
"y": 200,
|
||||
"width": 200,
|
||||
"height": 80,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#b2f2bb",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a0",
|
||||
"roundness": { "type": 3 },
|
||||
"seed": 1234567890,
|
||||
"version": 1,
|
||||
"versionNonce": 987654321,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Step 1",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "arrow1",
|
||||
"type": "arrow",
|
||||
"x": 500,
|
||||
"y": 280,
|
||||
"width": 0,
|
||||
"height": 100,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a1",
|
||||
"roundness": { "type": 2 },
|
||||
"seed": 1234567891,
|
||||
"version": 1,
|
||||
"versionNonce": 987654322,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[0, 100]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "step2",
|
||||
"type": "rectangle",
|
||||
"x": 400,
|
||||
"y": 380,
|
||||
"width": 200,
|
||||
"height": 80,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#b2f2bb",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a2",
|
||||
"roundness": { "type": 3 },
|
||||
"seed": 1234567892,
|
||||
"version": 1,
|
||||
"versionNonce": 987654323,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Step 2",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "arrow2",
|
||||
"type": "arrow",
|
||||
"x": 500,
|
||||
"y": 460,
|
||||
"width": 0,
|
||||
"height": 100,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a3",
|
||||
"roundness": { "type": 2 },
|
||||
"seed": 1234567893,
|
||||
"version": 1,
|
||||
"versionNonce": 987654324,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[0, 100]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "step3",
|
||||
"type": "rectangle",
|
||||
"x": 400,
|
||||
"y": 560,
|
||||
"width": 200,
|
||||
"height": 80,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#b2f2bb",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a4",
|
||||
"roundness": { "type": 3 },
|
||||
"seed": 1234567894,
|
||||
"version": 1,
|
||||
"versionNonce": 987654325,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Step 3",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"gridSize": 20
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor",
|
||||
"elements": [
|
||||
{
|
||||
"id": "center",
|
||||
"type": "rectangle",
|
||||
"x": 500,
|
||||
"y": 350,
|
||||
"width": 200,
|
||||
"height": 100,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffd43b",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a0",
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 3333333333,
|
||||
"version": 3,
|
||||
"versionNonce": 641024845,
|
||||
"isDeleted": false,
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "arrow1",
|
||||
"type": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "arrow2",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1769755916717,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Central Topic",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "branch1",
|
||||
"type": "rectangle",
|
||||
"x": 250,
|
||||
"y": 150,
|
||||
"width": 150,
|
||||
"height": 80,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#96f2d7",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a1",
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 3333333334,
|
||||
"version": 2,
|
||||
"versionNonce": 2040232045,
|
||||
"isDeleted": false,
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "arrow1",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1769755912840,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Branch 1",
|
||||
"fontSize": 18,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "arrow1",
|
||||
"type": "arrow",
|
||||
"x": 600,
|
||||
"y": 350,
|
||||
"width": 246.39999389648438,
|
||||
"height": 111.20001220703125,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a2",
|
||||
"roundness": {
|
||||
"type": 2
|
||||
},
|
||||
"seed": 3333333335,
|
||||
"version": 23,
|
||||
"versionNonce": 308894189,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1769755914127,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-246.39999389648438,
|
||||
-111.20001220703125
|
||||
]
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "center",
|
||||
"focus": 0.5255972360761778,
|
||||
"gap": 1
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "branch1",
|
||||
"focus": 0.48604063201707415,
|
||||
"gap": 8.79998779296875
|
||||
},
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "branch2",
|
||||
"type": "rectangle",
|
||||
"x": 750,
|
||||
"y": 150,
|
||||
"width": 150,
|
||||
"height": 80,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#96f2d7",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a3",
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 3333333336,
|
||||
"version": 2,
|
||||
"versionNonce": 1459929741,
|
||||
"isDeleted": false,
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "arrow2",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1769755916716,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Branch 2",
|
||||
"fontSize": 18,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "arrow2",
|
||||
"type": "arrow",
|
||||
"x": 600,
|
||||
"y": 350,
|
||||
"width": 216,
|
||||
"height": 112.80001831054688,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a4",
|
||||
"roundness": {
|
||||
"type": 2
|
||||
},
|
||||
"seed": 3333333337,
|
||||
"version": 41,
|
||||
"versionNonce": 1447859213,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1769756030188,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
216,
|
||||
-112.80001831054688
|
||||
]
|
||||
],
|
||||
"startBinding": {
|
||||
"elementId": "center",
|
||||
"focus": -0.48913039421990545,
|
||||
"gap": 1
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "branch2",
|
||||
"focus": -0.5368418212214556,
|
||||
"gap": 7.199981689453125
|
||||
},
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"gridModeEnabled": false,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"id": "entity1",
|
||||
"type": "rectangle",
|
||||
"x": 300,
|
||||
"y": 300,
|
||||
"width": 180,
|
||||
"height": 100,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#a5d8ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a0",
|
||||
"roundness": { "type": 3 },
|
||||
"seed": 1111111111,
|
||||
"version": 1,
|
||||
"versionNonce": 2222222222,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Entity A",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "entity2",
|
||||
"type": "rectangle",
|
||||
"x": 600,
|
||||
"y": 300,
|
||||
"width": 180,
|
||||
"height": 100,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#a5d8ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a1",
|
||||
"roundness": { "type": 3 },
|
||||
"seed": 1111111112,
|
||||
"version": 1,
|
||||
"versionNonce": 2222222223,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Entity B",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "relationship",
|
||||
"type": "arrow",
|
||||
"x": 480,
|
||||
"y": 350,
|
||||
"width": 120,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a2",
|
||||
"roundness": { "type": 2 },
|
||||
"seed": 1111111113,
|
||||
"version": 1,
|
||||
"versionNonce": 2222222224,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[120, 0]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "label",
|
||||
"type": "text",
|
||||
"x": 510,
|
||||
"y": 325,
|
||||
"width": 60,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a3",
|
||||
"roundness": null,
|
||||
"seed": 1111111114,
|
||||
"version": 1,
|
||||
"versionNonce": 2222222225,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "relates to",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"gridSize": 20
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"id": "object-1",
|
||||
"type": "rectangle",
|
||||
"x": 150,
|
||||
"y": 100,
|
||||
"width": 120,
|
||||
"height": 50,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a0",
|
||||
"roundness": null,
|
||||
"seed": 4001001001,
|
||||
"version": 1,
|
||||
"versionNonce": 5002002001,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Client",
|
||||
"fontSize": 18,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "lifeline-1",
|
||||
"type": "line",
|
||||
"x": 210,
|
||||
"y": 150,
|
||||
"width": 0,
|
||||
"height": 300,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a1",
|
||||
"roundness": null,
|
||||
"seed": 4001001002,
|
||||
"version": 1,
|
||||
"versionNonce": 5002002002,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[0, 300]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "object-2",
|
||||
"type": "rectangle",
|
||||
"x": 350,
|
||||
"y": 100,
|
||||
"width": 120,
|
||||
"height": 50,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#fff3bf",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a2",
|
||||
"roundness": null,
|
||||
"seed": 4001001003,
|
||||
"version": 1,
|
||||
"versionNonce": 5002002003,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Server",
|
||||
"fontSize": 18,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "lifeline-2",
|
||||
"type": "line",
|
||||
"x": 410,
|
||||
"y": 150,
|
||||
"width": 0,
|
||||
"height": 300,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a3",
|
||||
"roundness": null,
|
||||
"seed": 4001001004,
|
||||
"version": 1,
|
||||
"versionNonce": 5002002004,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[0, 300]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "object-3",
|
||||
"type": "rectangle",
|
||||
"x": 550,
|
||||
"y": 100,
|
||||
"width": 120,
|
||||
"height": 50,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#d0f0c0",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a4",
|
||||
"roundness": null,
|
||||
"seed": 4001001005,
|
||||
"version": 1,
|
||||
"versionNonce": 5002002005,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Database",
|
||||
"fontSize": 18,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"id": "lifeline-3",
|
||||
"type": "line",
|
||||
"x": 610,
|
||||
"y": 150,
|
||||
"width": 0,
|
||||
"height": 300,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a5",
|
||||
"roundness": null,
|
||||
"seed": 4001001006,
|
||||
"version": 1,
|
||||
"versionNonce": 5002002006,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[0, 300]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "message-1",
|
||||
"type": "arrow",
|
||||
"x": 210,
|
||||
"y": 200,
|
||||
"width": 200,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a6",
|
||||
"roundness": { "type": 2 },
|
||||
"seed": 4001001007,
|
||||
"version": 1,
|
||||
"versionNonce": 5002002007,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[200, 0]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "message-label-1",
|
||||
"type": "text",
|
||||
"x": 250,
|
||||
"y": 180,
|
||||
"width": 120,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a7",
|
||||
"roundness": null,
|
||||
"seed": 4001001008,
|
||||
"version": 1,
|
||||
"versionNonce": 5002002008,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "1: request()",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "activation-1",
|
||||
"type": "rectangle",
|
||||
"x": 405,
|
||||
"y": 200,
|
||||
"width": 10,
|
||||
"height": 80,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "#ffd43b",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a8",
|
||||
"roundness": null,
|
||||
"seed": 4001001009,
|
||||
"version": 1,
|
||||
"versionNonce": 5002002009,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "message-2",
|
||||
"type": "arrow",
|
||||
"x": 415,
|
||||
"y": 230,
|
||||
"width": 195,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a9",
|
||||
"roundness": { "type": 2 },
|
||||
"seed": 4001001010,
|
||||
"version": 1,
|
||||
"versionNonce": 5002002010,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[195, 0]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "message-label-2",
|
||||
"type": "text",
|
||||
"x": 450,
|
||||
"y": 210,
|
||||
"width": 120,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a10",
|
||||
"roundness": null,
|
||||
"seed": 4001001011,
|
||||
"version": 1,
|
||||
"versionNonce": 5002002011,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "2: query()",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "return-message-1",
|
||||
"type": "arrow",
|
||||
"x": 610,
|
||||
"y": 250,
|
||||
"width": 195,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a11",
|
||||
"roundness": { "type": 2 },
|
||||
"seed": 4001001012,
|
||||
"version": 1,
|
||||
"versionNonce": 5002002012,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[-195, 0]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "return-label-1",
|
||||
"type": "text",
|
||||
"x": 450,
|
||||
"y": 255,
|
||||
"width": 120,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a12",
|
||||
"roundness": null,
|
||||
"seed": 4001001013,
|
||||
"version": 1,
|
||||
"versionNonce": 5002002013,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "3: result",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"id": "return-message-2",
|
||||
"type": "arrow",
|
||||
"x": 410,
|
||||
"y": 280,
|
||||
"width": 200,
|
||||
"height": 0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a13",
|
||||
"roundness": { "type": 2 },
|
||||
"seed": 4001001014,
|
||||
"version": 1,
|
||||
"versionNonce": 5002002014,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[0, 0],
|
||||
[-200, 0]
|
||||
],
|
||||
"startBinding": null,
|
||||
"endBinding": null
|
||||
},
|
||||
{
|
||||
"id": "return-label-2",
|
||||
"type": "text",
|
||||
"x": 250,
|
||||
"y": 285,
|
||||
"width": 120,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a14",
|
||||
"roundness": null,
|
||||
"seed": 4001001015,
|
||||
"version": 1,
|
||||
"versionNonce": 5002002015,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1706659200000,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "4: response",
|
||||
"fontSize": 14,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"gridSize": 20
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
342
skills/penpot-uiux-design/SKILL.md
Normal file
342
skills/penpot-uiux-design/SKILL.md
Normal file
@@ -0,0 +1,342 @@
|
||||
---
|
||||
name: penpot-uiux-design
|
||||
description: 'Comprehensive guide for creating professional UI/UX designs in Penpot using MCP tools. Use this skill when: (1) Creating new UI/UX designs for web, mobile, or desktop applications, (2) Building design systems with components and tokens, (3) Designing dashboards, forms, navigation, or landing pages, (4) Applying accessibility standards and best practices, (5) Following platform guidelines (iOS, Android, Material Design), (6) Reviewing or improving existing Penpot designs for usability. Triggers: "design a UI", "create interface", "build layout", "design dashboard", "create form", "design landing page", "make it accessible", "design system", "component library".'
|
||||
---
|
||||
|
||||
# Penpot UI/UX Design Guide
|
||||
|
||||
Create professional, user-centered designs in Penpot using the `penpot/penpot-mcp` MCP server and proven UI/UX principles.
|
||||
|
||||
## Available MCP Tools
|
||||
|
||||
| Tool | Purpose |
|
||||
| ---- | ------- |
|
||||
| `mcp__penpot__execute_code` | Run JavaScript in Penpot plugin context to create/modify designs |
|
||||
| `mcp__penpot__export_shape` | Export shapes as PNG/SVG for visual inspection |
|
||||
| `mcp__penpot__import_image` | Import images (icons, photos, logos) into designs |
|
||||
| `mcp__penpot__penpot_api_info` | Retrieve Penpot API documentation |
|
||||
|
||||
## MCP Server Setup
|
||||
|
||||
The Penpot MCP tools require the `penpot/penpot-mcp` server running locally. For detailed installation and troubleshooting, see [setup-troubleshooting.md](references/setup-troubleshooting.md).
|
||||
|
||||
### Before Setup: Check If Already Running
|
||||
|
||||
**Always check if the MCP server is already available before attempting setup:**
|
||||
|
||||
1. **Try calling a tool first**: Attempt `mcp__penpot__penpot_api_info` - if it succeeds, the server is running and connected. No setup needed.
|
||||
|
||||
2. **If the tool fails**, ask the user:
|
||||
> "The Penpot MCP server doesn't appear to be connected. Is the server already installed and running? If so, I can help troubleshoot. If not, I can guide you through the setup."
|
||||
|
||||
3. **Only proceed with setup instructions if the user confirms the server is not installed.**
|
||||
|
||||
### Quick Start (Only If Not Installed)
|
||||
|
||||
```bash
|
||||
# Clone and install
|
||||
git clone https://github.com/penpot/penpot-mcp.git
|
||||
cd penpot-mcp
|
||||
npm install
|
||||
|
||||
# Build and start servers
|
||||
npm run bootstrap
|
||||
```
|
||||
|
||||
Then in Penpot:
|
||||
1. Open a design file
|
||||
2. Go to **Plugins** → **Load plugin from URL**
|
||||
3. Enter: `http://localhost:4400/manifest.json`
|
||||
4. Click **"Connect to MCP server"** in the plugin UI
|
||||
|
||||
### VS Code Configuration
|
||||
|
||||
Add to `settings.json`:
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"penpot": {
|
||||
"url": "http://localhost:4401/sse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Troubleshooting (If Server Is Installed But Not Working)
|
||||
|
||||
| Issue | Solution |
|
||||
| ----- | -------- |
|
||||
| Plugin won't connect | Check servers are running (`npm run start:all` in penpot-mcp dir) |
|
||||
| Browser blocks localhost | Allow local network access prompt, or disable Brave Shield, or try Firefox |
|
||||
| Tools not appearing in client | Restart VS Code/Claude completely after config changes |
|
||||
| Tool execution fails/times out | Ensure Penpot plugin UI is open and shows "Connected" |
|
||||
| "WebSocket connection failed" | Check firewall allows ports 4400, 4401, 4402 |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Reference File |
|
||||
| ---- | -------------- |
|
||||
| MCP server installation & troubleshooting | [setup-troubleshooting.md](references/setup-troubleshooting.md) |
|
||||
| Component specs (buttons, forms, nav) | [component-patterns.md](references/component-patterns.md) |
|
||||
| Accessibility (contrast, touch targets) | [accessibility.md](references/accessibility.md) |
|
||||
| Screen sizes & platform specs | [platform-guidelines.md](references/platform-guidelines.md) |
|
||||
|
||||
## Core Design Principles
|
||||
|
||||
### The Golden Rules
|
||||
|
||||
1. **Clarity over cleverness**: Every element must have a purpose
|
||||
2. **Consistency builds trust**: Reuse patterns, colors, and components
|
||||
3. **User goals first**: Design for tasks, not features
|
||||
4. **Accessibility is not optional**: Design for everyone
|
||||
5. **Test with real users**: Validate assumptions early
|
||||
|
||||
### Visual Hierarchy (Priority Order)
|
||||
|
||||
1. **Size**: Larger = more important
|
||||
2. **Color/Contrast**: High contrast draws attention
|
||||
3. **Position**: Top-left (LTR) gets seen first
|
||||
4. **Whitespace**: Isolation emphasizes importance
|
||||
5. **Typography weight**: Bold stands out
|
||||
|
||||
## Design Workflow
|
||||
|
||||
1. **Check for design system first**: Ask user if they have existing tokens/specs, or discover from current Penpot file
|
||||
2. **Understand the page**: Call `mcp__penpot__execute_code` with `penpotUtils.shapeStructure()` to see hierarchy
|
||||
3. **Find elements**: Use `penpotUtils.findShapes()` to locate elements by type or name
|
||||
4. **Create/modify**: Use `penpot.createBoard()`, `penpot.createRectangle()`, `penpot.createText()` etc.
|
||||
5. **Apply layout**: Use `addFlexLayout()` for responsive containers
|
||||
6. **Validate**: Call `mcp__penpot__export_shape` to visually check your work
|
||||
|
||||
## Design System Handling
|
||||
|
||||
**Before creating designs, determine if the user has an existing design system:**
|
||||
|
||||
1. **Ask the user**: "Do you have a design system or brand guidelines to follow?"
|
||||
2. **Discover from Penpot**: Check for existing components, colors, and patterns
|
||||
|
||||
```javascript
|
||||
// Discover existing design patterns in current file
|
||||
const allShapes = penpotUtils.findShapes(() => true, penpot.root);
|
||||
|
||||
// Find existing colors in use
|
||||
const colors = new Set();
|
||||
allShapes.forEach(s => {
|
||||
if (s.fills) s.fills.forEach(f => colors.add(f.fillColor));
|
||||
});
|
||||
|
||||
// Find existing text styles (font sizes, weights)
|
||||
const textStyles = allShapes
|
||||
.filter(s => s.type === 'text')
|
||||
.map(s => ({ fontSize: s.fontSize, fontWeight: s.fontWeight }));
|
||||
|
||||
// Find existing components
|
||||
const components = penpot.library.local.components;
|
||||
|
||||
return { colors: [...colors], textStyles, componentCount: components.length };
|
||||
```
|
||||
|
||||
**If user HAS a design system:**
|
||||
|
||||
- Use their specified colors, spacing, typography
|
||||
- Match their existing component patterns
|
||||
- Follow their naming conventions
|
||||
|
||||
**If user has NO design system:**
|
||||
|
||||
- Use the default tokens below as a starting point
|
||||
- Offer to help establish consistent patterns
|
||||
- Reference specs in [component-patterns.md](references/component-patterns.md)
|
||||
|
||||
## Key Penpot API Gotchas
|
||||
|
||||
- `width`/`height` are READ-ONLY → use `shape.resize(w, h)`
|
||||
- `parentX`/`parentY` are READ-ONLY → use `penpotUtils.setParentXY(shape, x, y)`
|
||||
- Use `insertChild(index, shape)` for z-ordering (not `appendChild`)
|
||||
- Flex children array order is REVERSED for `dir="column"` or `dir="row"`
|
||||
- After `text.resize()`, reset `growType` to `"auto-width"` or `"auto-height"`
|
||||
|
||||
## Positioning New Boards
|
||||
|
||||
**Always check existing boards before creating new ones** to avoid overlap:
|
||||
|
||||
```javascript
|
||||
// Find all existing boards and calculate next position
|
||||
const boards = penpotUtils.findShapes(s => s.type === 'board', penpot.root);
|
||||
let nextX = 0;
|
||||
const gap = 100; // Space between boards
|
||||
|
||||
if (boards.length > 0) {
|
||||
// Find rightmost board edge
|
||||
boards.forEach(b => {
|
||||
const rightEdge = b.x + b.width;
|
||||
if (rightEdge + gap > nextX) {
|
||||
nextX = rightEdge + gap;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create new board at calculated position
|
||||
const newBoard = penpot.createBoard();
|
||||
newBoard.x = nextX;
|
||||
newBoard.y = 0;
|
||||
newBoard.resize(375, 812);
|
||||
```
|
||||
|
||||
**Board spacing guidelines:**
|
||||
|
||||
- Use 100px gap between related screens (same flow)
|
||||
- Use 200px+ gap between different sections/flows
|
||||
- Align boards vertically (same y) for visual organization
|
||||
- Group related screens horizontally in user flow order
|
||||
|
||||
## Default Design Tokens
|
||||
|
||||
**Use these defaults only when user has no design system. Always prefer user's tokens if available.**
|
||||
|
||||
### Spacing Scale (8px base)
|
||||
|
||||
| Token | Value | Usage |
|
||||
| ----- | ----- | ----- |
|
||||
| `spacing-xs` | 4px | Tight inline elements |
|
||||
| `spacing-sm` | 8px | Related elements |
|
||||
| `spacing-md` | 16px | Default padding |
|
||||
| `spacing-lg` | 24px | Section spacing |
|
||||
| `spacing-xl` | 32px | Major sections |
|
||||
| `spacing-2xl` | 48px | Page-level spacing |
|
||||
|
||||
### Typography Scale
|
||||
|
||||
| Level | Size | Weight | Usage |
|
||||
| ----- | ---- | ------ | ----- |
|
||||
| Display | 48-64px | Bold | Hero headlines |
|
||||
| H1 | 32-40px | Bold | Page titles |
|
||||
| H2 | 24-28px | Semibold | Section headers |
|
||||
| H3 | 20-22px | Semibold | Subsections |
|
||||
| Body | 16px | Regular | Main content |
|
||||
| Small | 14px | Regular | Secondary text |
|
||||
| Caption | 12px | Regular | Labels, hints |
|
||||
|
||||
### Color Usage
|
||||
|
||||
| Purpose | Recommendation |
|
||||
| ------- | -------------- |
|
||||
| Primary | Main brand color, CTAs |
|
||||
| Secondary | Supporting actions |
|
||||
| Success | #22C55E range (confirmations) |
|
||||
| Warning | #F59E0B range (caution) |
|
||||
| Error | #EF4444 range (errors) |
|
||||
| Neutral | Gray scale for text/borders |
|
||||
|
||||
## Common Layouts
|
||||
|
||||
### Mobile Screen (375×812)
|
||||
|
||||
```text
|
||||
┌─────────────────────────────┐
|
||||
│ Status Bar (44px) │
|
||||
├─────────────────────────────┤
|
||||
│ Header/Nav (56px) │
|
||||
├─────────────────────────────┤
|
||||
│ │
|
||||
│ Content Area │
|
||||
│ (Scrollable) │
|
||||
│ Padding: 16px horizontal │
|
||||
│ │
|
||||
├─────────────────────────────┤
|
||||
│ Bottom Nav/CTA (84px) │
|
||||
└─────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
### Desktop Dashboard (1440×900)
|
||||
|
||||
```text
|
||||
┌──────┬──────────────────────────────────┐
|
||||
│ │ Header (64px) │
|
||||
│ Side │──────────────────────────────────│
|
||||
│ bar │ Page Title + Actions │
|
||||
│ │──────────────────────────────────│
|
||||
│ 240 │ Content Grid │
|
||||
│ px │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ │Card │ │Card │ │Card │ │Card │ │
|
||||
│ │ └─────┘ └─────┘ └─────┘ └─────┘ │
|
||||
│ │ │
|
||||
└──────┴──────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
## Component Checklist
|
||||
|
||||
### Buttons
|
||||
|
||||
- [ ] Clear, action-oriented label (2-3 words)
|
||||
- [ ] Minimum touch target: 44×44px
|
||||
- [ ] Visual states: default, hover, active, disabled, loading
|
||||
- [ ] Sufficient contrast (3:1 against background)
|
||||
- [ ] Consistent border radius across app
|
||||
|
||||
### Forms
|
||||
|
||||
- [ ] Labels above inputs (not just placeholders)
|
||||
- [ ] Required field indicators
|
||||
- [ ] Error messages adjacent to fields
|
||||
- [ ] Logical tab order
|
||||
- [ ] Input types match content (email, tel, etc.)
|
||||
|
||||
### Navigation
|
||||
|
||||
- [ ] Current location clearly indicated
|
||||
- [ ] Consistent position across screens
|
||||
- [ ] Maximum 7±2 top-level items
|
||||
- [ ] Touch-friendly on mobile (48px targets)
|
||||
|
||||
## Accessibility Quick Checks
|
||||
|
||||
1. **Color contrast**: Text 4.5:1, Large text 3:1
|
||||
2. **Touch targets**: Minimum 44×44px
|
||||
3. **Focus states**: Visible keyboard focus indicators
|
||||
4. **Alt text**: Meaningful descriptions for images
|
||||
5. **Hierarchy**: Proper heading levels (H1→H2→H3)
|
||||
6. **Color independence**: Never rely solely on color
|
||||
|
||||
## Design Review Checklist
|
||||
|
||||
Before finalizing any design:
|
||||
|
||||
- [ ] Visual hierarchy is clear
|
||||
- [ ] Consistent spacing and alignment
|
||||
- [ ] Typography is readable (16px+ body text)
|
||||
- [ ] Color contrast meets WCAG AA
|
||||
- [ ] Interactive elements are obvious
|
||||
- [ ] Mobile-friendly touch targets
|
||||
- [ ] Loading/empty/error states considered
|
||||
- [ ] Consistent with design system
|
||||
|
||||
## Validating Designs
|
||||
|
||||
Use these validation approaches with `mcp__penpot__execute_code`:
|
||||
|
||||
| Check | Method |
|
||||
| ----- | ------ |
|
||||
| Elements outside bounds | `penpotUtils.analyzeDescendants()` with `isContainedIn()` |
|
||||
| Text too small (<12px) | `penpotUtils.findShapes()` filtering by `fontSize` |
|
||||
| Missing contrast | Call `mcp__penpot__export_shape` and visually inspect |
|
||||
| Hierarchy structure | `penpotUtils.shapeStructure()` to review nesting |
|
||||
|
||||
### Export CSS
|
||||
|
||||
Use `penpot.generateStyle(selection, { type: 'css', includeChildren: true })` via `mcp__penpot__execute_code` to extract CSS from designs.
|
||||
|
||||
## Tips for Great Designs
|
||||
|
||||
1. **Start with content**: Real content reveals layout needs
|
||||
2. **Design mobile-first**: Constraints breed creativity
|
||||
3. **Use a grid**: 8px base grid keeps things aligned
|
||||
4. **Limit colors**: 1 primary + 1 secondary + neutrals
|
||||
5. **Limit fonts**: 1-2 typefaces maximum
|
||||
6. **Embrace whitespace**: Breathing room improves comprehension
|
||||
7. **Be consistent**: Same action = same appearance everywhere
|
||||
8. **Provide feedback**: Every action needs a response
|
||||
329
skills/penpot-uiux-design/references/accessibility.md
Normal file
329
skills/penpot-uiux-design/references/accessibility.md
Normal file
@@ -0,0 +1,329 @@
|
||||
# Accessibility Guidelines Reference (WCAG)
|
||||
|
||||
## Quick Compliance Checklist
|
||||
|
||||
### Level AA Requirements (Minimum Standard)
|
||||
|
||||
- [ ] Color contrast 4.5:1 for normal text
|
||||
- [ ] Color contrast 3:1 for large text (18px+ or 14px bold)
|
||||
- [ ] Touch targets minimum 44×44px
|
||||
- [ ] All functionality available via keyboard
|
||||
- [ ] Visible focus indicators
|
||||
- [ ] No content flashes more than 3 times/second
|
||||
- [ ] Page has descriptive title
|
||||
- [ ] Link purpose clear from text
|
||||
- [ ] Form inputs have labels
|
||||
- [ ] Error messages are descriptive
|
||||
|
||||
---
|
||||
|
||||
## Color and Contrast
|
||||
|
||||
### Contrast Ratios
|
||||
|
||||
| Element | Minimum Ratio | Enhanced (AAA) |
|
||||
| ------- | ------------- | -------------- |
|
||||
| Body text | 4.5:1 | 7:1 |
|
||||
| Large text (18px+) | 3:1 | 4.5:1 |
|
||||
| UI components | 3:1 | - |
|
||||
| Graphical objects | 3:1 | - |
|
||||
|
||||
### Color Independence
|
||||
|
||||
Never use color as the only means of conveying information:
|
||||
|
||||
```text
|
||||
✗ Error fields shown only in red
|
||||
✓ Error fields with red border + error icon + text message
|
||||
|
||||
✗ Required fields marked only with red asterisk
|
||||
✓ Required fields labeled "(required)" or with icon + tooltip
|
||||
|
||||
✗ Status shown only by color dots
|
||||
✓ Status with color + icon + label text
|
||||
|
||||
```
|
||||
|
||||
### Accessible Color Combinations
|
||||
|
||||
**Safe text colors on backgrounds:**
|
||||
|
||||
| Background | Text Color | Contrast |
|
||||
| ---------- | ---------- | -------- |
|
||||
| White (#FFFFFF) | Dark gray (#1F2937) | 15.5:1 ✓ |
|
||||
| Light gray (#F3F4F6) | Dark gray (#374151) | 10.9:1 ✓ |
|
||||
| Primary blue (#2563EB) | White (#FFFFFF) | 4.6:1 ✓ |
|
||||
| Dark (#111827) | White (#FFFFFF) | 18.1:1 ✓ |
|
||||
|
||||
**Colors to avoid for text:**
|
||||
|
||||
- Yellow on white (insufficient contrast)
|
||||
- Light gray on white
|
||||
- Orange on white (marginal at best)
|
||||
|
||||
---
|
||||
|
||||
## Keyboard Navigation
|
||||
|
||||
### Requirements
|
||||
|
||||
1. **All interactive elements** must be reachable via Tab key
|
||||
2. **Logical tab order** following visual layout
|
||||
3. **No keyboard traps** (user can always Tab away)
|
||||
4. **Focus visible** at all times during keyboard navigation
|
||||
5. **Skip links** to bypass repetitive navigation
|
||||
|
||||
### Focus Indicators
|
||||
|
||||
```css
|
||||
/* Example focus styles */
|
||||
:focus {
|
||||
outline: 2px solid #2563EB;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
:focus:not(:focus-visible) {
|
||||
outline: none; /* Hide for mouse users */
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid #2563EB;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Key | Expected Behavior |
|
||||
| --- | ----------------- |
|
||||
| Tab | Move to next interactive element |
|
||||
| Shift+Tab | Move to previous element |
|
||||
| Enter | Activate button/link |
|
||||
| Space | Activate button, toggle checkbox |
|
||||
| Escape | Close modal/dropdown |
|
||||
| Arrow keys | Navigate within components |
|
||||
|
||||
---
|
||||
|
||||
## Screen Reader Support
|
||||
|
||||
### Semantic HTML Elements
|
||||
|
||||
Use appropriate elements for their purpose:
|
||||
|
||||
| Purpose | Element | Not This |
|
||||
| ------- | ------- | -------- |
|
||||
| Navigation | `<nav>` | `<div class="nav">` |
|
||||
| Main content | `<main>` | `<div id="main">` |
|
||||
| Header | `<header>` | `<div class="header">` |
|
||||
| Footer | `<footer>` | `<div class="footer">` |
|
||||
| Button | `<button>` | `<div onclick>` |
|
||||
| Link | `<a href>` | `<span onclick>` |
|
||||
|
||||
### Heading Hierarchy
|
||||
|
||||
```text
|
||||
h1 - Page Title (one per page)
|
||||
h2 - Major Section
|
||||
h3 - Subsection
|
||||
h4 - Sub-subsection
|
||||
h3 - Another Subsection
|
||||
h2 - Another Major Section
|
||||
|
||||
```
|
||||
|
||||
**Never skip levels** (h1 → h3 without h2)
|
||||
|
||||
### Image Alt Text
|
||||
|
||||
```text
|
||||
Decorative: alt="" (empty, not omitted)
|
||||
Informative: alt="Description of what image shows"
|
||||
Functional: alt="Action the image performs"
|
||||
Complex: alt="Brief description" + detailed description nearby
|
||||
|
||||
```
|
||||
|
||||
**Alt text examples:**
|
||||
|
||||
```text
|
||||
✓ alt="Bar chart showing sales growth from $10M to $15M in Q4"
|
||||
✓ alt="Company logo"
|
||||
✓ alt="" (for decorative background pattern)
|
||||
|
||||
✗ alt="image" or alt="photo"
|
||||
✗ alt="img_12345.jpg"
|
||||
✗ Missing alt attribute entirely
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Touch and Pointer
|
||||
|
||||
### Touch Target Sizes
|
||||
|
||||
| Platform | Minimum | Recommended |
|
||||
| -------- | ------- | ----------- |
|
||||
| WCAG 2.1 | 44×44px | 48×48px |
|
||||
| iOS (Apple) | 44×44pt | - |
|
||||
| Android | 48×48dp | - |
|
||||
|
||||
### Touch Target Spacing
|
||||
|
||||
- Minimum 8px between adjacent targets
|
||||
- Prefer 16px+ for comfort
|
||||
- Larger targets for primary actions
|
||||
|
||||
### Pointer Gestures
|
||||
|
||||
- Complex gestures must have single-pointer alternatives
|
||||
- Drag operations need equivalent click actions
|
||||
- Avoid hover-only functionality on touch devices
|
||||
|
||||
---
|
||||
|
||||
## Forms Accessibility
|
||||
|
||||
### Labels
|
||||
|
||||
Every input must have an associated label:
|
||||
|
||||
```text
|
||||
<label for="email">Email Address</label>
|
||||
<input type="email" id="email" name="email">
|
||||
|
||||
```
|
||||
|
||||
### Required Fields
|
||||
|
||||
```text
|
||||
<!-- Announce to screen readers -->
|
||||
<label for="name">
|
||||
Name <span aria-label="required">*</span>
|
||||
</label>
|
||||
<input type="text" id="name" required aria-required="true">
|
||||
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```text
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" aria-invalid="true" aria-describedby="email-error">
|
||||
<span id="email-error" role="alert">
|
||||
Please enter a valid email address
|
||||
</span>
|
||||
|
||||
```
|
||||
|
||||
### Form Instructions
|
||||
|
||||
- Provide format hints before input
|
||||
- Show password requirements before errors
|
||||
- Group related fields with fieldset/legend
|
||||
|
||||
---
|
||||
|
||||
## Dynamic Content
|
||||
|
||||
### Live Regions
|
||||
|
||||
For content that updates dynamically:
|
||||
|
||||
```text
|
||||
aria-live="polite" - Announce when convenient
|
||||
aria-live="assertive" - Announce immediately (interrupts)
|
||||
role="alert" - Urgent messages (like assertive)
|
||||
role="status" - Status updates (like polite)
|
||||
|
||||
```
|
||||
|
||||
### Loading States
|
||||
|
||||
```text
|
||||
<button aria-busy="true" aria-live="polite">
|
||||
<span class="spinner"></span>
|
||||
Loading...
|
||||
</button>
|
||||
|
||||
```
|
||||
|
||||
### Modal Dialogs
|
||||
|
||||
- Focus moves into modal when opened
|
||||
- Focus trapped within modal
|
||||
- Escape key closes modal
|
||||
- Focus returns to trigger element when closed
|
||||
|
||||
---
|
||||
|
||||
## Testing Accessibility
|
||||
|
||||
### Manual Testing Checklist
|
||||
|
||||
1. **Keyboard only:** Navigate entire page with Tab/Enter
|
||||
2. **Screen reader:** Test with VoiceOver (Mac) or NVDA (Windows)
|
||||
3. **Zoom 200%:** Content remains readable and usable
|
||||
4. **High contrast:** Test with system high contrast mode
|
||||
5. **No mouse:** Complete all tasks without pointing device
|
||||
|
||||
### Automated Tools
|
||||
|
||||
- axe DevTools (browser extension)
|
||||
- WAVE (WebAIM browser extension)
|
||||
- Lighthouse (Chrome DevTools)
|
||||
- Color contrast checkers (WebAIM, Contrast Ratio)
|
||||
|
||||
### Common Issues to Check
|
||||
|
||||
- [ ] Missing or empty alt text
|
||||
- [ ] Empty links or buttons
|
||||
- [ ] Missing form labels
|
||||
- [ ] Insufficient color contrast
|
||||
- [ ] Missing language attribute
|
||||
- [ ] Incorrect heading structure
|
||||
- [ ] Missing skip navigation link
|
||||
- [ ] Inaccessible custom widgets
|
||||
|
||||
---
|
||||
|
||||
## ARIA Quick Reference
|
||||
|
||||
### Roles
|
||||
|
||||
| Role | Purpose |
|
||||
| ---- | ------- |
|
||||
| `button` | Clickable button |
|
||||
| `link` | Navigation link |
|
||||
| `dialog` | Modal dialog |
|
||||
| `alert` | Important message |
|
||||
| `navigation` | Navigation region |
|
||||
| `main` | Main content |
|
||||
| `search` | Search functionality |
|
||||
| `tab/tablist/tabpanel` | Tab interface |
|
||||
|
||||
### Properties
|
||||
|
||||
| Property | Purpose |
|
||||
| -------- | ------- |
|
||||
| `aria-label` | Accessible name |
|
||||
| `aria-labelledby` | Reference to labeling element |
|
||||
| `aria-describedby` | Reference to description |
|
||||
| `aria-hidden` | Hide from assistive tech |
|
||||
| `aria-expanded` | Expandable state |
|
||||
| `aria-selected` | Selection state |
|
||||
| `aria-disabled` | Disabled state |
|
||||
| `aria-required` | Required field |
|
||||
| `aria-invalid` | Invalid input |
|
||||
|
||||
### Golden Rule
|
||||
|
||||
**First rule of ARIA:** Don't use ARIA if native HTML works.
|
||||
|
||||
```text
|
||||
✗ <div role="button" tabindex="0">Click</div>
|
||||
✓ <button>Click</button>
|
||||
|
||||
```
|
||||
339
skills/penpot-uiux-design/references/component-patterns.md
Normal file
339
skills/penpot-uiux-design/references/component-patterns.md
Normal file
@@ -0,0 +1,339 @@
|
||||
# UI Component Patterns Reference
|
||||
|
||||
## Buttons
|
||||
|
||||
### Button Types
|
||||
|
||||
| Type | Purpose | Visual Treatment |
|
||||
| ---- | ------- | ---------------- |
|
||||
| Primary | Main action on page | Solid fill, brand color |
|
||||
| Secondary | Supporting actions | Outline or muted fill |
|
||||
| Tertiary | Low-emphasis actions | Text-only, underline optional |
|
||||
| Destructive | Delete/remove actions | Red color, confirmation required |
|
||||
| Ghost | Minimal UI, icon buttons | Transparent, subtle hover |
|
||||
|
||||
### Button States
|
||||
|
||||
```text
|
||||
Default → Resting state, clearly interactive
|
||||
Hover → Cursor over (desktop): darken 10%, subtle shadow
|
||||
Active → Being pressed: darken 20%, slight scale down
|
||||
Focus → Keyboard selected: visible outline ring
|
||||
Disabled → Not available: 50% opacity, cursor: not-allowed
|
||||
Loading → Processing: spinner replaces or accompanies label
|
||||
|
||||
```
|
||||
|
||||
### Button Specifications
|
||||
|
||||
- **Minimum size:** 44×44px (touch target)
|
||||
- **Padding:** 12-16px horizontal, 8-12px vertical
|
||||
- **Border radius:** 4-8px (consistent across app)
|
||||
- **Font weight:** Medium or Semibold (600-700)
|
||||
- **Text:** Sentence case, 2-4 words max
|
||||
|
||||
### Button Label Patterns
|
||||
|
||||
```text
|
||||
✓ Save Changes ✗ Submit
|
||||
✓ Add to Cart ✗ Click Here
|
||||
✓ Create Account ✗ OK
|
||||
✓ Download PDF ✗ Go
|
||||
✓ Start Free Trial ✗ Continue
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Forms
|
||||
|
||||
### Form Layout Guidelines
|
||||
|
||||
- **Single column preferred:** Reduces cognitive load
|
||||
- **Top-aligned labels:** Fastest completion times
|
||||
- **Logical grouping:** Related fields together
|
||||
- **Smart defaults:** Pre-fill when possible
|
||||
|
||||
### Input Field Anatomy
|
||||
|
||||
```text
|
||||
┌─ Label (required) ─────────────────────────┐
|
||||
│ │
|
||||
│ ┌────────────────────────────────────┐ │
|
||||
│ │ Placeholder text... │ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
│ Helper text or error message │
|
||||
└────────────────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
### Input States
|
||||
|
||||
| State | Border | Background | Additional |
|
||||
| ----- | ------ | ---------- | ---------- |
|
||||
| Default | Gray (#D1D5DB) | White | - |
|
||||
| Focus | Primary color | White | Shadow/glow |
|
||||
| Filled | Gray | White | Checkmark optional |
|
||||
| Error | Red (#EF4444) | Light red tint | Error icon + message |
|
||||
| Disabled | Light gray | Gray (#F3F4F6) | 50% opacity text |
|
||||
|
||||
### Validation Timing
|
||||
|
||||
- **On blur:** Validate when user leaves field
|
||||
- **On change (after error):** Clear error as user types correct input
|
||||
- **On submit:** Final validation before processing
|
||||
- **Never on focus:** Don't show errors before user types
|
||||
|
||||
### Error Message Guidelines
|
||||
|
||||
```text
|
||||
✓ "Email address is required"
|
||||
✓ "Password must be at least 8 characters"
|
||||
✓ "Please enter a valid phone number (e.g., 555-123-4567)"
|
||||
|
||||
✗ "Invalid input"
|
||||
✗ "Error"
|
||||
✗ "This field is required" (generic)
|
||||
|
||||
```
|
||||
|
||||
### Form Best Practices
|
||||
|
||||
- Mark optional fields, not required (fewer asterisks)
|
||||
- Show password requirements before errors occur
|
||||
- Use input masks for formatted data (phone, date)
|
||||
- Preserve data on errors (don't clear the form)
|
||||
- Provide clear success confirmation
|
||||
|
||||
---
|
||||
|
||||
## Navigation
|
||||
|
||||
### Navigation Patterns
|
||||
|
||||
#### Top Navigation Bar
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Logo Nav Item Nav Item Nav Item [Search] [User] │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
- **Best for:** Marketing sites, simple apps
|
||||
- **Max items:** 5-7 top-level links
|
||||
- **Mobile:** Collapse to hamburger menu
|
||||
|
||||
#### Sidebar Navigation
|
||||
|
||||
```text
|
||||
┌────────┬────────────────────────────────┐
|
||||
│ Logo │ Content Area │
|
||||
├────────┤ │
|
||||
│ Nav 1 │ │
|
||||
│ Nav 2 │ │
|
||||
│ Nav 3 │ │
|
||||
│ │ │
|
||||
│ ────── │ │
|
||||
│ Nav 4 │ │
|
||||
│ Nav 5 │ │
|
||||
└────────┴────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
- **Best for:** Dashboards, complex apps
|
||||
- **Width:** 200-280px expanded, 64px collapsed
|
||||
- **Mobile:** Overlay drawer
|
||||
|
||||
#### Bottom Navigation (Mobile)
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────┐
|
||||
│ Content Area │
|
||||
│ │
|
||||
├─────────────────────────────────────┤
|
||||
│ 🏠 🔍 ➕ 💬 👤 │
|
||||
│ Home Search Add Chat Profile │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
- **Max items:** 3-5 destinations
|
||||
- **Best for:** Primary app sections
|
||||
- **Always visible:** Persistent navigation
|
||||
|
||||
#### Breadcrumbs
|
||||
|
||||
```text
|
||||
Home > Products > Electronics > Headphones
|
||||
|
||||
```
|
||||
|
||||
- **Use for:** Deep hierarchies (3+ levels)
|
||||
- **Current page:** Not clickable, different style
|
||||
- **Separator:** > or / or chevron icon
|
||||
|
||||
### Tab Navigation
|
||||
|
||||
```text
|
||||
┌─────────┬─────────┬─────────┬─────────┐
|
||||
│ Tab 1 │ Tab 2 │ Tab 3 │ Tab 4 │
|
||||
└─────────┴─────────┴─────────┴─────────┘
|
||||
│ │
|
||||
│ Tab Content Area │
|
||||
│ │
|
||||
└───────────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
- **Max tabs:** 3-5 for clarity
|
||||
- **Active indicator:** Underline or background
|
||||
- **Use for:** Related content within same page
|
||||
|
||||
---
|
||||
|
||||
## Cards
|
||||
|
||||
### Card Anatomy
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────┐
|
||||
│ ░░░░░░░ Image/Media ░░░░░░░░░░ │
|
||||
├─────────────────────────────────┤
|
||||
│ Category Label │
|
||||
│ Card Title │
|
||||
│ Description text that may │
|
||||
│ span multiple lines... │
|
||||
│ │
|
||||
│ [Action Button] [Secondary] │
|
||||
└─────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
### Card Guidelines
|
||||
|
||||
- **Consistent sizing:** Use grid, equal heights
|
||||
- **Content hierarchy:** Image → Title → Description → Actions
|
||||
- **Padding:** 16-24px internal spacing
|
||||
- **Border radius:** 8-12px (matching buttons)
|
||||
- **Shadow:** Subtle elevation (0 2px 4px rgba(0,0,0,0.1))
|
||||
|
||||
---
|
||||
|
||||
## Modals and Dialogs
|
||||
|
||||
### Modal Structure
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────┐
|
||||
│ Modal Title [×] │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ Modal content goes here. │
|
||||
│ Keep it focused on one task. │
|
||||
│ │
|
||||
├─────────────────────────────────────┤
|
||||
│ [Cancel] [Confirm] │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
### Modal Guidelines
|
||||
|
||||
- **Size:** 400-600px width (desktop), full-width minus margins (mobile)
|
||||
- **Overlay:** Semi-transparent dark background (rgba(0,0,0,0.5))
|
||||
- **Close options:** X button, overlay click, Escape key
|
||||
- **Focus trap:** Keep keyboard focus within modal
|
||||
- **Primary action:** Right-aligned, visually prominent
|
||||
|
||||
---
|
||||
|
||||
## Dashboards
|
||||
|
||||
### Dashboard Layout Principles
|
||||
|
||||
1. **Most important metrics at top:** KPIs, summary cards
|
||||
2. **Progressive detail:** Overview → Drill-down capability
|
||||
3. **Consistent card sizes:** Use grid system
|
||||
4. **Minimal chartjunk:** Only data-serving visuals
|
||||
5. **Actionable insights:** Highlight anomalies
|
||||
|
||||
### Data Visualization Selection
|
||||
|
||||
| Data Type | Chart Type |
|
||||
| --------- | ---------- |
|
||||
| Comparison across categories | Bar chart |
|
||||
| Trend over time | Line chart |
|
||||
| Part of whole | Pie (≤5 slices) or Donut |
|
||||
| Distribution | Histogram |
|
||||
| Correlation | Scatter plot |
|
||||
| Geographic | Map |
|
||||
| Single metric | Big number + sparkline |
|
||||
|
||||
### Dashboard Best Practices
|
||||
|
||||
- **Limit to 5-9 widgets** per view
|
||||
- **Align to grid:** Consistent gutters and sizing
|
||||
- **Filter controls:** Top or sidebar, always visible
|
||||
- **Date range selector:** Common need, make prominent
|
||||
- **Export options:** PDF, CSV for data tables
|
||||
- **Responsive:** Stack cards on smaller screens
|
||||
|
||||
---
|
||||
|
||||
## Empty States
|
||||
|
||||
### Empty State Components
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────┐
|
||||
│ │
|
||||
│ [Illustration/Icon] │
|
||||
│ │
|
||||
│ No projects yet │
|
||||
│ │
|
||||
│ Create your first project to │
|
||||
│ start organizing your work. │
|
||||
│ │
|
||||
│ [Create Project] │
|
||||
│ │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
### Empty State Guidelines
|
||||
|
||||
- **Friendly illustration:** Not just "No data"
|
||||
- **Explain value:** Why create something?
|
||||
- **Clear CTA:** Primary action to fix empty state
|
||||
- **Keep it brief:** 1-2 sentences max
|
||||
|
||||
---
|
||||
|
||||
## Loading States
|
||||
|
||||
### Loading Patterns
|
||||
|
||||
| Duration | Pattern |
|
||||
| -------- | ------- |
|
||||
| <1 second | No indicator (feels instant) |
|
||||
| 1-3 seconds | Spinner or progress indicator |
|
||||
| 3-10 seconds | Skeleton screens + progress |
|
||||
| >10 seconds | Progress bar + explanation |
|
||||
|
||||
### Skeleton Screen
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────┐
|
||||
│ ░░░░░░░░░░░░ ░░░░░░░░░░ │
|
||||
├─────────────────────────────────────┤
|
||||
│ ░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
||||
│ ░░░░░░░░░░░░░░░░░░░ │
|
||||
│ ░░░░░░░░░░░░░░░░░░░░░░░ │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
- Match layout of loaded content
|
||||
- Use subtle animation (shimmer/pulse)
|
||||
- Show actual content structure
|
||||
367
skills/penpot-uiux-design/references/platform-guidelines.md
Normal file
367
skills/penpot-uiux-design/references/platform-guidelines.md
Normal file
@@ -0,0 +1,367 @@
|
||||
# Platform Design Guidelines Reference
|
||||
|
||||
## Mobile Design Fundamentals
|
||||
|
||||
### Screen Sizes
|
||||
|
||||
| Device | Size | Design At |
|
||||
| ------ | ---- | --------- |
|
||||
| iPhone SE | 375×667 | Small mobile |
|
||||
| iPhone 14/15 | 390×844 | Standard mobile |
|
||||
| iPhone 14 Pro Max | 430×932 | Large mobile |
|
||||
| Android small | 360×640 | Minimum target |
|
||||
| Android large | 412×915 | Large Android |
|
||||
|
||||
### Safe Areas
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────┐
|
||||
│ ▓▓▓▓▓▓▓ Status Bar ▓▓▓▓▓▓▓▓▓▓ │ 44-47px
|
||||
├─────────────────────────────────┤
|
||||
│ │
|
||||
│ Safe Content Area │
|
||||
│ │
|
||||
│ │
|
||||
├─────────────────────────────────┤
|
||||
│ ▓▓▓▓▓▓ Home Indicator ▓▓▓▓▓▓▓ │ 34px
|
||||
└─────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
### Touch Targets
|
||||
|
||||
- **Minimum:** 44×44pt (iOS) / 48×48dp (Android)
|
||||
- **Recommended:** 48×48px for all platforms
|
||||
- **Spacing:** Minimum 8px between targets
|
||||
|
||||
---
|
||||
|
||||
## iOS Human Interface Guidelines (HIG)
|
||||
|
||||
### Design Philosophy
|
||||
|
||||
- **Clarity:** Text is legible, icons precise, adornments subtle
|
||||
- **Deference:** UI helps people understand content, never competes
|
||||
- **Depth:** Distinct visual layers convey hierarchy
|
||||
|
||||
### Navigation Patterns
|
||||
|
||||
| Pattern | When to Use |
|
||||
| ------- | ----------- |
|
||||
| Tab Bar | 3-5 top-level destinations |
|
||||
| Navigation Bar | Hierarchical content |
|
||||
| Sidebar | iPad, rich content apps |
|
||||
| Search | Content discovery |
|
||||
|
||||
### Tab Bar Specifications
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────┐
|
||||
│ 🏠 🔍 ➕ 💬 👤 │
|
||||
│ Home Search Add Chat Profile │ 49pt height
|
||||
└─────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
- Max 5 tabs
|
||||
- Icons 25×25pt with 10pt labels
|
||||
- Active tab uses fill/tint color
|
||||
- Inactive tabs use gray
|
||||
|
||||
### Navigation Bar
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────┐
|
||||
│ ‹ Back Page Title Action │ 44pt minimum
|
||||
└─────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
- Left: Back button or cancel
|
||||
- Center: Title
|
||||
- Right: Primary action (text or icon)
|
||||
|
||||
### Typography (SF Pro)
|
||||
|
||||
| Style | Size | Weight |
|
||||
| ----- | ---- | ------ |
|
||||
| Large Title | 34pt | Bold |
|
||||
| Title 1 | 28pt | Bold |
|
||||
| Title 2 | 22pt | Bold |
|
||||
| Title 3 | 20pt | Semibold |
|
||||
| Headline | 17pt | Semibold |
|
||||
| Body | 17pt | Regular |
|
||||
| Callout | 16pt | Regular |
|
||||
| Subhead | 15pt | Regular |
|
||||
| Footnote | 13pt | Regular |
|
||||
| Caption | 12pt | Regular |
|
||||
|
||||
### iOS Colors (System)
|
||||
|
||||
| Color | Light | Dark |
|
||||
| ----- | ----- | ---- |
|
||||
| Label | #000000 | #FFFFFF |
|
||||
| Secondary Label | #3C3C43 @ 60% | #EBEBF5 @ 60% |
|
||||
| Tertiary Label | #3C3C43 @ 30% | #EBEBF5 @ 30% |
|
||||
| System Blue | #007AFF | #0A84FF |
|
||||
| System Green | #34C759 | #30D158 |
|
||||
| System Red | #FF3B30 | #FF453A |
|
||||
| System Orange | #FF9500 | #FF9F0A |
|
||||
|
||||
### iOS-Specific Patterns
|
||||
|
||||
- **Swipe gestures:** Delete, archive, actions
|
||||
- **Pull to refresh:** Standard list refresh
|
||||
- **Long press:** Context menus
|
||||
- **Haptic feedback:** Confirm actions
|
||||
- **Edge swipe:** Back navigation
|
||||
|
||||
---
|
||||
|
||||
## Android Material Design
|
||||
|
||||
### Android Design Philosophy
|
||||
|
||||
- **Material as metaphor:** Physical properties, elevation
|
||||
- **Bold, graphic, intentional:** Deliberate color, typography, space
|
||||
- **Motion provides meaning:** Feedback and continuity
|
||||
|
||||
### Android Navigation Patterns
|
||||
|
||||
| Pattern | When to Use |
|
||||
| ------- | ----------- |
|
||||
| Bottom Navigation | 3-5 top destinations |
|
||||
| Navigation Drawer | 5+ destinations, less frequent |
|
||||
| Navigation Rail | Tablet landscape |
|
||||
| Tabs | Related content groups |
|
||||
|
||||
### Bottom Navigation
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────┐
|
||||
│ 🏠 🔍 📷 💬 👤 │
|
||||
│ Home Search Camera Chat Account│ 80dp height
|
||||
└─────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
- 3-5 destinations
|
||||
- Icons 24dp with 12sp labels
|
||||
- Active: filled icon + primary color
|
||||
- Inactive: outlined icon + on-surface
|
||||
|
||||
### App Bar
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────┐
|
||||
│ ≡ App Title 🔍 │ 64dp height
|
||||
└─────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
- Left: Navigation icon (menu or back)
|
||||
- Center: Title (can be left-aligned)
|
||||
- Right: Action icons (max 3)
|
||||
|
||||
### Floating Action Button (FAB)
|
||||
|
||||
- **Size:** 56dp standard, 40dp mini
|
||||
- **Position:** Bottom right, 16dp from edges
|
||||
- **Purpose:** Primary action only
|
||||
- **Behavior:** Can hide on scroll
|
||||
|
||||
### Typography (Roboto)
|
||||
|
||||
| Style | Size | Weight | Tracking |
|
||||
| ----- | ---- | ------ | -------- |
|
||||
| Display Large | 57sp | Regular | -0.25 |
|
||||
| Display Medium | 45sp | Regular | 0 |
|
||||
| Display Small | 36sp | Regular | 0 |
|
||||
| Headline Large | 32sp | Regular | 0 |
|
||||
| Headline Medium | 28sp | Regular | 0 |
|
||||
| Headline Small | 24sp | Regular | 0 |
|
||||
| Title Large | 22sp | Regular | 0 |
|
||||
| Title Medium | 16sp | Medium | 0.15 |
|
||||
| Title Small | 14sp | Medium | 0.1 |
|
||||
| Body Large | 16sp | Regular | 0.5 |
|
||||
| Body Medium | 14sp | Regular | 0.25 |
|
||||
| Body Small | 12sp | Regular | 0.4 |
|
||||
| Label Large | 14sp | Medium | 0.1 |
|
||||
| Label Medium | 12sp | Medium | 0.5 |
|
||||
| Label Small | 11sp | Medium | 0.5 |
|
||||
|
||||
### Material Colors
|
||||
|
||||
| Role | Purpose |
|
||||
| ---- | ------- |
|
||||
| Primary | Main brand color |
|
||||
| On Primary | Text/icons on primary |
|
||||
| Primary Container | Filled buttons, active states |
|
||||
| Secondary | Less prominent components |
|
||||
| Tertiary | Contrast, balance |
|
||||
| Error | Error states |
|
||||
| Surface | Card backgrounds |
|
||||
| On Surface | Text on surfaces |
|
||||
| Outline | Borders, dividers |
|
||||
|
||||
### Elevation (Shadows)
|
||||
|
||||
| Level | Elevation | Use Case |
|
||||
| ----- | --------- | -------- |
|
||||
| 0 | 0dp | Flat surfaces |
|
||||
| 1 | 1dp | Cards, raised buttons |
|
||||
| 2 | 3dp | Elevated cards |
|
||||
| 3 | 6dp | FAB resting |
|
||||
| 4 | 8dp | Dialogs, pickers |
|
||||
| 5 | 12dp | FAB pressed |
|
||||
|
||||
### Android-Specific Patterns
|
||||
|
||||
- **Snackbar:** Brief feedback at bottom
|
||||
- **Bottom sheet:** Additional content/actions
|
||||
- **Chips:** Filter, input, choice, action
|
||||
- **Speed dial FAB:** Multiple related actions
|
||||
|
||||
---
|
||||
|
||||
## Responsive Web Design
|
||||
|
||||
### Breakpoints
|
||||
|
||||
| Name | Width | Typical Device |
|
||||
| ---- | ----- | -------------- |
|
||||
| xs | <576px | Mobile portrait |
|
||||
| sm | 576-767px | Mobile landscape |
|
||||
| md | 768-991px | Tablet |
|
||||
| lg | 992-1199px | Small desktop |
|
||||
| xl | 1200-1399px | Desktop |
|
||||
| xxl | ≥1400px | Large desktop |
|
||||
|
||||
### Grid System
|
||||
|
||||
- **Columns:** 12-column grid standard
|
||||
- **Gutters:** 16-24px between columns
|
||||
- **Margins:** 16px (mobile) to 64px (desktop)
|
||||
- **Max content width:** 1200-1440px
|
||||
|
||||
### Responsive Typography
|
||||
|
||||
```text
|
||||
Mobile (base):
|
||||
Body: 16px
|
||||
H1: 28-32px
|
||||
H2: 22-24px
|
||||
|
||||
Tablet:
|
||||
Body: 16px
|
||||
H1: 32-40px
|
||||
H2: 24-28px
|
||||
|
||||
Desktop:
|
||||
Body: 16-18px
|
||||
H1: 40-56px
|
||||
H2: 28-36px
|
||||
|
||||
```
|
||||
|
||||
### Mobile-First Approach
|
||||
|
||||
1. Design for smallest screen first
|
||||
2. Add complexity for larger screens
|
||||
3. Content priority: What's essential?
|
||||
4. Performance: Minimize for mobile
|
||||
5. Touch-first interactions
|
||||
|
||||
### Responsive Patterns
|
||||
|
||||
| Pattern | Description |
|
||||
| ------- | ----------- |
|
||||
| Stack | Columns become rows on mobile |
|
||||
| Reflow | Content reorders based on priority |
|
||||
| Reveal | More content shown at larger sizes |
|
||||
| Off-canvas | Navigation slides in on mobile |
|
||||
| Scale | Elements scale proportionally |
|
||||
|
||||
---
|
||||
|
||||
## Desktop Applications
|
||||
|
||||
### Window Chrome
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────┐
|
||||
│ ● ● ● App Title ─ □ × │ Title bar
|
||||
├────────┬────────────────────────────────┤
|
||||
│ Sidebar│ Content Area │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ ├────────────────────────────────┤
|
||||
│ │ Status Bar │
|
||||
└────────┴────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
### Keyboard-First Design
|
||||
|
||||
- All actions accessible via keyboard
|
||||
- Visible keyboard shortcuts
|
||||
- Focus management for tab order
|
||||
- Search/command palette (Cmd/Ctrl+K)
|
||||
|
||||
### Hover States
|
||||
|
||||
Desktop has hover (mobile doesn't):
|
||||
|
||||
- Show additional info on hover
|
||||
- Preview actions before click
|
||||
- Tooltips for icon-only buttons
|
||||
- Dropdown menus on hover
|
||||
|
||||
### Dense Information
|
||||
|
||||
Desktop allows for:
|
||||
|
||||
- Smaller touch targets (32px min)
|
||||
- More visible information
|
||||
- Complex tables and data grids
|
||||
- Multi-column layouts
|
||||
- Side-by-side comparisons
|
||||
|
||||
---
|
||||
|
||||
## Cross-Platform Considerations
|
||||
|
||||
### Shared Principles
|
||||
|
||||
- Consistent brand identity
|
||||
- Same core user flows
|
||||
- Synchronized data/state
|
||||
- Familiar information architecture
|
||||
|
||||
### Platform-Specific Adaptations
|
||||
|
||||
| Aspect | iOS | Android | Web |
|
||||
| ------ | --- | ------- | --- |
|
||||
| Back | Left nav | Left or gesture | Browser back |
|
||||
| Primary action | Right nav | FAB | Top right button |
|
||||
| Lists | Swipe actions | Long press | Hover actions |
|
||||
| Menus | Action sheets | Bottom sheet | Dropdown/context |
|
||||
| Alerts | Centered modal | Centered modal | Various positions |
|
||||
|
||||
### Design Tokens Across Platforms
|
||||
|
||||
Create platform-agnostic tokens:
|
||||
|
||||
```text
|
||||
// Spacing
|
||||
spacing-sm: 8
|
||||
spacing-md: 16
|
||||
spacing-lg: 24
|
||||
|
||||
// These map to platform units
|
||||
iOS: points (pt)
|
||||
Android: density-independent pixels (dp)
|
||||
Web: pixels (px) or rem
|
||||
|
||||
```
|
||||
328
skills/penpot-uiux-design/references/setup-troubleshooting.md
Normal file
328
skills/penpot-uiux-design/references/setup-troubleshooting.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# Penpot MCP Server Setup & Troubleshooting
|
||||
|
||||
Complete guide for installing, configuring, and troubleshooting the Penpot MCP Server.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The Penpot MCP integration requires **three components** working together:
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ MCP Client │────▶│ MCP Server │◀───▶│ Penpot Plugin │
|
||||
│ (VS Code/Claude)│ │ (port 4401) │ │ (in browser) │
|
||||
└─────────────────┘ └────────┬────────┘ └────────┬────────┘
|
||||
│ │
|
||||
│ WebSocket │
|
||||
│ (port 4402) │
|
||||
└───────────────────────┘
|
||||
```
|
||||
|
||||
1. **MCP Server** - Exposes tools to your AI client (HTTP on port 4401)
|
||||
2. **Plugin Server** - Serves the Penpot plugin files (HTTP on port 4400)
|
||||
3. **Penpot MCP Plugin** - Runs inside Penpot browser, executes design commands
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js v22+** - [Download](https://nodejs.org/)
|
||||
- **Git** - For cloning the repository
|
||||
- **Modern browser** - Chrome, Firefox, or Chromium-based browser
|
||||
|
||||
Verify Node.js installation:
|
||||
```bash
|
||||
node --version # Should be v22.x or higher
|
||||
npm --version
|
||||
npx --version
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Step 1: Clone and Install
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/penpot/penpot-mcp.git
|
||||
cd penpot-mcp
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
```
|
||||
|
||||
### Step 2: Build and Start Servers
|
||||
|
||||
```bash
|
||||
# Build all components and start servers
|
||||
npm run bootstrap
|
||||
```
|
||||
|
||||
This command:
|
||||
|
||||
- Installs dependencies for all components
|
||||
- Builds the MCP server and plugin
|
||||
- Starts both servers (MCP on 4401, Plugin on 4400)
|
||||
|
||||
**Expected output:**
|
||||
|
||||
```txt
|
||||
MCP Server listening on http://localhost:4401
|
||||
Plugin server listening on http://localhost:4400
|
||||
WebSocket server listening on port 4402
|
||||
```
|
||||
|
||||
### Step 3: Load Plugin in Penpot
|
||||
|
||||
1. Open [Penpot](https://design.penpot.app/) in your browser
|
||||
2. Open or create a design file
|
||||
3. Go to **Plugins** menu (or press the plugins icon)
|
||||
4. Click **Load plugin from URL**
|
||||
5. Enter: `http://localhost:4400/manifest.json`
|
||||
6. The plugin UI will appear - click **"Connect to MCP server"**
|
||||
7. Status should change to **"Connected to MCP server"**
|
||||
|
||||
> **Important**: Keep the plugin UI open while using MCP tools. Closing it disconnects the server.
|
||||
|
||||
### Step 4: Configure Your MCP Client
|
||||
|
||||
#### VS Code with GitHub Copilot
|
||||
|
||||
Add to your VS Code `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"penpot": {
|
||||
"url": "http://localhost:4401/sse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or use the HTTP endpoint:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"penpot": {
|
||||
"url": "http://localhost:4401/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Claude Desktop
|
||||
|
||||
Claude Desktop requires the `mcp-remote` proxy (stdio-only transport):
|
||||
|
||||
1. Install the proxy:
|
||||
|
||||
```bash
|
||||
npm install -g mcp-remote
|
||||
```
|
||||
|
||||
2. Edit Claude Desktop config:
|
||||
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
- **Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
|
||||
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
||||
|
||||
3. Add the Penpot server:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"penpot": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "http://localhost:4401/sse", "--allow-http"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Fully quit** Claude Desktop (File → Quit, not just close window) and restart
|
||||
|
||||
#### Claude Code (CLI)
|
||||
|
||||
```bash
|
||||
claude mcp add penpot -t http http://localhost:4401/mcp
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
|
||||
#### "Plugin cannot connect to MCP server"
|
||||
|
||||
**Symptoms**: Plugin shows "Not connected" even after clicking Connect
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Verify servers are running:
|
||||
```bash
|
||||
# Check if ports are in use
|
||||
lsof -i :4401 # MCP server
|
||||
lsof -i :4402 # WebSocket
|
||||
lsof -i :4400 # Plugin server
|
||||
```
|
||||
|
||||
2. Restart the servers:
|
||||
|
||||
```bash
|
||||
# In the penpot-mcp directory
|
||||
npm run start:all
|
||||
```
|
||||
|
||||
3. Check browser console (F12) for WebSocket errors
|
||||
|
||||
#### Browser Blocks Local Connection
|
||||
|
||||
**Symptoms**: Browser refuses to connect to localhost from Penpot
|
||||
|
||||
**Cause**: Chromium 142+ enforces Private Network Access (PNA) restrictions
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Chrome/Chromium**: When prompted, allow access to local network
|
||||
2. **Brave**: Disable Shield for the Penpot website:
|
||||
- Click the Brave Shield icon in address bar
|
||||
- Toggle Shield off for this site
|
||||
3. **Try Firefox**: Firefox doesn't enforce these restrictions as strictly
|
||||
|
||||
#### "WebSocket connection failed"
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check firewall settings - allow ports 4400, 4401, 4402
|
||||
2. Disable VPN if active
|
||||
3. Check for conflicting applications using the same ports
|
||||
|
||||
### MCP Client Issues
|
||||
|
||||
#### Tools Not Appearing in VS Code/Claude
|
||||
|
||||
1. **Verify endpoint**:
|
||||
|
||||
```bash
|
||||
# Test the SSE endpoint
|
||||
curl http://localhost:4401/sse
|
||||
|
||||
# Test the MCP endpoint
|
||||
curl http://localhost:4401/mcp
|
||||
```
|
||||
|
||||
2. **Check configuration syntax** - JSON must be valid
|
||||
3. **Restart the MCP client** completely
|
||||
4. **Check MCP server logs**:
|
||||
|
||||
```bash
|
||||
# Logs are in mcp-server/logs/
|
||||
tail -f mcp-server/logs/mcp-server.log
|
||||
```
|
||||
|
||||
#### "Tool execution timed out"
|
||||
|
||||
**Cause**: Plugin disconnected or operation took too long
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Ensure plugin UI is still open in Penpot
|
||||
2. Verify plugin shows "Connected" status
|
||||
3. Try reconnecting: click Disconnect then Connect in plugin
|
||||
|
||||
### Plugin Issues
|
||||
|
||||
#### "Plugin failed to load"
|
||||
|
||||
1. Verify plugin server is running on port 4400
|
||||
2. Try accessing `http://localhost:4400/manifest.json` directly in browser
|
||||
3. Clear browser cache and reload Penpot
|
||||
4. Remove and re-add the plugin
|
||||
|
||||
#### "Cannot find penpot object"
|
||||
|
||||
**Cause**: Plugin not properly initialized or design file not open
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Make sure you have a design file open (not just the dashboard)
|
||||
2. Wait a few seconds after opening file before connecting
|
||||
3. Refresh Penpot and reload the plugin
|
||||
|
||||
### Server Issues
|
||||
|
||||
#### Port Already in Use
|
||||
|
||||
```bash
|
||||
# Find process using the port
|
||||
lsof -i :4401
|
||||
|
||||
# Kill the process if needed
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
Or configure different ports via environment variables:
|
||||
```bash
|
||||
PENPOT_MCP_SERVER_PORT=4501 npm run start:all
|
||||
```
|
||||
|
||||
#### Server Crashes on Startup
|
||||
|
||||
1. Check Node.js version (must be v22+)
|
||||
2. Delete `node_modules` and reinstall:
|
||||
|
||||
```bash
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
npm run bootstrap
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `PENPOT_MCP_SERVER_PORT` | 4401 | HTTP/SSE server port |
|
||||
| `PENPOT_MCP_WEBSOCKET_PORT` | 4402 | WebSocket server port |
|
||||
| `PENPOT_MCP_SERVER_LISTEN_ADDRESS` | localhost | Server bind address |
|
||||
| `PENPOT_MCP_LOG_LEVEL` | info | Log level (trace/debug/info/warn/error) |
|
||||
| `PENPOT_MCP_LOG_DIR` | logs | Log file directory |
|
||||
| `PENPOT_MCP_REMOTE_MODE` | false | Enable remote mode (disables file system access) |
|
||||
|
||||
### Example: Custom Configuration
|
||||
|
||||
```bash
|
||||
# Run on different ports with debug logging
|
||||
PENPOT_MCP_SERVER_PORT=5000 \
|
||||
PENPOT_MCP_WEBSOCKET_PORT=5001 \
|
||||
PENPOT_MCP_LOG_LEVEL=debug \
|
||||
npm run start:all
|
||||
```
|
||||
|
||||
## Verifying the Setup
|
||||
|
||||
Run this checklist to confirm everything works:
|
||||
|
||||
1. **Servers Running**:
|
||||
```bash
|
||||
curl -s http://localhost:4401/sse | head -1
|
||||
# Should return SSE stream headers
|
||||
```
|
||||
|
||||
2. **Plugin Connected**: Plugin UI shows "Connected to MCP server"
|
||||
|
||||
3. **Tools Available**: In your MCP client, verify these tools appear:
|
||||
- `mcp__penpot__execute_code`
|
||||
- `mcp__penpot__export_shape`
|
||||
- `mcp__penpot__import_image`
|
||||
- `mcp__penpot__penpot_api_info`
|
||||
|
||||
4. **Test Execution**: Ask your AI assistant to run a simple command:
|
||||
> "Use Penpot to get the current page name"
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **GitHub Issues**: [penpot/penpot-mcp/issues](https://github.com/penpot/penpot-mcp/issues)
|
||||
- **GitHub Discussions**: [penpot/penpot-mcp/discussions](https://github.com/penpot/penpot-mcp/discussions)
|
||||
- **Penpot Community**: [community.penpot.app](https://community.penpot.app/)
|
||||
153
skills/powerbi-modeling/SKILL.md
Normal file
153
skills/powerbi-modeling/SKILL.md
Normal file
@@ -0,0 +1,153 @@
|
||||
---
|
||||
name: powerbi-modeling
|
||||
description: 'Power BI semantic modeling assistant for building optimized data models. Use when working with Power BI semantic models, creating measures, designing star schemas, configuring relationships, implementing RLS, or optimizing model performance. Triggers on queries about DAX calculations, table relationships, dimension/fact table design, naming conventions, model documentation, cardinality, cross-filter direction, calculation groups, and data model best practices. Always connects to the active model first using power-bi-modeling MCP tools to understand the data structure before providing guidance.'
|
||||
---
|
||||
|
||||
# Power BI Semantic Modeling
|
||||
|
||||
Guide users in building optimized, well-documented Power BI semantic models following Microsoft best practices.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when users ask about:
|
||||
- Creating or optimizing Power BI semantic models
|
||||
- Designing star schemas (dimension/fact tables)
|
||||
- Writing DAX measures or calculated columns
|
||||
- Configuring table relationships (cardinality, cross-filter)
|
||||
- Implementing row-level security (RLS)
|
||||
- Naming conventions for tables, columns, measures
|
||||
- Adding descriptions and documentation to models
|
||||
- Performance tuning and optimization
|
||||
- Calculation groups and field parameters
|
||||
- Model validation and best practice checks
|
||||
|
||||
**Trigger phrases:** "create a measure", "add relationship", "star schema", "optimize model", "DAX formula", "RLS", "naming convention", "model documentation", "cardinality", "cross-filter"
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required Tools
|
||||
- **Power BI Modeling MCP Server**: Required for connecting to and modifying semantic models
|
||||
- Enables: connection_operations, table_operations, measure_operations, relationship_operations, etc.
|
||||
- Must be configured and running to interact with models
|
||||
|
||||
### Optional Dependencies
|
||||
- **Microsoft Learn MCP Server**: Recommended for researching latest best practices
|
||||
- Enables: microsoft_docs_search, microsoft_docs_fetch
|
||||
- Use for complex scenarios, new features, and official documentation
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Connect and Analyze First
|
||||
|
||||
Before providing any modeling guidance, always examine the current model state:
|
||||
|
||||
```
|
||||
1. List connections: connection_operations(operation: "ListConnections")
|
||||
2. If no connection, check for local instances: connection_operations(operation: "ListLocalInstances")
|
||||
3. Connect to the model (Desktop or Fabric)
|
||||
4. Get model overview: model_operations(operation: "Get")
|
||||
5. List tables: table_operations(operation: "List")
|
||||
6. List relationships: relationship_operations(operation: "List")
|
||||
7. List measures: measure_operations(operation: "List")
|
||||
```
|
||||
|
||||
### 2. Evaluate Model Health
|
||||
|
||||
After connecting, assess the model against best practices:
|
||||
|
||||
- **Star Schema**: Are tables properly classified as dimension or fact?
|
||||
- **Relationships**: Correct cardinality? Minimal bidirectional filters?
|
||||
- **Naming**: Human-readable, consistent naming conventions?
|
||||
- **Documentation**: Do tables, columns, measures have descriptions?
|
||||
- **Measures**: Explicit measures for key calculations?
|
||||
- **Hidden Fields**: Are technical columns hidden from report view?
|
||||
|
||||
### 3. Provide Targeted Guidance
|
||||
|
||||
Based on analysis, guide improvements using references:
|
||||
- Star schema design: See [STAR-SCHEMA.md](references/STAR-SCHEMA.md)
|
||||
- Relationship configuration: See [RELATIONSHIPS.md](references/RELATIONSHIPS.md)
|
||||
- DAX measures and naming: See [MEASURES-DAX.md](references/MEASURES-DAX.md)
|
||||
- Performance optimization: See [PERFORMANCE.md](references/PERFORMANCE.md)
|
||||
- Row-level security: See [RLS.md](references/RLS.md)
|
||||
|
||||
## Quick Reference: Model Quality Checklist
|
||||
|
||||
| Area | Best Practice |
|
||||
|------|--------------|
|
||||
| Tables | Clear dimension vs fact classification |
|
||||
| Naming | Human-readable: `Customer Name` not `CUST_NM` |
|
||||
| Descriptions | All tables, columns, measures documented |
|
||||
| Measures | Explicit DAX measures for business metrics |
|
||||
| Relationships | One-to-many from dimension to fact |
|
||||
| Cross-filter | Single direction unless specifically needed |
|
||||
| Hidden fields | Hide technical keys, IDs from report view |
|
||||
| Date table | Dedicated marked date table |
|
||||
|
||||
## MCP Tools Reference
|
||||
|
||||
Use these Power BI Modeling MCP operations:
|
||||
|
||||
| Operation Category | Key Operations |
|
||||
|-------------------|----------------|
|
||||
| `connection_operations` | Connect, ListConnections, ListLocalInstances, ConnectFabric |
|
||||
| `model_operations` | Get, GetStats, ExportTMDL |
|
||||
| `table_operations` | List, Get, Create, Update, GetSchema |
|
||||
| `column_operations` | List, Get, Create, Update (descriptions, hidden, format) |
|
||||
| `measure_operations` | List, Get, Create, Update, Move |
|
||||
| `relationship_operations` | List, Get, Create, Update, Activate, Deactivate |
|
||||
| `dax_query_operations` | Execute, Validate |
|
||||
| `calculation_group_operations` | List, Create, Update |
|
||||
| `security_role_operations` | List, Create, Update, GetEffectivePermissions |
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Add Measure with Description
|
||||
```
|
||||
measure_operations(
|
||||
operation: "Create",
|
||||
definitions: [{
|
||||
name: "Total Sales",
|
||||
tableName: "Sales",
|
||||
expression: "SUM(Sales[Amount])",
|
||||
formatString: "$#,##0",
|
||||
description: "Sum of all sales amounts"
|
||||
}]
|
||||
)
|
||||
```
|
||||
|
||||
### Update Column Description
|
||||
```
|
||||
column_operations(
|
||||
operation: "Update",
|
||||
definitions: [{
|
||||
tableName: "Customer",
|
||||
name: "CustomerKey",
|
||||
description: "Unique identifier for customer dimension",
|
||||
isHidden: true
|
||||
}]
|
||||
)
|
||||
```
|
||||
|
||||
### Create Relationship
|
||||
```
|
||||
relationship_operations(
|
||||
operation: "Create",
|
||||
definitions: [{
|
||||
fromTable: "Sales",
|
||||
fromColumn: "CustomerKey",
|
||||
toTable: "Customer",
|
||||
toColumn: "CustomerKey",
|
||||
crossFilteringBehavior: "OneDirection"
|
||||
}]
|
||||
)
|
||||
```
|
||||
|
||||
## When to Use Microsoft Learn MCP
|
||||
|
||||
Research current best practices using `microsoft_docs_search` for:
|
||||
- Latest DAX function documentation
|
||||
- New Power BI features and capabilities
|
||||
- Complex modeling scenarios (SCD Type 2, many-to-many)
|
||||
- Performance optimization techniques
|
||||
- Security implementation patterns
|
||||
195
skills/powerbi-modeling/references/MEASURES-DAX.md
Normal file
195
skills/powerbi-modeling/references/MEASURES-DAX.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# DAX Measures and Naming Conventions
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### General Rules
|
||||
- Use human-readable names (spaces allowed)
|
||||
- Be descriptive: `Total Sales Amount` not `TSA`
|
||||
- Avoid abbreviations unless universally understood
|
||||
- Use consistent capitalization (Title Case recommended)
|
||||
- Avoid special characters except spaces
|
||||
|
||||
### Table Naming
|
||||
| Type | Convention | Example |
|
||||
|------|------------|---------|
|
||||
| Dimension | Singular noun | Customer, Product, Date |
|
||||
| Fact | Business process | Sales, Orders, Inventory |
|
||||
| Bridge | Combined names | CustomerAccount, ProductCategory |
|
||||
| Measure Table | Underscore prefix | _Measures, _KPIs |
|
||||
|
||||
### Column Naming
|
||||
| Type | Convention | Example |
|
||||
|------|------------|---------|
|
||||
| Keys | Suffix with "Key" or "ID" | CustomerKey, ProductID |
|
||||
| Dates | Suffix with "Date" | OrderDate, ShipDate |
|
||||
| Amounts | Descriptive with unit hint | SalesAmount, QuantitySold |
|
||||
| Flags | Prefix with "Is" or "Has" | IsActive, HasDiscount |
|
||||
|
||||
### Measure Naming
|
||||
| Type | Convention | Example |
|
||||
|------|------------|---------|
|
||||
| Aggregations | Verb + Noun | Total Sales, Count of Orders |
|
||||
| Ratios | X per Y or X Rate | Sales per Customer, Conversion Rate |
|
||||
| Time Intelligence | Period + Metric | YTD Sales, PY Total Sales |
|
||||
| Comparisons | Metric + vs + Baseline | Sales vs Budget, Growth vs PY |
|
||||
|
||||
## Explicit vs Implicit Measures
|
||||
|
||||
### Always Create Explicit Measures For:
|
||||
1. Key business metrics users will query
|
||||
2. Complex calculations with filter manipulation
|
||||
3. Measures used in MDX (Excel PivotTables)
|
||||
4. Controlled aggregation (prevent sum of averages)
|
||||
|
||||
### Implicit Measures (Column Aggregations)
|
||||
- Acceptable for simple exploration
|
||||
- Set correct SummarizeBy property:
|
||||
- Amounts: Sum
|
||||
- Keys/IDs: None (Do Not Summarize)
|
||||
- Rates/Prices: None or Average
|
||||
|
||||
## Measure Patterns
|
||||
|
||||
### Basic Aggregations
|
||||
```dax
|
||||
Total Sales = SUM(Sales[SalesAmount])
|
||||
Order Count = COUNTROWS(Sales)
|
||||
Average Order Value = DIVIDE([Total Sales], [Order Count])
|
||||
Distinct Customers = DISTINCTCOUNT(Sales[CustomerKey])
|
||||
```
|
||||
|
||||
### Time Intelligence (Requires Date Table)
|
||||
```dax
|
||||
YTD Sales = TOTALYTD([Total Sales], 'Date'[Date])
|
||||
MTD Sales = TOTALMTD([Total Sales], 'Date'[Date])
|
||||
PY Sales = CALCULATE([Total Sales], SAMEPERIODLASTYEAR('Date'[Date]))
|
||||
YoY Growth = DIVIDE([Total Sales] - [PY Sales], [PY Sales])
|
||||
```
|
||||
|
||||
### Percentage Calculations
|
||||
```dax
|
||||
Sales % of Total =
|
||||
DIVIDE(
|
||||
[Total Sales],
|
||||
CALCULATE([Total Sales], REMOVEFILTERS(Product))
|
||||
)
|
||||
|
||||
Margin % = DIVIDE([Gross Profit], [Total Sales])
|
||||
```
|
||||
|
||||
### Running Totals
|
||||
```dax
|
||||
Running Total =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
FILTER(
|
||||
ALL('Date'),
|
||||
'Date'[Date] <= MAX('Date'[Date])
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Column References
|
||||
|
||||
### Best Practice: Always Qualify Column Names
|
||||
```dax
|
||||
// GOOD - Fully qualified
|
||||
Sales Amount = SUM(Sales[SalesAmount])
|
||||
|
||||
// BAD - Unqualified (can cause ambiguity)
|
||||
Sales Amount = SUM([SalesAmount])
|
||||
```
|
||||
|
||||
### Measure References: Never Qualify
|
||||
```dax
|
||||
// GOOD - Unqualified measure
|
||||
YTD Sales = TOTALYTD([Total Sales], 'Date'[Date])
|
||||
|
||||
// BAD - Qualified measure (breaks if home table changes)
|
||||
YTD Sales = TOTALYTD(Sales[Total Sales], 'Date'[Date])
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### Measure Descriptions
|
||||
Always add descriptions explaining:
|
||||
- What the measure calculates
|
||||
- Business context/usage
|
||||
- Any important assumptions
|
||||
|
||||
```
|
||||
measure_operations(
|
||||
operation: "Update",
|
||||
definitions: [{
|
||||
name: "Total Sales",
|
||||
tableName: "Sales",
|
||||
description: "Sum of all completed sales transactions. Excludes returns and cancelled orders."
|
||||
}]
|
||||
)
|
||||
```
|
||||
|
||||
### Format Strings
|
||||
| Data Type | Format String | Example Output |
|
||||
|-----------|---------------|----------------|
|
||||
| Currency | $#,##0.00 | $1,234.56 |
|
||||
| Percentage | 0.0% | 12.3% |
|
||||
| Whole Number | #,##0 | 1,234 |
|
||||
| Decimal | #,##0.00 | 1,234.56 |
|
||||
|
||||
## Display Folders
|
||||
|
||||
Organize measures into logical groups:
|
||||
```
|
||||
measure_operations(
|
||||
operation: "Update",
|
||||
definitions: [{
|
||||
name: "YTD Sales",
|
||||
tableName: "_Measures",
|
||||
displayFolder: "Time Intelligence\\Year"
|
||||
}]
|
||||
)
|
||||
```
|
||||
|
||||
Common folder structure:
|
||||
```
|
||||
_Measures
|
||||
├── Sales
|
||||
│ ├── Total Sales
|
||||
│ └── Average Sale
|
||||
├── Time Intelligence
|
||||
│ ├── Year
|
||||
│ │ ├── YTD Sales
|
||||
│ │ └── PY Sales
|
||||
│ └── Month
|
||||
│ └── MTD Sales
|
||||
└── Ratios
|
||||
├── Margin %
|
||||
└── Conversion Rate
|
||||
```
|
||||
|
||||
## Variables for Performance
|
||||
|
||||
Use variables to:
|
||||
- Avoid recalculating the same expression
|
||||
- Improve readability
|
||||
- Enable debugging
|
||||
|
||||
```dax
|
||||
Gross Margin % =
|
||||
VAR TotalSales = [Total Sales]
|
||||
VAR TotalCost = [Total Cost]
|
||||
VAR GrossProfit = TotalSales - TotalCost
|
||||
RETURN
|
||||
DIVIDE(GrossProfit, TotalSales)
|
||||
```
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
- [ ] All key business metrics have explicit measures
|
||||
- [ ] Measures have clear, descriptive names
|
||||
- [ ] Measures have descriptions
|
||||
- [ ] Appropriate format strings applied
|
||||
- [ ] Display folders organize related measures
|
||||
- [ ] Column references are fully qualified
|
||||
- [ ] Measure references are not qualified
|
||||
- [ ] Variables used for complex calculations
|
||||
215
skills/powerbi-modeling/references/PERFORMANCE.md
Normal file
215
skills/powerbi-modeling/references/PERFORMANCE.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# Performance Optimization for Power BI Models
|
||||
|
||||
## Data Reduction Techniques
|
||||
|
||||
### 1. Remove Unnecessary Columns
|
||||
- Only import columns needed for reporting
|
||||
- Remove audit columns (CreatedBy, ModifiedDate) unless required
|
||||
- Remove duplicate/redundant columns
|
||||
|
||||
```
|
||||
column_operations(operation: "List", filter: { tableNames: ["Sales"] })
|
||||
// Review and remove unneeded columns
|
||||
```
|
||||
|
||||
### 2. Remove Unnecessary Rows
|
||||
- Filter historical data to relevant period
|
||||
- Exclude cancelled/void transactions if not needed
|
||||
- Apply filters in Power Query (not in DAX)
|
||||
|
||||
### 3. Reduce Cardinality
|
||||
High cardinality (many unique values) impacts:
|
||||
- Model size
|
||||
- Refresh time
|
||||
- Query performance
|
||||
|
||||
**Solutions:**
|
||||
| Column Type | Reduction Technique |
|
||||
|-------------|---------------------|
|
||||
| DateTime | Split into Date and Time columns |
|
||||
| Decimal precision | Round to needed precision |
|
||||
| Text with patterns | Extract common prefix/suffix |
|
||||
| High-precision IDs | Use surrogate integer keys |
|
||||
|
||||
### 4. Optimize Data Types
|
||||
| From | To | Benefit |
|
||||
|------|-----|---------|
|
||||
| DateTime | Date (if time not needed) | 8 bytes to 4 bytes |
|
||||
| Decimal | Fixed Decimal | Better compression |
|
||||
| Text with numbers | Whole Number | Much better compression |
|
||||
| Long text | Shorter text | Reduces storage |
|
||||
|
||||
### 5. Group and Summarize
|
||||
Pre-aggregate data when detail not needed:
|
||||
- Daily instead of transactional
|
||||
- Monthly instead of daily
|
||||
- Consider aggregation tables for DirectQuery
|
||||
|
||||
## Column Optimization
|
||||
|
||||
### Prefer Power Query Columns Over Calculated Columns
|
||||
| Approach | When to Use |
|
||||
|----------|-------------|
|
||||
| Power Query (M) | Can be computed at source, static values |
|
||||
| Calculated Column (DAX) | Needs model relationships, dynamic logic |
|
||||
|
||||
Power Query columns:
|
||||
- Load faster
|
||||
- Compress better
|
||||
- Use less memory
|
||||
|
||||
### Avoid Calculated Columns on Relationship Keys
|
||||
DAX calculated columns in relationships:
|
||||
- Cannot use indexes
|
||||
- Generate complex SQL for DirectQuery
|
||||
- Hurt performance significantly
|
||||
|
||||
**Use COMBINEVALUES for multi-column relationships:**
|
||||
```dax
|
||||
// If you must use calculated column for composite key
|
||||
CompositeKey = COMBINEVALUES(",", [Country], [City])
|
||||
```
|
||||
|
||||
### Set Appropriate Summarization
|
||||
Prevent accidental aggregation of non-additive columns:
|
||||
```
|
||||
column_operations(
|
||||
operation: "Update",
|
||||
definitions: [{
|
||||
tableName: "Product",
|
||||
name: "UnitPrice",
|
||||
summarizeBy: "None"
|
||||
}]
|
||||
)
|
||||
```
|
||||
|
||||
## Relationship Optimization
|
||||
|
||||
### 1. Minimize Bidirectional Relationships
|
||||
Each bidirectional relationship:
|
||||
- Increases query complexity
|
||||
- Can create ambiguous paths
|
||||
- Reduces performance
|
||||
|
||||
### 2. Avoid Many-to-Many When Possible
|
||||
Many-to-many relationships:
|
||||
- Generate more complex queries
|
||||
- Require more memory
|
||||
- Can produce unexpected results
|
||||
|
||||
### 3. Reduce Relationship Cardinality
|
||||
Keep relationship columns low cardinality:
|
||||
- Use integer keys over text
|
||||
- Consider higher-grain relationships
|
||||
|
||||
## DAX Optimization
|
||||
|
||||
### 1. Use Variables
|
||||
```dax
|
||||
// GOOD - Calculate once, use twice
|
||||
Sales Growth =
|
||||
VAR CurrentSales = [Total Sales]
|
||||
VAR PriorSales = [PY Sales]
|
||||
RETURN DIVIDE(CurrentSales - PriorSales, PriorSales)
|
||||
|
||||
// BAD - Recalculates [Total Sales] and [PY Sales]
|
||||
Sales Growth =
|
||||
DIVIDE([Total Sales] - [PY Sales], [PY Sales])
|
||||
```
|
||||
|
||||
### 2. Avoid FILTER with Entire Tables
|
||||
```dax
|
||||
// BAD - Iterates entire table
|
||||
Sales High Value =
|
||||
CALCULATE([Total Sales], FILTER(Sales, Sales[Amount] > 1000))
|
||||
|
||||
// GOOD - Uses column reference
|
||||
Sales High Value =
|
||||
CALCULATE([Total Sales], Sales[Amount] > 1000)
|
||||
```
|
||||
|
||||
### 3. Use KEEPFILTERS Appropriately
|
||||
```dax
|
||||
// Respects existing filters
|
||||
Sales with Filter =
|
||||
CALCULATE([Total Sales], KEEPFILTERS(Product[Category] = "Bikes"))
|
||||
```
|
||||
|
||||
### 4. Prefer DIVIDE Over Division Operator
|
||||
```dax
|
||||
// GOOD - Handles divide by zero
|
||||
Margin % = DIVIDE([Profit], [Sales])
|
||||
|
||||
// BAD - Errors on zero
|
||||
Margin % = [Profit] / [Sales]
|
||||
```
|
||||
|
||||
## DirectQuery Optimization
|
||||
|
||||
### 1. Minimize Columns and Tables
|
||||
DirectQuery models:
|
||||
- Query source for every visual
|
||||
- Performance depends on source
|
||||
- Minimize data retrieved
|
||||
|
||||
### 2. Avoid Complex Power Query Transformations
|
||||
- Transforms become subqueries
|
||||
- Native queries are faster
|
||||
- Materialize at source when possible
|
||||
|
||||
### 3. Keep Measures Simple Initially
|
||||
Complex DAX generates complex SQL:
|
||||
- Start with basic aggregations
|
||||
- Add complexity gradually
|
||||
- Monitor query performance
|
||||
|
||||
### 4. Disable Auto Date/Time
|
||||
For DirectQuery models, disable auto date/time:
|
||||
- Creates hidden calculated tables
|
||||
- Increases model complexity
|
||||
- Use explicit date table instead
|
||||
|
||||
## Aggregations
|
||||
|
||||
### User-Defined Aggregations
|
||||
Pre-aggregate fact tables for:
|
||||
- Very large models (billions of rows)
|
||||
- Hybrid DirectQuery/Import
|
||||
- Common query patterns
|
||||
|
||||
```
|
||||
table_operations(
|
||||
operation: "Create",
|
||||
definitions: [{
|
||||
name: "SalesAgg",
|
||||
mode: "Import",
|
||||
mExpression: "..."
|
||||
}]
|
||||
)
|
||||
```
|
||||
|
||||
## Performance Testing
|
||||
|
||||
### Use Performance Analyzer
|
||||
1. Enable in Power BI Desktop
|
||||
2. Start recording
|
||||
3. Interact with visuals
|
||||
4. Review DAX query times
|
||||
|
||||
### Monitor with DAX Studio
|
||||
External tool for:
|
||||
- Query timing
|
||||
- Server timings
|
||||
- Query plans
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
- [ ] Unnecessary columns removed
|
||||
- [ ] Appropriate data types used
|
||||
- [ ] High-cardinality columns addressed
|
||||
- [ ] Bidirectional relationships minimized
|
||||
- [ ] DAX uses variables for repeated expressions
|
||||
- [ ] No FILTER on entire tables
|
||||
- [ ] DIVIDE used instead of division operator
|
||||
- [ ] Auto date/time disabled for DirectQuery
|
||||
- [ ] Performance tested with representative data
|
||||
147
skills/powerbi-modeling/references/RELATIONSHIPS.md
Normal file
147
skills/powerbi-modeling/references/RELATIONSHIPS.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Relationships in Power BI
|
||||
|
||||
## Relationship Properties
|
||||
|
||||
### Cardinality
|
||||
| Type | Use Case | Notes |
|
||||
|------|----------|-------|
|
||||
| One-to-Many (*:1) | Dimension to Fact | Most common, preferred |
|
||||
| Many-to-One (1:*) | Fact to Dimension | Same as above, direction reversed |
|
||||
| One-to-One (1:1) | Dimension extensions | Use sparingly |
|
||||
| Many-to-Many (*:*) | Bridge tables, complex scenarios | Requires careful design |
|
||||
|
||||
### Cross-Filter Direction
|
||||
| Setting | Behavior | When to Use |
|
||||
|---------|----------|-------------|
|
||||
| Single | Filters flow from "one" to "many" | Default, best performance |
|
||||
| Both | Filters flow in both directions | Only when necessary |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Prefer One-to-Many Relationships
|
||||
```
|
||||
Customer (1) --> (*) Sales
|
||||
Product (1) --> (*) Sales
|
||||
Date (1) --> (*) Sales
|
||||
```
|
||||
|
||||
### 2. Use Single-Direction Cross-Filtering
|
||||
Bidirectional filtering:
|
||||
- Impacts performance negatively
|
||||
- Can create ambiguous filter paths
|
||||
- May produce unexpected results
|
||||
|
||||
**Only use bidirectional when:**
|
||||
- Dimension-to-dimension analysis through fact table
|
||||
- Specific RLS requirements
|
||||
|
||||
**Better alternative:** Use CROSSFILTER in DAX measures:
|
||||
```dax
|
||||
Countries Sold =
|
||||
CALCULATE(
|
||||
DISTINCTCOUNT(Customer[Country]),
|
||||
CROSSFILTER(Customer[CustomerKey], Sales[CustomerKey], BOTH)
|
||||
)
|
||||
```
|
||||
|
||||
### 3. One Active Path Between Tables
|
||||
- Only one active relationship between any two tables
|
||||
- Use USERELATIONSHIP for role-playing dimensions:
|
||||
|
||||
```dax
|
||||
Sales by Ship Date =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
USERELATIONSHIP(Sales[ShipDate], Date[Date])
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Avoid Ambiguous Paths
|
||||
Circular references cause errors. Solutions:
|
||||
- Deactivate one relationship
|
||||
- Restructure model
|
||||
- Use USERELATIONSHIP in measures
|
||||
|
||||
## Relationship Patterns
|
||||
|
||||
### Standard Star Schema
|
||||
```
|
||||
[Date]
|
||||
|
|
||||
[Product]--[Sales]--[Customer]
|
||||
|
|
||||
[Store]
|
||||
```
|
||||
|
||||
### Role-Playing Dimension
|
||||
```
|
||||
[Date] --(active)-- [Sales.OrderDate]
|
||||
|
|
||||
+--(inactive)-- [Sales.ShipDate]
|
||||
```
|
||||
|
||||
### Bridge Table (Many-to-Many)
|
||||
```
|
||||
[Customer]--(*)--[CustomerAccount]--(*)--[Account]
|
||||
```
|
||||
|
||||
### Factless Fact Table
|
||||
```
|
||||
[Product]--[ProductPromotion]--[Promotion]
|
||||
```
|
||||
Used to capture relationships without measures.
|
||||
|
||||
## Creating Relationships via MCP
|
||||
|
||||
### List Current Relationships
|
||||
```
|
||||
relationship_operations(operation: "List")
|
||||
```
|
||||
|
||||
### Create New Relationship
|
||||
```
|
||||
relationship_operations(
|
||||
operation: "Create",
|
||||
definitions: [{
|
||||
fromTable: "Sales",
|
||||
fromColumn: "ProductKey",
|
||||
toTable: "Product",
|
||||
toColumn: "ProductKey",
|
||||
crossFilteringBehavior: "OneDirection",
|
||||
isActive: true
|
||||
}]
|
||||
)
|
||||
```
|
||||
|
||||
### Deactivate Relationship
|
||||
```
|
||||
relationship_operations(
|
||||
operation: "Deactivate",
|
||||
references: [{ name: "relationship-guid-here" }]
|
||||
)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Ambiguous Path" Error
|
||||
Multiple active paths exist between tables.
|
||||
- Check for: Multiple fact tables sharing dimensions
|
||||
- Solution: Deactivate redundant relationships
|
||||
|
||||
### Bidirectional Not Allowed
|
||||
Circular reference would be created.
|
||||
- Solution: Restructure or use DAX CROSSFILTER
|
||||
|
||||
### Relationship Not Detected
|
||||
Columns may have different data types.
|
||||
- Ensure both columns have identical types
|
||||
- Check for trailing spaces in text keys
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
- [ ] All relationships are one-to-many where possible
|
||||
- [ ] Cross-filter is single direction by default
|
||||
- [ ] Only one active path between any two tables
|
||||
- [ ] Role-playing dimensions use inactive relationships
|
||||
- [ ] No circular reference paths
|
||||
- [ ] Key columns have matching data types
|
||||
226
skills/powerbi-modeling/references/RLS.md
Normal file
226
skills/powerbi-modeling/references/RLS.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# Row-Level Security (RLS) in Power BI
|
||||
|
||||
## Overview
|
||||
|
||||
Row-Level Security restricts data access at the row level based on user identity. Users see only the data they're authorized to view.
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Filter on Dimension Tables
|
||||
Apply RLS to dimensions, not fact tables:
|
||||
- More efficient (smaller tables)
|
||||
- Filters propagate through relationships
|
||||
- Easier to maintain
|
||||
|
||||
```dax
|
||||
// On Customer dimension - filters propagate to Sales
|
||||
[Region] = "West"
|
||||
```
|
||||
|
||||
### 2. Create Minimal Roles
|
||||
Avoid many role combinations:
|
||||
- Each role = separate cache
|
||||
- Roles are additive (union, not intersection)
|
||||
- Consolidate where possible
|
||||
|
||||
### 3. Use Dynamic RLS When Possible
|
||||
Data-driven rules scale better:
|
||||
- User mapping in a table
|
||||
- USERPRINCIPALNAME() for identity
|
||||
- No role changes when users change
|
||||
|
||||
## Static vs Dynamic RLS
|
||||
|
||||
### Static RLS
|
||||
Fixed rules per role:
|
||||
```dax
|
||||
// Role: West Region
|
||||
[Region] = "West"
|
||||
|
||||
// Role: East Region
|
||||
[Region] = "East"
|
||||
```
|
||||
|
||||
**Pros:** Simple, clear
|
||||
**Cons:** Doesn't scale, requires role per group
|
||||
|
||||
### Dynamic RLS
|
||||
User identity drives filtering:
|
||||
```dax
|
||||
// Single role filters based on logged-in user
|
||||
[ManagerEmail] = USERPRINCIPALNAME()
|
||||
```
|
||||
|
||||
**Pros:** Scales, self-maintaining
|
||||
**Cons:** Requires user mapping data
|
||||
|
||||
## Implementation Patterns
|
||||
|
||||
### Pattern 1: Direct User Mapping
|
||||
User email in dimension table:
|
||||
```dax
|
||||
// On Customer table
|
||||
[CustomerEmail] = USERPRINCIPALNAME()
|
||||
```
|
||||
|
||||
### Pattern 2: Security Table
|
||||
Separate table mapping users to data:
|
||||
```
|
||||
SecurityMapping table:
|
||||
| UserEmail | Region |
|
||||
|-----------|--------|
|
||||
| joe@co.com | West |
|
||||
| sue@co.com | East |
|
||||
```
|
||||
|
||||
```dax
|
||||
// On Region dimension
|
||||
[Region] IN
|
||||
SELECTCOLUMNS(
|
||||
FILTER(SecurityMapping, [UserEmail] = USERPRINCIPALNAME()),
|
||||
"Region", [Region]
|
||||
)
|
||||
```
|
||||
|
||||
### Pattern 3: Manager Hierarchy
|
||||
Users see their data plus subordinates:
|
||||
```dax
|
||||
// Using PATH functions for hierarchy
|
||||
PATHCONTAINS(Employee[ManagerPath],
|
||||
LOOKUPVALUE(Employee[EmployeeID], Employee[Email], USERPRINCIPALNAME()))
|
||||
```
|
||||
|
||||
### Pattern 4: Multiple Rules
|
||||
Combine conditions:
|
||||
```dax
|
||||
// Users see their region OR if they're a global viewer
|
||||
[Region] = LOOKUPVALUE(Users[Region], Users[Email], USERPRINCIPALNAME())
|
||||
|| LOOKUPVALUE(Users[IsGlobal], Users[Email], USERPRINCIPALNAME()) = TRUE()
|
||||
```
|
||||
|
||||
## Creating Roles via MCP
|
||||
|
||||
### List Existing Roles
|
||||
```
|
||||
security_role_operations(operation: "List")
|
||||
```
|
||||
|
||||
### Create Role with Permission
|
||||
```
|
||||
security_role_operations(
|
||||
operation: "Create",
|
||||
definitions: [{
|
||||
name: "Regional Sales",
|
||||
modelPermission: "Read",
|
||||
description: "Restricts sales data by region"
|
||||
}]
|
||||
)
|
||||
```
|
||||
|
||||
### Add Table Permission (Filter)
|
||||
```
|
||||
security_role_operations(
|
||||
operation: "CreatePermissions",
|
||||
permissionDefinitions: [{
|
||||
roleName: "Regional Sales",
|
||||
tableName: "Customer",
|
||||
filterExpression: "[Region] = USERPRINCIPALNAME()"
|
||||
}]
|
||||
)
|
||||
```
|
||||
|
||||
### Get Effective Permissions
|
||||
```
|
||||
security_role_operations(
|
||||
operation: "GetEffectivePermissions",
|
||||
references: [{ name: "Regional Sales" }]
|
||||
)
|
||||
```
|
||||
|
||||
## Testing RLS
|
||||
|
||||
### In Power BI Desktop
|
||||
1. Modeling tab > View As
|
||||
2. Select role(s) to test
|
||||
3. Optionally specify user identity
|
||||
4. Verify data filtering
|
||||
|
||||
### Test Unexpected Values
|
||||
For dynamic RLS, test:
|
||||
- Valid users
|
||||
- Unknown users (should see nothing or error gracefully)
|
||||
- NULL/blank values
|
||||
|
||||
```dax
|
||||
// Defensive pattern - returns no data for unknown users
|
||||
IF(
|
||||
USERPRINCIPALNAME() IN VALUES(SecurityMapping[UserEmail]),
|
||||
[Region] IN SELECTCOLUMNS(...),
|
||||
FALSE()
|
||||
)
|
||||
```
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### 1. RLS on Fact Tables Only
|
||||
**Problem:** Large table scans, poor performance
|
||||
**Solution:** Apply to dimension tables, let relationships propagate
|
||||
|
||||
### 2. Using LOOKUPVALUE Instead of Relationships
|
||||
**Problem:** Expensive, doesn't scale
|
||||
**Solution:** Create proper relationships, let filters flow
|
||||
|
||||
### 3. Expecting Intersection Behavior
|
||||
**Problem:** Multiple roles = UNION (additive), not intersection
|
||||
**Solution:** Design roles with union behavior in mind
|
||||
|
||||
### 4. Forgetting About DirectQuery
|
||||
**Problem:** RLS filters become WHERE clauses
|
||||
**Solution:** Ensure source database can handle the query patterns
|
||||
|
||||
### 5. Not Testing Edge Cases
|
||||
**Problem:** Users see unexpected data
|
||||
**Solution:** Test with: valid users, invalid users, multiple roles
|
||||
|
||||
## Bidirectional RLS
|
||||
|
||||
For bidirectional relationships with RLS:
|
||||
```
|
||||
Enable "Apply security filter in both directions"
|
||||
```
|
||||
|
||||
Only use when:
|
||||
- RLS requires filtering through many-to-many
|
||||
- Dimension-to-dimension security needed
|
||||
|
||||
**Caution:** Only one bidirectional relationship per path allowed.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- RLS adds WHERE clauses to every query
|
||||
- Complex DAX in filters hurts performance
|
||||
- Test with realistic user counts
|
||||
- Consider aggregations for large models
|
||||
|
||||
## Object-Level Security (OLS)
|
||||
|
||||
Restrict access to entire tables or columns:
|
||||
```
|
||||
// Via XMLA/TMSL - not available in Desktop UI
|
||||
```
|
||||
|
||||
Use for:
|
||||
- Hiding sensitive columns (salary, SSN)
|
||||
- Restricting entire tables
|
||||
- Combined with RLS for comprehensive security
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
- [ ] RLS applied to dimension tables (not fact tables)
|
||||
- [ ] Filters propagate correctly through relationships
|
||||
- [ ] Dynamic RLS uses USERPRINCIPALNAME()
|
||||
- [ ] Tested with valid and invalid users
|
||||
- [ ] Edge cases handled (NULL, unknown users)
|
||||
- [ ] Performance tested under load
|
||||
- [ ] Role mappings documented
|
||||
- [ ] Workspace roles understood (Admins bypass RLS)
|
||||
103
skills/powerbi-modeling/references/STAR-SCHEMA.md
Normal file
103
skills/powerbi-modeling/references/STAR-SCHEMA.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Star Schema Design for Power BI
|
||||
|
||||
## Overview
|
||||
|
||||
Star schema is the optimal design pattern for Power BI semantic models. It organizes data into:
|
||||
- **Dimension tables**: Enable filtering and grouping (the "one" side)
|
||||
- **Fact tables**: Enable summarization (the "many" side)
|
||||
|
||||
## Table Classification
|
||||
|
||||
### Dimension Tables
|
||||
- Contain descriptive attributes for filtering/slicing
|
||||
- Have unique key columns (one row per entity)
|
||||
- Examples: Customer, Product, Date, Geography, Employee
|
||||
- Naming convention: Singular noun (`Customer`, `Product`)
|
||||
|
||||
### Fact Tables
|
||||
- Contain measurable, quantitative data
|
||||
- Have foreign keys to dimensions
|
||||
- Store data at consistent grain (one row per transaction/event)
|
||||
- Examples: Sales, Orders, Inventory, WebVisits
|
||||
- Naming convention: Business process noun (`Sales`, `Orders`)
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Separate Dimensions from Facts
|
||||
```
|
||||
BAD: Single denormalized "Sales" table with customer details
|
||||
GOOD: "Sales" fact table + "Customer" dimension table
|
||||
```
|
||||
|
||||
### 2. Consistent Grain
|
||||
Every row in a fact table represents the same thing:
|
||||
- Order line level (most common)
|
||||
- Daily aggregation
|
||||
- Monthly summary
|
||||
|
||||
Never mix grains in one table.
|
||||
|
||||
### 3. Surrogate Keys
|
||||
Add surrogate keys when source lacks unique identifiers:
|
||||
```m
|
||||
// Power Query: Add index column
|
||||
= Table.AddIndexColumn(Source, "CustomerKey", 1, 1)
|
||||
```
|
||||
|
||||
### 4. Date Dimension
|
||||
Always create a dedicated date table:
|
||||
- Mark as date table in Power BI
|
||||
- Include fiscal periods if needed
|
||||
- Add relative date columns (IsCurrentMonth, IsPreviousYear)
|
||||
|
||||
```dax
|
||||
Date =
|
||||
ADDCOLUMNS(
|
||||
CALENDAR(DATE(2020,1,1), DATE(2030,12,31)),
|
||||
"Year", YEAR([Date]),
|
||||
"Month", FORMAT([Date], "MMMM"),
|
||||
"MonthNum", MONTH([Date]),
|
||||
"Quarter", "Q" & FORMAT([Date], "Q"),
|
||||
"WeekDay", FORMAT([Date], "dddd")
|
||||
)
|
||||
```
|
||||
|
||||
## Special Dimension Types
|
||||
|
||||
### Role-Playing Dimensions
|
||||
Same dimension used multiple times (e.g., Date for OrderDate, ShipDate):
|
||||
- Option 1: Duplicate the table (OrderDate, ShipDate tables)
|
||||
- Option 2: Use inactive relationships with USERELATIONSHIP in DAX
|
||||
|
||||
### Slowly Changing Dimensions (Type 2)
|
||||
Track historical changes with version columns:
|
||||
- StartDate, EndDate columns
|
||||
- IsCurrent flag
|
||||
- Requires pre-processing in data warehouse
|
||||
|
||||
### Junk Dimensions
|
||||
Combine low-cardinality flags into one table:
|
||||
```
|
||||
OrderFlags dimension: IsRush, IsGift, IsOnline
|
||||
```
|
||||
|
||||
### Degenerate Dimensions
|
||||
Keep transaction identifiers (OrderNumber, InvoiceID) in fact table.
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
| Anti-Pattern | Problem | Solution |
|
||||
|--------------|---------|----------|
|
||||
| Wide denormalized tables | Poor performance, hard to maintain | Split into star schema |
|
||||
| Snowflake (normalized dims) | Extra joins hurt performance | Flatten dimensions |
|
||||
| Many-to-many without bridge | Ambiguous results | Add bridge/junction table |
|
||||
| Mixed grain facts | Incorrect aggregations | Separate tables per grain |
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
- [ ] Each table is clearly dimension or fact
|
||||
- [ ] Fact tables have foreign keys to all related dimensions
|
||||
- [ ] Dimensions have unique key columns
|
||||
- [ ] Date table exists and is marked
|
||||
- [ ] No circular relationship paths
|
||||
- [ ] Consistent naming conventions
|
||||
@@ -1415,6 +1415,14 @@ a:hover {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Last Updated */
|
||||
.last-updated {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
cursor: default;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Collection Items */
|
||||
.collection-items {
|
||||
margin-top: 12px;
|
||||
|
||||
@@ -35,6 +35,13 @@ import Modal from '../components/Modal.astro';
|
||||
Has Handoffs
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="sort-select">Sort:</label>
|
||||
<select id="sort-select" aria-label="Sort by">
|
||||
<option value="title">Name (A-Z)</option>
|
||||
<option value="lastUpdated">Recently Updated</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,6 +24,13 @@ import Modal from '../components/Modal.astro';
|
||||
<label for="filter-extension">File Extension:</label>
|
||||
<select id="filter-extension" multiple aria-label="Filter by file extension"></select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="sort-select">Sort:</label>
|
||||
<select id="sort-select" aria-label="Sort by">
|
||||
<option value="title">Name (A-Z)</option>
|
||||
<option value="lastUpdated">Recently Updated</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,6 +24,13 @@ import Modal from '../components/Modal.astro';
|
||||
<label for="filter-tool">Tool:</label>
|
||||
<select id="filter-tool" multiple aria-label="Filter by tool"></select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="sort-select">Sort:</label>
|
||||
<select id="sort-select" aria-label="Sort by">
|
||||
<option value="title">Name (A-Z)</option>
|
||||
<option value="lastUpdated">Recently Updated</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -30,6 +30,13 @@ import Modal from '../components/Modal.astro';
|
||||
Has Bundled Assets
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="sort-select">Sort:</label>
|
||||
<select id="sort-select" aria-label="Sort by">
|
||||
<option value="title">Name (A-Z)</option>
|
||||
<option value="lastUpdated">Recently Updated</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { createChoices, getChoicesValues, type Choices } from '../choices';
|
||||
import { FuzzySearch, SearchItem } from '../search';
|
||||
import { fetchData, debounce, escapeHtml, getGitHubUrl, getInstallDropdownHtml, setupDropdownCloseHandlers, getActionButtonsHtml, setupActionHandlers } from '../utils';
|
||||
import { fetchData, debounce, escapeHtml, getGitHubUrl, getInstallDropdownHtml, setupDropdownCloseHandlers, getActionButtonsHtml, setupActionHandlers, getLastUpdatedHtml } from '../utils';
|
||||
import { setupModal, openFileModal } from '../modal';
|
||||
|
||||
interface Agent extends SearchItem {
|
||||
@@ -11,6 +11,7 @@ interface Agent extends SearchItem {
|
||||
model?: string;
|
||||
tools?: string[];
|
||||
hasHandoffs?: boolean;
|
||||
lastUpdated?: string | null;
|
||||
}
|
||||
|
||||
interface AgentsData {
|
||||
@@ -21,11 +22,14 @@ interface AgentsData {
|
||||
};
|
||||
}
|
||||
|
||||
type SortOption = 'title' | 'lastUpdated';
|
||||
|
||||
const resourceType = 'agent';
|
||||
let allItems: Agent[] = [];
|
||||
let search = new FuzzySearch<Agent>();
|
||||
let modelSelect: Choices;
|
||||
let toolSelect: Choices;
|
||||
let currentSort: SortOption = 'title';
|
||||
|
||||
let currentFilters = {
|
||||
models: [] as string[],
|
||||
@@ -33,6 +37,19 @@ let currentFilters = {
|
||||
hasHandoffs: false,
|
||||
};
|
||||
|
||||
function sortItems(items: Agent[]): Agent[] {
|
||||
return [...items].sort((a, b) => {
|
||||
if (currentSort === 'lastUpdated') {
|
||||
// Sort by last updated (newest first), with null/undefined at end
|
||||
const dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
|
||||
const dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
}
|
||||
// Default: sort by title
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const countEl = document.getElementById('results-count');
|
||||
@@ -59,6 +76,9 @@ function applyFiltersAndRender(): void {
|
||||
results = results.filter(item => item.hasHandoffs);
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
results = sortItems(results);
|
||||
|
||||
renderItems(results, query);
|
||||
|
||||
const activeFilters: string[] = [];
|
||||
@@ -97,6 +117,7 @@ function renderItems(items: Agent[], query = ''): void {
|
||||
${item.tools?.slice(0, 3).map(t => `<span class="resource-tag">${escapeHtml(t)}</span>`).join('') || ''}
|
||||
${item.tools && item.tools.length > 3 ? `<span class="resource-tag">+${item.tools.length - 3} more</span>` : ''}
|
||||
${item.hasHandoffs ? `<span class="resource-tag tag-handoffs">handoffs</span>` : ''}
|
||||
${getLastUpdatedHtml(item.lastUpdated)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-actions">
|
||||
@@ -123,6 +144,7 @@ export async function initAgentsPage(): Promise<void> {
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const handoffsCheckbox = document.getElementById('filter-handoffs') as HTMLInputElement;
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement;
|
||||
|
||||
const data = await fetchData<AgentsData>('agents.json');
|
||||
if (!data || !data.items) {
|
||||
@@ -149,6 +171,12 @@ export async function initAgentsPage(): Promise<void> {
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
// Initialize sort select
|
||||
sortSelect?.addEventListener('change', () => {
|
||||
currentSort = sortSelect.value as SortOption;
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
|
||||
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
|
||||
@@ -160,10 +188,12 @@ export async function initAgentsPage(): Promise<void> {
|
||||
|
||||
clearFiltersBtn?.addEventListener('click', () => {
|
||||
currentFilters = { models: [], tools: [], hasHandoffs: false };
|
||||
currentSort = 'title';
|
||||
modelSelect.removeActiveItems();
|
||||
toolSelect.removeActiveItems();
|
||||
if (handoffsCheckbox) handoffsCheckbox.checked = false;
|
||||
if (searchInput) searchInput.value = '';
|
||||
if (sortSelect) sortSelect.value = 'title';
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
*/
|
||||
import { createChoices, getChoicesValues, type Choices } from '../choices';
|
||||
import { FuzzySearch, SearchItem } from '../search';
|
||||
import { fetchData, debounce, escapeHtml, getGitHubUrl, getInstallDropdownHtml, setupDropdownCloseHandlers, getActionButtonsHtml, setupActionHandlers } from '../utils';
|
||||
import { fetchData, debounce, escapeHtml, getGitHubUrl, getInstallDropdownHtml, setupDropdownCloseHandlers, getActionButtonsHtml, setupActionHandlers, getLastUpdatedHtml } from '../utils';
|
||||
import { setupModal, openFileModal } from '../modal';
|
||||
|
||||
interface Instruction extends SearchItem {
|
||||
path: string;
|
||||
applyTo?: string;
|
||||
extensions?: string[];
|
||||
lastUpdated?: string | null;
|
||||
}
|
||||
|
||||
interface InstructionsData {
|
||||
@@ -19,11 +20,25 @@ interface InstructionsData {
|
||||
};
|
||||
}
|
||||
|
||||
type SortOption = 'title' | 'lastUpdated';
|
||||
|
||||
const resourceType = 'instruction';
|
||||
let allItems: Instruction[] = [];
|
||||
let search = new FuzzySearch<Instruction>();
|
||||
let extensionSelect: Choices;
|
||||
let currentFilters = { extensions: [] as string[] };
|
||||
let currentSort: SortOption = 'title';
|
||||
|
||||
function sortItems(items: Instruction[]): Instruction[] {
|
||||
return [...items].sort((a, b) => {
|
||||
if (currentSort === 'lastUpdated') {
|
||||
const dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
|
||||
const dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
}
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
@@ -41,6 +56,8 @@ function applyFiltersAndRender(): void {
|
||||
});
|
||||
}
|
||||
|
||||
results = sortItems(results);
|
||||
|
||||
renderItems(results, query);
|
||||
let countText = `${results.length} of ${allItems.length} instructions`;
|
||||
if (currentFilters.extensions.length > 0) {
|
||||
@@ -67,6 +84,7 @@ function renderItems(items: Instruction[], query = ''): void {
|
||||
${item.applyTo ? `<span class="resource-tag">applies to: ${escapeHtml(item.applyTo)}</span>` : ''}
|
||||
${item.extensions?.slice(0, 4).map(e => `<span class="resource-tag tag-extension">${escapeHtml(e)}</span>`).join('') || ''}
|
||||
${item.extensions && item.extensions.length > 4 ? `<span class="resource-tag">+${item.extensions.length - 4} more</span>` : ''}
|
||||
${getLastUpdatedHtml(item.lastUpdated)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-actions">
|
||||
@@ -92,6 +110,7 @@ export async function initInstructionsPage(): Promise<void> {
|
||||
const list = document.getElementById('resource-list');
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement;
|
||||
|
||||
const data = await fetchData<InstructionsData>('instructions.json');
|
||||
if (!data || !data.items) {
|
||||
@@ -109,13 +128,20 @@ export async function initInstructionsPage(): Promise<void> {
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
sortSelect?.addEventListener('change', () => {
|
||||
currentSort = sortSelect.value as SortOption;
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
|
||||
|
||||
clearFiltersBtn?.addEventListener('click', () => {
|
||||
currentFilters = { extensions: [] };
|
||||
currentSort = 'title';
|
||||
extensionSelect.removeActiveItems();
|
||||
if (searchInput) searchInput.value = '';
|
||||
if (sortSelect) sortSelect.value = 'title';
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
*/
|
||||
import { createChoices, getChoicesValues, type Choices } from '../choices';
|
||||
import { FuzzySearch, SearchItem } from '../search';
|
||||
import { fetchData, debounce, escapeHtml, getGitHubUrl, getInstallDropdownHtml, setupDropdownCloseHandlers, getActionButtonsHtml, setupActionHandlers } from '../utils';
|
||||
import { fetchData, debounce, escapeHtml, getGitHubUrl, getInstallDropdownHtml, setupDropdownCloseHandlers, getActionButtonsHtml, setupActionHandlers, getLastUpdatedHtml } from '../utils';
|
||||
import { setupModal, openFileModal } from '../modal';
|
||||
|
||||
interface Prompt extends SearchItem {
|
||||
path: string;
|
||||
tools?: string[];
|
||||
lastUpdated?: string | null;
|
||||
}
|
||||
|
||||
interface PromptsData {
|
||||
@@ -18,11 +19,25 @@ interface PromptsData {
|
||||
};
|
||||
}
|
||||
|
||||
type SortOption = 'title' | 'lastUpdated';
|
||||
|
||||
const resourceType = 'prompt';
|
||||
let allItems: Prompt[] = [];
|
||||
let search = new FuzzySearch<Prompt>();
|
||||
let toolSelect: Choices;
|
||||
let currentFilters = { tools: [] as string[] };
|
||||
let currentSort: SortOption = 'title';
|
||||
|
||||
function sortItems(items: Prompt[]): Prompt[] {
|
||||
return [...items].sort((a, b) => {
|
||||
if (currentSort === 'lastUpdated') {
|
||||
const dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
|
||||
const dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
}
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
@@ -37,6 +52,8 @@ function applyFiltersAndRender(): void {
|
||||
);
|
||||
}
|
||||
|
||||
results = sortItems(results);
|
||||
|
||||
renderItems(results, query);
|
||||
let countText = `${results.length} of ${allItems.length} prompts`;
|
||||
if (currentFilters.tools.length > 0) {
|
||||
@@ -62,6 +79,7 @@ function renderItems(items: Prompt[], query = ''): void {
|
||||
<div class="resource-meta">
|
||||
${item.tools?.slice(0, 4).map(t => `<span class="resource-tag">${escapeHtml(t)}</span>`).join('') || ''}
|
||||
${item.tools && item.tools.length > 4 ? `<span class="resource-tag">+${item.tools.length - 4} more</span>` : ''}
|
||||
${getLastUpdatedHtml(item.lastUpdated)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-actions">
|
||||
@@ -87,6 +105,7 @@ export async function initPromptsPage(): Promise<void> {
|
||||
const list = document.getElementById('resource-list');
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement;
|
||||
|
||||
const data = await fetchData<PromptsData>('prompts.json');
|
||||
if (!data || !data.items) {
|
||||
@@ -104,13 +123,20 @@ export async function initPromptsPage(): Promise<void> {
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
sortSelect?.addEventListener('change', () => {
|
||||
currentSort = sortSelect.value as SortOption;
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
searchInput?.addEventListener('input', debounce(() => applyFiltersAndRender(), 200));
|
||||
|
||||
clearFiltersBtn?.addEventListener('click', () => {
|
||||
currentFilters = { tools: [] };
|
||||
currentSort = 'title';
|
||||
toolSelect.removeActiveItems();
|
||||
if (searchInput) searchInput.value = '';
|
||||
if (sortSelect) sortSelect.value = 'title';
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getGitHubUrl,
|
||||
getRawGitHubUrl,
|
||||
showToast,
|
||||
getLastUpdatedHtml,
|
||||
} from "../utils";
|
||||
import { setupModal, openFileModal } from "../modal";
|
||||
import JSZip from "../jszip";
|
||||
@@ -27,6 +28,7 @@ interface Skill extends SearchItem {
|
||||
hasAssets: boolean;
|
||||
assetCount: number;
|
||||
files: SkillFile[];
|
||||
lastUpdated?: string | null;
|
||||
}
|
||||
|
||||
interface SkillsData {
|
||||
@@ -36,6 +38,8 @@ interface SkillsData {
|
||||
};
|
||||
}
|
||||
|
||||
type SortOption = 'title' | 'lastUpdated';
|
||||
|
||||
const resourceType = "skill";
|
||||
let allItems: Skill[] = [];
|
||||
let search = new FuzzySearch<Skill>();
|
||||
@@ -44,6 +48,18 @@ let currentFilters = {
|
||||
categories: [] as string[],
|
||||
hasAssets: false,
|
||||
};
|
||||
let currentSort: SortOption = 'title';
|
||||
|
||||
function sortItems(items: Skill[]): Skill[] {
|
||||
return [...items].sort((a, b) => {
|
||||
if (currentSort === 'lastUpdated') {
|
||||
const dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0;
|
||||
const dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
}
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
|
||||
function applyFiltersAndRender(): void {
|
||||
const searchInput = document.getElementById(
|
||||
@@ -63,6 +79,8 @@ function applyFiltersAndRender(): void {
|
||||
results = results.filter((item) => item.hasAssets);
|
||||
}
|
||||
|
||||
results = sortItems(results);
|
||||
|
||||
renderItems(results, query);
|
||||
const activeFilters: string[] = [];
|
||||
if (currentFilters.categories.length > 0)
|
||||
@@ -116,6 +134,7 @@ function renderItems(items: Skill[], query = ""): void {
|
||||
<span class="resource-tag">${item.files.length} file${
|
||||
item.files.length === 1 ? "" : "s"
|
||||
}</span>
|
||||
${getLastUpdatedHtml(item.lastUpdated)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-actions">
|
||||
@@ -236,6 +255,7 @@ export async function initSkillsPage(): Promise<void> {
|
||||
"filter-has-assets"
|
||||
) as HTMLInputElement;
|
||||
const clearFiltersBtn = document.getElementById("clear-filters");
|
||||
const sortSelect = document.getElementById("sort-select") as HTMLSelectElement;
|
||||
|
||||
const data = await fetchData<SkillsData>("skills.json");
|
||||
if (!data || !data.items) {
|
||||
@@ -262,6 +282,11 @@ export async function initSkillsPage(): Promise<void> {
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
sortSelect?.addEventListener("change", () => {
|
||||
currentSort = sortSelect.value as SortOption;
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
applyFiltersAndRender();
|
||||
searchInput?.addEventListener(
|
||||
"input",
|
||||
@@ -275,9 +300,11 @@ export async function initSkillsPage(): Promise<void> {
|
||||
|
||||
clearFiltersBtn?.addEventListener("click", () => {
|
||||
currentFilters = { categories: [], hasAssets: false };
|
||||
currentSort = 'title';
|
||||
categorySelect.removeActiveItems();
|
||||
if (hasAssetsCheckbox) hasAssetsCheckbox.checked = false;
|
||||
if (searchInput) searchInput.value = "";
|
||||
if (sortSelect) sortSelect.value = "title";
|
||||
applyFiltersAndRender();
|
||||
});
|
||||
|
||||
|
||||
@@ -429,3 +429,75 @@ export function setupActionHandlers(): void {
|
||||
|
||||
let dropdownHandlersReady = false;
|
||||
let actionHandlersReady = false;
|
||||
|
||||
/**
|
||||
* Format a date as relative time (e.g., "3 days ago")
|
||||
* @param isoDate - ISO 8601 date string
|
||||
* @returns Relative time string
|
||||
*/
|
||||
export function formatRelativeTime(isoDate: string | null | undefined): string {
|
||||
if (!isoDate) return "Unknown";
|
||||
|
||||
const date = new Date(isoDate);
|
||||
if (isNaN(date.getTime())) return "Unknown";
|
||||
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffSeconds = Math.floor(diffMs / 1000);
|
||||
const diffMinutes = Math.floor(diffSeconds / 60);
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
const diffWeeks = Math.floor(diffDays / 7);
|
||||
const diffMonths = Math.floor(diffDays / 30);
|
||||
const diffYears = Math.floor(diffDays / 365);
|
||||
|
||||
if (diffDays === 0) {
|
||||
if (diffHours === 0) {
|
||||
if (diffMinutes === 0) return "just now";
|
||||
return diffMinutes === 1 ? "1 minute ago" : `${diffMinutes} minutes ago`;
|
||||
}
|
||||
return diffHours === 1 ? "1 hour ago" : `${diffHours} hours ago`;
|
||||
}
|
||||
if (diffDays === 1) return "yesterday";
|
||||
if (diffDays < 7) return `${diffDays} days ago`;
|
||||
if (diffWeeks === 1) return "1 week ago";
|
||||
if (diffWeeks < 4) return `${diffWeeks} weeks ago`;
|
||||
if (diffMonths === 1) return "1 month ago";
|
||||
if (diffMonths < 12) return `${diffMonths} months ago`;
|
||||
if (diffYears === 1) return "1 year ago";
|
||||
return `${diffYears} years ago`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date for display (e.g., "January 15, 2026")
|
||||
* @param isoDate - ISO 8601 date string
|
||||
* @returns Formatted date string
|
||||
*/
|
||||
export function formatFullDate(isoDate: string | null | undefined): string {
|
||||
if (!isoDate) return "Unknown";
|
||||
|
||||
const date = new Date(isoDate);
|
||||
if (isNaN(date.getTime())) return "Unknown";
|
||||
|
||||
return date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML for displaying last updated time with hover tooltip
|
||||
* @param isoDate - ISO 8601 date string
|
||||
* @returns HTML string with relative time and title attribute
|
||||
*/
|
||||
export function getLastUpdatedHtml(isoDate: string | null | undefined): string {
|
||||
const relativeTime = formatRelativeTime(isoDate);
|
||||
const fullDate = formatFullDate(isoDate);
|
||||
|
||||
if (relativeTime === "Unknown") {
|
||||
return `<span class="last-updated">Updated: Unknown</span>`;
|
||||
}
|
||||
|
||||
return `<span class="last-updated" title="${escapeHtml(fullDate)}">Updated ${relativeTime}</span>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user