diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6587fc4..101c237 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,9 @@ jobs: - name: Build run: npm run build + - name: Install playwright + run: npx playwright install + - name: Run tests run: npm test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ae7bbe3..949e4f5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,31 +1,23 @@ -name: Publish to GitHub Packages +name: Publish to NPM on: release: - types: [created] - workflow_dispatch: - + types: [published] +permissions: + contents: write jobs: publish: runs-on: ubuntu-latest - permissions: - contents: read - packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: '24.x' registry-url: 'https://registry.npmjs.org' - run: npm ci - - - run: npm test - - - name: Configure npm for GitHub Packages - run: | - echo "//npm.pkg.github.com/:_authToken=${{ secrets.GH_PAT }}" > ~/.npmrc - echo "@dallasbpeters:registry=https://npm.pkg.github.com" >> ~/.npmrc - + - run: npm run build - run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index d83ee98..714dc93 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,7 @@ logs/ .cursor/rules/byterover-rules.mdc .kiro/steering/byterover-rules.md .qoder/rules/byterover-rules.md -.augment/rules/byterover-rules.md \ No newline at end of file +.augment/rules/byterover-rules.md + +# Vitest +spec/**/__screenshots__/* diff --git a/README.md b/README.md index 1ed408e..45e65d2 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ graph TB subgraph "Optics MCP Server" SERVER[MCP Server
stdio transport] - + subgraph "Resources (13)" SYSTEM[optics://system-overview] DOC_INTRO[optics://documentation/introduction] @@ -52,7 +52,7 @@ graph TB TOK_TYPO[optics://tokens/typography] COMP_ALL[optics://components/all] end - + subgraph "Core Tools (7)" T1[get_token] T2[search_tokens] @@ -62,7 +62,7 @@ graph TB T6[get_component_tokens] T7[search_documentation] end - + subgraph "Advanced Tools (7)" T8[generate_theme] T9[validate_token_usage] @@ -72,7 +72,7 @@ graph TB T13[generate_component_scaffold] T14[generate_sticker_sheet] end - + subgraph "Prompts (5)" P1[start-here] P2[get-token-reference] @@ -80,14 +80,14 @@ graph TB P4[theme-customization] P5[migration-guide] end - + subgraph "Data Layer" TOKENS[83 Design Tokens
HSL colors, spacing,
typography, borders, shadows] COMPONENTS[24 Components
with token dependencies] DOCS[Documentation
Guidelines & best practices] end end - + CLIENT -->|JSON-RPC| SERVER SERVER --> SYSTEM SERVER --> DOC_INTRO @@ -152,16 +152,20 @@ graph TB 1. Command Palette → **MCP: Open User Configuration** 2. Add this configuration: - ```json - { - "servers": { - "optics": { - "command": "npx", - "args": ["-y", "optics-mcp"] - } - } - } - ``` + +```json +{ + "servers": { + "optics": { + "command": "npx", + "args": [ + "@rolemodel/optics-mcp@latest" + ] + } + } +} +``` + 3. Open GitHub Copilot in **Agent Mode** 4. Click the tools icon to see Optics tools available @@ -180,16 +184,20 @@ cursor://anysphere.cursor-deeplink/mcp/install?name=optics&config=eyJvcHRpY3MiOn 1. Open Cursor Settings → **MCP** 2. Add this configuration: - ```json - { - "servers": { - "optics": { - "command": "npx", - "args": ["-y", "optics-mcp"] - } - } - } - ``` + +```json +{ + "servers": { + "optics": { + "command": "npx", + "args": [ + "@rolemodel/optics-mcp@latest" + ] + } + } +} +``` + 3. Chat with Cursor AI to access Optics tools ### Quick Start (Zero-Install) ⚡ @@ -205,7 +213,9 @@ Add to your MCP configuration: "mcpServers": { "optics": { "command": "npx", - "args": ["-y", "optics-mcp"] + "args": [ + "@rolemodel/optics-mcp@latest" + ] } } } diff --git a/editor-configs/cursor-mcp.json b/editor-configs/cursor-mcp.json new file mode 100644 index 0000000..b3ef8ac --- /dev/null +++ b/editor-configs/cursor-mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "optics": { + "command": "npx", + "args": ["-y", "@rolemodel/optics-mcp"], + "env": {} + } + } +} diff --git a/package-lock.json b/package-lock.json index cc3e0e5..5d03f24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "optics-mcp", - "version": "0.2.6", + "name": "@rolemodel/optics-mcp", + "version": "0.2.7", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "optics-mcp", - "version": "0.2.6", + "name": "@rolemodel/optics-mcp", + "version": "0.2.7", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.24.0", @@ -16,14 +16,33 @@ "optics-mcp": "dist/index.js" }, "devDependencies": { + "@rolemodel/optics": "^2.3.0", "@types/node": "^20.10.0", + "@vitest/browser-playwright": "^4.0.18", + "@vitest/ui": "^4.0.18", "esbuild": "^0.27.2", - "typescript": "^5.9.3" + "playwright": "^1.58.1", + "ts-node": "^10.9.2", + "typescript": "^5.9.3", + "vitest": "^4.0.18" }, "engines": { "node": ">=24.13.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -478,6 +497,34 @@ "hono": "^4" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.26.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", @@ -517,192 +564,870 @@ } } }, - "node_modules/@types/node": { - "version": "20.19.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.31.tgz", - "integrity": "sha512-5jsi0wpncvTD33Sh1UCgacK37FFwDn+EG7wCmEvs62fCvBL+n8/76cAYDok21NF6+jaVWIqKwCZyX7Vbu8eB3A==", + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } + "license": "MIT" }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "node_modules/@rolemodel/optics": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@rolemodel/optics/-/optics-2.3.0.tgz", + "integrity": "sha512-retjCKOscYSvLAh9aWRDPGnZRmP6+pc2yZNivU2WRFrp2FeaLfcw9g5oKvw9Lv03yEXL1dT1yQ6bUXeFjp7jkA==", + "dev": true, "license": "MIT", "dependencies": { - "ajv": "^8.0.0" + "modern-css-reset": "^1.4.0" }, "peerDependencies": { - "ajv": "^8.0.0" + "tom-select": "^2.0.0" }, "peerDependenciesMeta": { - "ajv": { + "tom-select": { "optional": true } } }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "engines": { - "node": ">= 0.6" - } + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "engines": { - "node": ">=6.6.0" - } + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": { - "ms": "^2.1.3" + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.31.tgz", + "integrity": "sha512-5jsi0wpncvTD33Sh1UCgacK37FFwDn+EG7wCmEvs62fCvBL+n8/76cAYDok21NF6+jaVWIqKwCZyX7Vbu8eB3A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/browser": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.18.tgz", + "integrity": "sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/mocker": "4.0.18", + "@vitest/utils": "4.0.18", + "magic-string": "^0.30.21", + "pixelmatch": "7.1.0", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.0.3", + "ws": "^8.18.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.18" + } + }, + "node_modules/@vitest/browser-playwright": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.18.tgz", + "integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/browser": "4.0.18", + "@vitest/mocker": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": false + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.18.tgz", + "integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/utils": "4.0.18", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.18" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -721,6 +1446,16 @@ "node": ">= 0.8" } }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -763,6 +1498,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -821,6 +1563,16 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -850,6 +1602,16 @@ "node": ">=18.0.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -932,6 +1694,31 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -952,6 +1739,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -968,6 +1762,21 @@ "node": ">= 0.8" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1141,6 +1950,23 @@ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1191,11 +2017,47 @@ "url": "https://opencollective.com/express" } }, + "node_modules/modern-css-reset": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/modern-css-reset/-/modern-css-reset-1.4.0.tgz", + "integrity": "sha512-0crZmSFmrxkI7159rvQWjpDhy0u4+Awg/iOycJdlVn0RSeft/a+6BrQHR3IqvmdK25sqt0o6Z5Ap7cWgUee2rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -1224,6 +2086,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -1232,50 +2105,163 @@ "ee-first": "1.1.1" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pixelmatch": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz", + "integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^7.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/playwright": { + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz", + "integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "playwright-core": "1.58.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "node_modules/playwright-core": { + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz", + "integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, "engines": { - "node": ">= 0.8" + "node": ">=18" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=14.19.0" } }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, "engines": { - "node": ">=16.20.0" + "node": "^10 || ^12 || >=14" } }, "node_modules/proxy-addr": { @@ -1335,6 +2321,51 @@ "node": ">=0.10.0" } }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1492,6 +2523,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1500,6 +2570,57 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1508,6 +2629,60 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1527,6 +2702,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1550,6 +2726,13 @@ "node": ">= 0.8" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1559,6 +2742,176 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1574,11 +2927,60 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 10dc0ca..4eb80c7 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "optics-mcp", - "version": "0.2.6", - "mcpName": "io.github.RoleModel/optics-mcp", + "name": "@rolemodel/optics-mcp", + "version": "0.2.7", "description": "MCP Server for Optics Design System documentation and token usage with AI-optimized comprehension guide", + "mcpName": "io.github.rolemodel/optics-mcp", "main": "dist/index.js", "type": "module", "bin": { @@ -14,8 +14,9 @@ "prepare": "npm run build", "watch": "npx tsc --watch", "start": "node dist/index.js", - "test": "node dist/test.js", - "test:interactive": "npm run build && node dist/interactive-client.js" + "test": "NODE_ENV=test vitest", + "test:interactive": "npm run build && node dist/interactive-client.js", + "sync-data": "npx ts-node scripts/sync-optics-data.ts" }, "keywords": [ "mcp", @@ -42,9 +43,15 @@ "zod": "^3.25.76" }, "devDependencies": { + "@rolemodel/optics": "^2.3.0", "@types/node": "^20.10.0", + "@vitest/browser-playwright": "^4.0.18", + "@vitest/ui": "^4.0.18", "esbuild": "^0.27.2", - "typescript": "^5.9.3" + "playwright": "^1.58.1", + "ts-node": "^10.9.2", + "typescript": "^5.9.3", + "vitest": "^4.0.18" }, "engines": { "node": ">=24.13.0" @@ -53,10 +60,8 @@ "dist", "README.md", "LICENSE", - "docs/SYSTEM_OVERVIEW.md", - "docs/AI_IMPROVEMENTS.md", - "docs/WARP.md", - "docs/TOOLS.md", - "docs/EXAMPLES.md" + "docs", + "editor-configs", + ".claude/commands" ] } diff --git a/scripts/sync-optics-data.ts b/scripts/sync-optics-data.ts new file mode 100644 index 0000000..3bd75f1 --- /dev/null +++ b/scripts/sync-optics-data.ts @@ -0,0 +1,872 @@ +#!/usr/bin/env npx ts-node + +/** + * Sync Optics Data Script + * Run: npm run sync-data + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +// ESM compatibility +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Configuration +const OPTICS_PACKAGE = '@rolemodel/optics'; +const OPTICS_SOURCE_REPO = path.join(__dirname, '../../optics'); // Assumes optics repo is sibling +const OUTPUT_FILE = path.join(__dirname, '../src/optics-data.ts'); + +// Token categories matching Optics Storybook +type TokenCategory = + | 'animation' + | 'border' + | 'breakpoint' + | 'color' + | 'encoded-image' + | 'input' + | 'opacity' + | 'shadow' + | 'sizing' + | 'spacing' + | 'typography' + | 'z-index'; + +interface DesignToken { + name: string; + cssVar: string; + value: string; + category: TokenCategory; + description: string; +} + +interface CSSPattern { + name: string; + description: string; + className: string; + type: 'component' | 'layout' | 'utility'; + modifiers: string[]; + elements: string[]; + exampleHtml: string; + docsUrl: string; +} + +interface Documentation { + section: string; + title: string; + content: string; + tokens: string[]; +} + +// ============================================================================ +// Find Optics paths +// ============================================================================ + +function findOpticsPackage(): string | null { + const possiblePaths = [ + path.join(__dirname, '../node_modules', OPTICS_PACKAGE), + path.join(__dirname, '../../node_modules', OPTICS_PACKAGE), + ]; + + for (const p of possiblePaths) { + if (fs.existsSync(p)) { + return p; + } + } + return null; +} + +function findOpticsSourceRepo(): string | null { + if (fs.existsSync(OPTICS_SOURCE_REPO) && fs.existsSync(path.join(OPTICS_SOURCE_REPO, 'src/stories'))) { + return OPTICS_SOURCE_REPO; + } + return null; +} + +// ============================================================================ +// Token Extraction from tokens.json +// ============================================================================ + +function flattenTokens(obj: any, prefix: string = ''): DesignToken[] { + const tokens: DesignToken[] = []; + + for (const [key, value] of Object.entries(obj)) { + const name = prefix ? `${prefix}-${key}` : key; + + if (typeof value === 'string') { + const category = categorizeToken(name); + tokens.push({ + name, + cssVar: `--op-${name}`, + value, + category, + description: generateTokenDescription(name, category), + }); + } else if (typeof value === 'object' && value !== null) { + tokens.push(...flattenTokens(value, name)); + } + } + + return tokens; +} + +function categorizeToken(name: string): TokenCategory { + if (name.includes('z-index') || name.startsWith('z-')) return 'z-index'; + if (name.startsWith('animation-') || name.startsWith('transition-') || name.startsWith('duration-') || name.startsWith('easing-')) return 'animation'; + if (name.startsWith('radius-') || name.startsWith('border-')) return 'border'; + if (name.startsWith('breakpoint-') || name.startsWith('screen-')) return 'breakpoint'; + if (name.startsWith('input-')) return 'input'; + if (name.startsWith('opacity-') || name.includes('-opacity')) return 'opacity'; + if (name.startsWith('shadow-') || name.includes('-shadow')) return 'shadow'; + if (name.startsWith('size-') || name.includes('-width') || name.includes('-height')) return 'sizing'; + if (name.startsWith('space-') || name.startsWith('gap-')) return 'spacing'; + if (name.startsWith('font-') || name.startsWith('line-height') || name.startsWith('letter-') || name.startsWith('text-')) return 'typography'; + if (name.startsWith('encoded-') || name.startsWith('image-')) return 'encoded-image'; + if (name.startsWith('color-') || name.includes('-color-') || name.includes('-on-') || name.includes('-base') || name.includes('-plus-') || name.includes('-minus-')) return 'color'; + return 'sizing'; +} + +function generateTokenDescription(name: string, category: TokenCategory): string { + if (category === 'color') { + if (name.includes('-on-')) { + const parts = name.split('-on-'); + return `Text color for use ON ${parts[1]} background. MUST be paired with matching background color.`; + } + if (name.endsWith('-h')) return `Hue component (HSL) for ${name.replace('-h', '')}`; + if (name.endsWith('-s')) return `Saturation component (HSL) for ${name.replace('-s', '')}`; + if (name.endsWith('-l')) return `Lightness component (HSL) for ${name.replace('-l', '')}`; + } + return `${category} token: ${name}`; +} + +// ============================================================================ +// CSS Token Extraction from dist CSS (fallback) +// ============================================================================ + +function extractTokensFromCSS(cssContent: string): DesignToken[] { + const tokens: DesignToken[] = []; + const customPropRegex = /--op-([a-z0-9-]+):\s*([^;]+);/g; + let match; + + while ((match = customPropRegex.exec(cssContent)) !== null) { + const name = match[1]; + const value = match[2].trim(); + const category = categorizeToken(name); + + tokens.push({ + name, + cssVar: `--op-${name}`, + value, + category, + description: generateTokenDescription(name, category), + }); + } + + return tokens; +} + +// ============================================================================ +// CSS Class Extraction for validation +// ============================================================================ + +function extractCSSClasses(cssContent: string): Set { + const classes = new Set(); + const classRegex = /\.([a-z][a-z0-9_-]*)/g; + let match; + + while ((match = classRegex.exec(cssContent)) !== null) { + classes.add(match[1]); + } + + return classes; +} + +// ============================================================================ +// Storybook MDX Parsing +// ============================================================================ + +interface ClassDoc { + className: string; + description: string; + isModifier: boolean; + baseClass?: string; +} + +function parseComponentMdx(mdxPath: string, componentName: string): { + componentDescription: string; + classes: ClassDoc[]; +} { + const content = fs.readFileSync(mdxPath, 'utf-8'); + + // Extract description (first paragraph after # ComponentName) + const descMatch = content.match(new RegExp(`# ${componentName}[\\s\\S]*?\\n\\n([^#\\n<][^\\n]+)`)); + const componentDescription = descMatch ? descMatch[1].trim() : ''; + + // Extract ALL class documentation: `.class-name` Description text + const classes: ClassDoc[] = []; + const classDocRegex = /`\.([a-z][a-z0-9_-]*(?:--[a-z0-9-]+)?)`\s+([^`\n]+)/g; + let classMatch; + + while ((classMatch = classDocRegex.exec(content)) !== null) { + const className = classMatch[1]; + const description = classMatch[2].trim(); + const isModifier = className.includes('--'); + const baseClass = isModifier ? className.split('--')[0] : undefined; + + classes.push({ className, description, isModifier, baseClass }); + } + + return { componentDescription, classes }; +} + +function getBaseClasses(classes: ClassDoc[]): string[] { + // Get unique base classes (non-modifiers) + const baseClasses = classes + .filter(c => !c.isModifier) + .map(c => c.className); + return [...new Set(baseClasses)]; +} + +// ============================================================================ +// Storybook Stories Parsing +// ============================================================================ + +function parseStoriesFile(storiesPath: string): { variants: string[]; sizes: string[] } { + const content = fs.readFileSync(storiesPath, 'utf-8'); + + // Extract variant options + const variantMatch = content.match(/variant:\s*\{[^}]*options:\s*\[([^\]]+)\]/); + const variants = variantMatch + ? variantMatch[1].split(',').map(v => v.trim().replace(/['"]/g, '')) + : []; + + // Extract size options + const sizeMatch = content.match(/size:\s*\{[^}]*options:\s*\[([^\]]+)\]/); + const sizes = sizeMatch + ? sizeMatch[1].split(',').map(s => s.trim().replace(/['"]/g, '')) + : []; + + return { variants, sizes }; +} + +// ============================================================================ +// Scrape Components from Storybook Source +// NOTE: Requires the Optics git repo to be cloned at ../../optics +// Storybook files are NOT included in the npm package +// ============================================================================ + +function scrapeComponentsFromStorybook(sourceRepo: string, opticsPackagePath: string, validClasses: Set): CSSPattern[] { + const patterns: CSSPattern[] = []; + const componentsDir = path.join(sourceRepo, 'src/stories/Components'); + const cssComponentsDir = path.join(opticsPackagePath, 'dist/css/components'); + + const processDirectory = (dir: string, type: 'component' | 'layout' | 'utility') => { + if (!fs.existsSync(dir)) return; + + const items = fs.readdirSync(dir); + + for (const item of items) { + const itemPath = path.join(dir, item); + if (!fs.statSync(itemPath).isDirectory()) continue; + + const mdxPath = path.join(itemPath, `${item}.mdx`); + const storiesPath = path.join(itemPath, `${item}.stories.js`); + + if (!fs.existsSync(mdxPath)) continue; + + console.log(` Parsing ${item}...`); + + // Parse MDX for ALL class documentation + const mdxData = parseComponentMdx(mdxPath, item); + const baseClasses = getBaseClasses(mdxData.classes); + + // Parse stories for variants + const storiesData = fs.existsSync(storiesPath) ? parseStoriesFile(storiesPath) : { variants: [], sizes: [] }; + + // For multi-class components (like Form), use primary class, store all classes + if (baseClasses.length > 1) { + // Use first class as primary, collect all modifiers/elements across all base classes + const primaryClass = baseClasses[0]; + const allModifiers: string[] = []; + const allElements: string[] = []; + + for (const baseClass of baseClasses) { + if (!validClasses.has(baseClass)) continue; + + // Collect modifiers + const mods = Array.from(validClasses).filter(c => c.startsWith(`${baseClass}--`)); + allModifiers.push(...mods); + + // Collect elements + const elems = Array.from(validClasses).filter(c => c.startsWith(`${baseClass}__`)); + allElements.push(...elems); + } + + patterns.push({ + name: item, + description: mdxData.componentDescription || `${item} component`, + className: primaryClass, + type, + modifiers: [...new Set(allModifiers)].sort(), + elements: [...new Set([...baseClasses.slice(1), ...allElements])].sort(), // Include other base classes as "elements" + exampleHtml: generateExampleHtml(primaryClass, [], allElements), + docsUrl: `https://docs.optics.rolemodel.design/?path=/docs/components-${item.toLowerCase()}--docs`, + }); + } else { + // Single base class component - use first base class or derive from component name + const className = baseClasses[0] || deriveClassName(item, validClasses); + if (!className || !validClasses.has(className)) { + console.log(` ⚠️ No valid base class found for ${item}`); + continue; + } + + // Build modifiers list from MDX and validate against actual CSS + let modifiers = mdxData.classes + .filter(c => c.isModifier) + .map(c => c.className) + .filter(m => validClasses.has(m)); + + // Add variant-based modifiers from stories + for (const variant of storiesData.variants) { + if (variant !== 'default') { + const modClass = `${className}--${variant}`; + if (validClasses.has(modClass) && !modifiers.includes(modClass)) { + modifiers.push(modClass); + } + } + } + + // Add size-based modifiers from stories + for (const size of storiesData.sizes) { + const sizeClass = `${className}--${size}`; + if (validClasses.has(sizeClass) && !modifiers.includes(sizeClass)) { + modifiers.push(sizeClass); + } + } + + // Also find modifiers from CSS + const cssModifiers = Array.from(validClasses) + .filter(c => c.startsWith(`${className}--`)); + + modifiers = [...new Set([...modifiers, ...cssModifiers])].sort(); + + // Extract elements (BEM __element classes) + const elements = Array.from(validClasses) + .filter(c => c.startsWith(`${className}__`)) + .sort(); + + patterns.push({ + name: item, + description: mdxData.componentDescription || `${item} component`, + className, + type, + modifiers, + elements, + exampleHtml: generateExampleHtml(className, modifiers, elements), + docsUrl: `https://docs.optics.rolemodel.design/?path=/docs/${type === 'component' ? 'components' : type === 'layout' ? 'layout' : 'utilities'}-${item.toLowerCase()}--docs`, + }); + } + } + }; + + // Helper to derive class name from component name + function deriveClassName(componentName: string, validClasses: Set): string | null { + // Try common patterns + const patterns = [ + componentName.toLowerCase(), + componentName.toLowerCase().replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, ''), + componentName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, ''), + ]; + + for (const pattern of patterns) { + if (validClasses.has(pattern)) return pattern; + } + + // Try finding a class that starts with component name + const prefix = componentName.toLowerCase(); + for (const cls of validClasses) { + if (cls.startsWith(prefix) && !cls.includes('--') && !cls.includes('__')) { + return cls; + } + } + + return null; + } + + processDirectory(componentsDir, 'component'); + // Skip utilities - they're CSS helpers, not components + + // Add layout utilities manually (op-stack, op-cluster, op-split) + const layoutPatterns = ['op-stack', 'op-cluster', 'op-split']; + for (const className of layoutPatterns) { + if (validClasses.has(className)) { + patterns.push({ + name: className.replace('op-', '').charAt(0).toUpperCase() + className.replace('op-', '').slice(1), + description: `Layout utility: ${className}`, + className, + type: 'layout', + modifiers: [], + elements: [], + exampleHtml: `
...
`, + docsUrl: `https://docs.optics.rolemodel.design/?path=/docs/layout-${className.replace('op-', '')}--docs`, + }); + } + } + + // Add data-attribute patterns (Tooltip, etc.) + patterns.push(...extractDataAttributePatterns(cssComponentsDir)); + + return patterns; +} + +// Extract patterns that use data attributes instead of classes (e.g., Tooltip) +function extractDataAttributePatterns(componentsDir: string): CSSPattern[] { + const patterns: CSSPattern[] = []; + + if (!fs.existsSync(componentsDir)) return patterns; + + const dataAttrComponents: Record = { + 'tooltip.css': { + attr: 'data-tooltip-text', + description: 'CSS-only tooltip using data attributes', + positions: ['top', 'bottom', 'left', 'right'] + } + }; + + for (const [cssFile, config] of Object.entries(dataAttrComponents)) { + const cssPath = path.join(componentsDir, cssFile); + if (!fs.existsSync(cssPath)) continue; + + const name = cssFile.replace('.css', '').split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''); + + patterns.push({ + name, + description: config.description, + className: `[${config.attr}]`, + type: 'component', + modifiers: config.positions?.map(p => `[data-tooltip-position="${p}"]`) || [], + elements: [], + exampleHtml: ``, + docsUrl: `https://docs.optics.rolemodel.design/?path=/docs/components-${cssFile.replace('.css', '')}--docs`, + }); + } + + return patterns; +} + +function generateExampleHtml(className: string, modifiers: string[], elements: string[]): string { + if (elements.length > 0) { + const elementsHtml = elements + .slice(0, 3) + .map(el => `
...
`) + .join('\n'); + return `
\n${elementsHtml}\n
`; + } + + if (className === 'btn') { + return ``; + } + + return `
...
`; +} + +// ============================================================================ +// Derive Components from CSS Files (when Storybook not available) +// Uses the component CSS files in dist/css/components/ from npm package +// ============================================================================ + +function deriveComponentsFromCSS(opticsPath: string, validClasses: Set): CSSPattern[] { + const patterns: CSSPattern[] = []; + const componentsDir = path.join(opticsPath, 'dist/css/components'); + + if (!fs.existsSync(componentsDir)) { + console.log(' ⚠️ Components CSS directory not found'); + return patterns; + } + + const cssFiles = fs.readdirSync(componentsDir).filter(f => f.endsWith('.css') && f !== 'index.css'); + + for (const cssFile of cssFiles) { + const cssPath = path.join(componentsDir, cssFile); + const cssContent = fs.readFileSync(cssPath, 'utf-8'); + + // Extract all classes from this CSS file + const fileClasses = new Set(); + const classRegex = /\.([a-z][a-z0-9_-]*)/g; + let match; + while ((match = classRegex.exec(cssContent)) !== null) { + fileClasses.add(match[1]); + } + + // Find base classes (no -- or __) + const baseClasses = Array.from(fileClasses).filter(c => !c.includes('--') && !c.includes('__')); + + // Group classes by their base + for (const baseClass of baseClasses) { + if (!validClasses.has(baseClass)) continue; + + // Skip if we already have this base class + if (patterns.some(p => p.className === baseClass)) continue; + + // Find modifiers and elements for this base class + const modifiers = Array.from(fileClasses) + .filter(c => c.startsWith(`${baseClass}--`)) + .sort(); + + const elements = Array.from(fileClasses) + .filter(c => c.startsWith(`${baseClass}__`)) + .sort(); + + // Generate readable name from class + const name = baseClass + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(''); + + // Generate description from CSS file header comment if available + const headerMatch = cssContent.match(/\/\*\*?\s*\n?\s*\*?\s*([^*\n]+)/); + const description = headerMatch ? headerMatch[1].trim() : `${name} component`; + + patterns.push({ + name, + description, + className: baseClass, + type: 'component', + modifiers, + elements, + exampleHtml: generateExampleHtml(baseClass, modifiers, elements), + docsUrl: `https://docs.optics.rolemodel.design/?path=/docs/components-${cssFile.replace('.css', '')}--docs`, + }); + } + } + + // Add layout utilities (these are in core/utilities, not components) + const layoutPatterns = ['op-stack', 'op-cluster', 'op-split']; + for (const className of layoutPatterns) { + if (validClasses.has(className)) { + const name = className.replace('op-', '').charAt(0).toUpperCase() + className.replace('op-', '').slice(1); + patterns.push({ + name, + description: `Layout utility: ${className}`, + className, + type: 'layout', + modifiers: Array.from(validClasses).filter(c => c.startsWith(`${className}--`)), + elements: [], + exampleHtml: `
...
`, + docsUrl: `https://docs.optics.rolemodel.design/?path=/docs/layout-${className.replace('op-', '')}--docs`, + }); + } + } + + return patterns; +} + +// ============================================================================ +// Documentation - Preserve existing documentation from optics-data.ts +// ============================================================================ + +interface ExistingDoc { + section: string; + title: string; + content: string; + tokens?: string[]; +} + +function readExistingDocumentation(): ExistingDoc[] { + // Read existing optics-data.ts to preserve manually written documentation + if (!fs.existsSync(OUTPUT_FILE)) { + return getDefaultDocumentation(); + } + + const content = fs.readFileSync(OUTPUT_FILE, 'utf-8'); + + // Extract documentation array from file + const docMatch = content.match(/export const documentation: Documentation\[\] = \[([\s\S]*?)\];(?=\s*$|\s*\/\/|\s*export)/); + if (!docMatch) { + return getDefaultDocumentation(); + } + + try { + // Parse the documentation array (it's JSON-like) + const docArrayStr = '[' + docMatch[1] + ']'; + // Remove trailing commas before parsing + const cleanedStr = docArrayStr.replace(/,(\s*[\]}])/g, '$1'); + return JSON.parse(cleanedStr); + } catch { + console.log(' ⚠️ Could not parse existing documentation, using defaults'); + return getDefaultDocumentation(); + } +} + +function getDefaultDocumentation(): ExistingDoc[] { + return [ + { + section: 'overview', + title: 'Optics Overview', + content: 'Optics is a CSS-only design system. It provides CSS custom properties (tokens) and utility classes - NOT JavaScript components. Use the provided CSS classes and tokens; do not write custom CSS for patterns that already exist.' + }, + { + section: 'color-pairing', + title: 'Color Pairing Rule', + content: 'CRITICAL: Background and text colors must ALWAYS be paired. Never use --op-color-{family}-{scale} without also setting color to --op-color-{family}-on-{scale}. The "on" tokens are calculated for proper contrast against their matching background.' + }, + { + section: 'color-system', + title: 'HSL Color System', + content: 'Optics uses HSL-based colors defined by -h (hue), -s (saturation), -l (lightness) tokens. A full scale is generated from plus-max (lightest) to minus-max (darkest). Each scale step has a matching "on-" token for text.' + }, + { + section: 'use-existing', + title: 'Use Existing Classes', + content: 'Don\'t write custom CSS for components that already exist. Use .btn for buttons, .card for cards, .op-stack/.op-cluster/.op-split for layouts. Only write custom CSS when truly extending the system.' + } + ]; +} + +function getDocumentation(tokens: DesignToken[]): Documentation[] { + // Preserve existing manually-written documentation + const existing = readExistingDocumentation(); + + // Add tokens field based on section content + return existing.map((doc: ExistingDoc): Documentation => { + let sectionTokens: string[] = []; + + // Auto-populate tokens based on section type + if (doc.section === 'color-system' || doc.section === 'color-pairing') { + sectionTokens = tokens.filter((t: DesignToken) => t.category === 'color').map((t: DesignToken) => t.name); + } else if (doc.section === 'spacing') { + sectionTokens = tokens.filter((t: DesignToken) => t.category === 'spacing').map((t: DesignToken) => t.name); + } else if (doc.section === 'typography') { + sectionTokens = tokens.filter((t: DesignToken) => t.category === 'typography').map((t: DesignToken) => t.name); + } + + return { + section: doc.section, + title: doc.title, + content: doc.content, + tokens: doc.tokens || sectionTokens + }; + }); +} + +// ============================================================================ +// Output Generation +// ============================================================================ + +function generateOutputFile( + tokens: DesignToken[], + patterns: CSSPattern[], + docs: Documentation[], + version: string +): string { + const tokenCategories = Array.from(new Set(tokens.map(t => t.category))).sort(); + + return `/** + * Optics Design System Data + * AUTO-GENERATED - Run: npm run sync-data + * Version: ${version} | Generated: ${new Date().toISOString()} + */ + +export type TokenCategory = ${tokenCategories.map(c => `'${c}'`).join(' | ')}; + +export interface DesignToken { + name: string; + cssVar: string; + value: string; + category: TokenCategory; + description: string; +} + +export interface CSSPattern { + name: string; + description: string; + className: string; + type: 'component' | 'layout' | 'utility'; + modifiers: string[]; + elements: string[]; + exampleHtml: string; + docsUrl: string; +} + +export interface Component extends CSSPattern { + tokens: string[]; + usage: string; + examples: string[]; +} + +export interface Documentation { + section: string; + title: string; + content: string; + tokens: string[]; +} + +export const designTokens: DesignToken[] = ${JSON.stringify(tokens, null, 2)}; + +export const cssPatterns: CSSPattern[] = ${JSON.stringify(patterns, null, 2)}; + +// Backwards compatibility: components alias with extended interface +export const components: Component[] = cssPatterns.map(p => ({ + ...p, + tokens: p.modifiers, + usage: p.description, + examples: p.exampleHtml ? [p.exampleHtml] : [], +})); + +export const documentation: Documentation[] = ${JSON.stringify(docs, null, 2)}; +`; +} + +// ============================================================================ +// Main +// ============================================================================ + +async function main() { + console.log('🔄 Syncing Optics data...\n'); + + // Find Optics package + const opticsPath = findOpticsPackage(); + if (!opticsPath) { + console.error('❌ Could not find @rolemodel/optics package'); + console.error(' Run: npm install @rolemodel/optics'); + process.exit(1); + } + console.log(`📦 Found Optics package at: ${opticsPath}`); + + // Find Optics source repo + const sourceRepo = findOpticsSourceRepo(); + if (sourceRepo) { + console.log(`📂 Found Optics source repo at: ${sourceRepo}`); + } else { + console.log(`⚠️ Optics source repo not found at ${OPTICS_SOURCE_REPO}`); + console.log(' Component data will use fallback patterns'); + } + + // Read package version + const packageJson = JSON.parse(fs.readFileSync(path.join(opticsPath, 'package.json'), 'utf-8')); + const version = packageJson.version; + console.log(`📌 Version: ${version}\n`); + + // 1. Extract tokens from tokens.json if available + let allTokens: DesignToken[] = []; + const tokensJsonPath = path.join(opticsPath, 'dist/tokens/tokens.json'); + + if (fs.existsSync(tokensJsonPath)) { + console.log('📊 Parsing tokens.json...'); + const tokensJson = JSON.parse(fs.readFileSync(tokensJsonPath, 'utf-8')); + allTokens = flattenTokens(tokensJson.op || tokensJson); + console.log(` ✓ Found ${allTokens.length} tokens from tokens.json`); + } else { + // Fallback: parse CSS files + console.log('📄 Parsing CSS files for tokens...'); + const cssDistPath = path.join(opticsPath, 'dist/css'); + + const findCSSFiles = (dir: string): string[] => { + const files: string[] = []; + if (!fs.existsSync(dir)) return files; + + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...findCSSFiles(fullPath)); + } else if (entry.name.endsWith('.css')) { + files.push(fullPath); + } + } + return files; + }; + + const cssFiles = findCSSFiles(cssDistPath); + for (const cssFile of cssFiles) { + const content = fs.readFileSync(cssFile, 'utf-8'); + allTokens.push(...extractTokensFromCSS(content)); + } + + // Dedupe + const seenTokens = new Set(); + allTokens = allTokens.filter(t => { + if (seenTokens.has(t.name)) return false; + seenTokens.add(t.name); + return true; + }); + console.log(` ✓ Found ${allTokens.length} tokens from CSS`); + } + + // 2. Extract valid CSS classes for validation + console.log('\n🎨 Extracting CSS classes for validation...'); + let validClasses = new Set(); + const cssDistPath = path.join(opticsPath, 'dist/css'); + + const findCSSFiles = (dir: string): string[] => { + const files: string[] = []; + if (!fs.existsSync(dir)) return files; + + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...findCSSFiles(fullPath)); + } else if (entry.name.endsWith('.css')) { + files.push(fullPath); + } + } + return files; + }; + + const cssFiles = findCSSFiles(cssDistPath); + for (const cssFile of cssFiles) { + const content = fs.readFileSync(cssFile, 'utf-8'); + const classes = extractCSSClasses(content); + classes.forEach(c => validClasses.add(c)); + } + console.log(` ✓ Found ${validClasses.size} valid CSS classes`); + + // 3. Scrape components from Storybook source or derive from CSS + let patterns: CSSPattern[] = []; + + if (sourceRepo) { + console.log('\n📖 Scraping components from Storybook source...'); + patterns = scrapeComponentsFromStorybook(sourceRepo, opticsPath, validClasses); + console.log(` ✓ Found ${patterns.length} components`); + } else { + console.log('\n📄 Deriving components from CSS files in npm package...'); + console.log(' (For richer documentation, clone https://github.com/RoleModel/optics to ../../optics)'); + patterns = deriveComponentsFromCSS(opticsPath, validClasses); + console.log(` ✓ Derived ${patterns.length} components from CSS`); + } + + // 4. Get documentation + console.log('\n📚 Loading documentation...'); + const docs = getDocumentation(allTokens); + console.log(` ✓ ${docs.length} documentation sections`); + + // 5. Generate output + console.log('\n✍️ Generating optics-data.ts...'); + const output = generateOutputFile(allTokens, patterns, docs, version); + fs.writeFileSync(OUTPUT_FILE, output); + console.log(` ✓ Written to ${OUTPUT_FILE}`); + + // Summary + const byCategory: Record = {}; + allTokens.forEach(t => { + byCategory[t.category] = (byCategory[t.category] || 0) + 1; + }); + + console.log('\n✅ Sync complete!'); + console.log(` - Optics v${version}`); + console.log(` - ${allTokens.length} tokens across ${Object.keys(byCategory).length} categories`); + console.log(` - ${patterns.length} CSS patterns`); + console.log(` - ${docs.length} documentation sections`); + + console.log('\n Token categories:'); + Object.entries(byCategory).sort().forEach(([cat, count]) => { + console.log(` - ${cat}: ${count}`); + }); +} + +main().catch(err => { + console.error('Error:', err); + process.exit(1); +}); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 0000000..885e347 --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true + }, + "include": ["*.ts"] +} diff --git a/spec/javascript/example.spec.js b/spec/javascript/example.spec.js new file mode 100644 index 0000000..8d6192d --- /dev/null +++ b/spec/javascript/example.spec.js @@ -0,0 +1,12 @@ +import { describe, it, expect } from 'vitest' + +describe('Invisible Class', () => { + it('shows the element', async () => { + document.body.innerHTML = ` + + ` + + const button = document.querySelector('button') + await expect.element(button).toBeVisible() + }) +}) diff --git a/spec/javascript/test-setup.js b/spec/javascript/test-setup.js new file mode 100644 index 0000000..e69de29 diff --git a/spec/javascript/tools/validate-color-pair-tool.spec.js b/spec/javascript/tools/validate-color-pair-tool.spec.js new file mode 100644 index 0000000..3dcc6e0 --- /dev/null +++ b/spec/javascript/tools/validate-color-pair-tool.spec.js @@ -0,0 +1,78 @@ +import { describe, it, expect } from 'vitest' + +import ValidateColorPairTool from '../../../src/tools/validate-color-pair-tool.js' + +describe('ValidateColorPairTool', () => { + describe('getValidForegroundTokens', () => { + it('returns valid foreground tokens for a given background token', async () => { + const tool = new ValidateColorPairTool() + + const response = tool.getValidForegroundTokens('op-color-primary-plus-three') + + expect(response).toEqual([ + 'op-color-primary-on-plus-three', + 'op-color-primary-on-plus-three-alt', + ]) + }) + + it('returns valid foreground tokens for more complex colors of a given background token', async () => { + const tool = new ValidateColorPairTool() + + const response = tool.getValidForegroundTokens('op-color-alerts-neutral-plus-one') + + expect(response).toEqual([ + 'op-color-alerts-neutral-on-plus-one', + 'op-color-alerts-neutral-on-plus-one-alt', + ]) + }) + + it('returns an empty array when no valid foreground tokens exist for the given argument', () => { + const tool = new ValidateColorPairTool() + + const response = tool.getValidForegroundTokens('op-color-nonexistent') + + expect(response).toEqual([]) + }) + }) + + describe('getResponse', () => { + it('returns a valid response with valid foreground tokens for a given background token', async () => { + const tool = new ValidateColorPairTool() + + const response = tool.getResponse('op-color-primary-plus-three', 'op-color-primary-on-plus-three') + + expect(response).toEqual({ + valid: true, + validForegroundTokens: [ + 'op-color-primary-on-plus-three', + 'op-color-primary-on-plus-three-alt', + ] + }) + }) + + it('returns an invalid response with valid foreground tokens for a given background token', async () => { + const tool = new ValidateColorPairTool() + + const response = tool.getResponse('op-color-primary-plus-three', 'op-color-primary-on-plus-six') + + expect(response).toEqual({ + valid: false, + validForegroundTokens: [ + 'op-color-primary-on-plus-three', + 'op-color-primary-on-plus-three-alt', + ] + }) + }) + + it('returns an invalid response with an error when no tokens exist for the given background token', () => { + const tool = new ValidateColorPairTool() + + const response = tool.getResponse('op-color-nonexistent', 'op-color-primary-on-plus-three') + + expect(response).toEqual({ + valid: false, + errorMessage: 'The given background token is not valid.' + }) + }) + }) +}) diff --git a/src/_internal/resource-path.ts b/src/_internal/resource-path.ts index 1393076..12569e1 100644 --- a/src/_internal/resource-path.ts +++ b/src/_internal/resource-path.ts @@ -11,9 +11,16 @@ const readResourceFile = async (filename: string): Promise => { const readPromptFile = async (filename: string): Promise => { const currentDir = dirname(fileURLToPath(import.meta.url)) - const filePath = join(currentDir, '..', 'prompts', filename) + const filePath = join(currentDir, '..', 'prompts', '_templates', filename) return readFileSync(filePath, 'utf-8') } -export { readResourceFile, readPromptFile } +const readToolFile = async (filename: string): Promise => { + const currentDir = dirname(fileURLToPath(import.meta.url)) + const filePath = join(currentDir, '..', 'tools', '_templates', filename) + + return readFileSync(filePath, 'utf-8') +} + +export { readResourceFile, readPromptFile, readToolFile } diff --git a/src/index.ts b/src/index.ts index 62c88b9..6178fd0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,21 +11,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import type { ListPromptsRequestSchema, GetPromptRequestSchema } from "@modelcontextprotocol/sdk/types" import { z } from 'zod'; -import { - designTokens, - components, - documentation, - getTokenUsageStats, - getComponentTokenDependencies, -} from './optics-data.js'; -import { generateTheme } from './tools/theme-generator.js'; -import { validateTokenUsage, formatValidationReport } from './tools/validate.js'; -import { replaceHardCodedValues, formatReplacementSuggestions } from './tools/replace.js'; -import { checkTokenContrast, formatContrastResult } from './tools/accessibility.js'; -import { suggestTokenMigration, formatMigrationSuggestions } from './tools/migration.js'; -import { generateComponentScaffold, formatScaffoldOutput } from './tools/scaffold.js'; -import { generateStickerSheet, formatStickerSheet } from './tools/sticker-sheet.js'; - +import { designTokens } from './optics-data.js'; // Resources import * as systemOverview from './resources/system-overview.js'; import * as documentationSection from './resources/documentation/section.js'; @@ -33,13 +19,49 @@ import * as allTokens from './resources/tokens/all.js'; import * as categoryTokens from './resources/tokens/category.js'; import * as allComponents from './resources/components/all.js'; -// Prompts +// Prompts - Original import * as createThemedComponentPrompt from './prompts/create-themed-component.js'; import * as migrateToTokensPrompt from './prompts/migrate-to-tokens.js'; import * as accessibleColorComboPrompt from './prompts/accessible-color-combo.js'; import * as designReviewPrompt from './prompts/design-review.js'; import * as explainTokenSystemPrompt from './prompts/explain-token-system.js'; import * as getTokenReferencePrompt from './prompts/get-token-reference.js'; +import * as configureIconsPrompt from './prompts/configure-icons.js'; +import * as useRecipePrompt from './prompts/use-recipe.js'; + +// Prompts - New (Phase 1) +import * as buildComponentPrompt from './prompts/building/build-component.js'; +import * as createBrandThemePrompt from './prompts/theming/create-brand-theme.js'; +import * as reviewCodePrompt from './prompts/review/review-code.js'; + +// Tools - Original +import GetTokenTool from './tools/get-token-tool.js'; +import GetTokenUsageStatsTool from './tools/get-token-usage-stats-tool.js'; +import SearchTokensTool from './tools/search-tokens-tool.js'; +import ListComponentsTool from './tools/list-components-tool.js'; +import GetComponentInfoTool from './tools/get-component-info-tool.js'; +import GetComponentTokensTool from './tools/get-component-tokens-tool.js'; +import SearchDocumentationTool from './tools/search-documentation-tool.js'; +import GenerateThemeTool from './tools/generate-theme-tool.js'; +import ValidateTokenUsageTool from './tools/validate-token-usage-tool.js'; +import ReplaceHardCodedValuesTool from './tools/replace-hard-coded-values-tool.js'; +import CheckContrastTool from './tools/check-contrast-tool.js'; +import SuggestTokenMigrationTool from './tools/suggest-token-migration-tool.js'; +import GenerateComponentScaffoldTool from './tools/generate-component-scaffold-tool.js'; +import GenerateStickerSheetTool from './tools/generate-sticker-sheet-tool.js'; + +// Tools - New (Phase 1) +import ValidateColorPairingTool from './tools/validation/validate-color-pairing-tool.js'; +import DetectRedundantCssTool from './tools/validation/detect-redundant-css-tool.js'; +import GetComponentHtmlTool from './tools/data-retrieval/get-component-html-tool.js'; +import GetLayoutUtilityTool from './tools/data-retrieval/get-layout-utility-tool.js'; +import GetColorScaleTool from './tools/data-retrieval/get-color-scale-tool.js'; +import CalculateContrastTool from './tools/calculation/calculate-contrast-tool.js'; +import CalculateHslTokensTool from './tools/calculation/calculate-hsl-tokens-tool.js'; + +// Tools +import ReplaceHardCodedValuesTool from './tools/replace-hard-coded-values-tool.js'; + /** * Create and configure the MCP server @@ -47,6 +69,12 @@ import * as getTokenReferencePrompt from './prompts/get-token-reference.js'; const server = new McpServer({ name: 'optics-mcp', version: '0.1.0', +}, { + capabilities: { + tools: {}, + prompts: {}, + resources: {}, + } }); /** @@ -138,12 +166,19 @@ tokenCategories.forEach((category) => { */ const prompts = [ + // Original prompts createThemedComponentPrompt, migrateToTokensPrompt, accessibleColorComboPrompt, designReviewPrompt, explainTokenSystemPrompt, - getTokenReferencePrompt + getTokenReferencePrompt, + configureIconsPrompt, + useRecipePrompt, + // New prompts (Phase 1) + buildComponentPrompt, + createBrandThemePrompt, + reviewCodePrompt, ] prompts.forEach((prompt) => { @@ -173,245 +208,61 @@ prompts.forEach((prompt) => { ) }) - -/** - * Tool: Get Token - */ -server.registerTool( - 'get_token', - { - title: 'Get Token', - description: 'Get detailed information about a specific design token by name', - inputSchema: { - tokenName: z.string().describe('The name of the design token (e.g., "color-primary", "spacing-md")'), - }, - }, - async ({ tokenName }) => { - const token = designTokens.find((t) => t.name === tokenName); - - if (!token) { - return { - content: [ - { - type: 'text', - text: `Token not found: ${tokenName}\n\nAvailable tokens: ${designTokens - .map((t) => t.name) - .join(', ')}`, - }, - ], - }; - } - - return { - content: [ - { - type: 'text', - text: JSON.stringify(token, null, 2), - }, - ], - }; - } -); - -/** - * Tool: Search Tokens - */ -server.registerTool( - 'search_tokens', - { - title: 'Search Tokens', - description: 'Search for design tokens by category or name pattern', - inputSchema: { - category: z.string().optional().describe('Filter by category (color, spacing, typography, border, shadow)'), - namePattern: z.string().optional().describe('Search pattern for token names (case-insensitive)'), - }, - }, - async ({ category, namePattern }) => { - let filtered = designTokens; - - if (category) { - filtered = filtered.filter((t) => t.category === category); - } - - if (namePattern) { - const pattern = namePattern.toLowerCase(); - filtered = filtered.filter((t) => - t.name.toLowerCase().includes(pattern) - ); - } - - return { - content: [ - { - type: 'text', - text: JSON.stringify(filtered, null, 2), - }, - ], - }; - } -); - /** - * Tool: Get Token Usage Stats + * Tools */ -server.registerTool( - 'get_token_usage_stats', - { - title: 'Get Token Usage Stats', - description: 'Get statistics about design token usage across the system', - inputSchema: {}, - }, - async () => { - const stats = getTokenUsageStats(); - return { - content: [ - { - type: 'text', - text: JSON.stringify(stats, null, 2), - }, - ], - }; - } -); - -/** - * Tool: Get Component Info - */ -server.registerTool( - 'get_component_info', - { - title: 'Get Component Info', - description: 'Get detailed information about a component including its design token dependencies', - inputSchema: { - componentName: z.string().describe('The name of the component (e.g., "Button", "Card", "Input")'), - }, - }, - async ({ componentName }) => { - const component = components.find( - (c) => c.name.toLowerCase() === componentName.toLowerCase() - ); - - if (!component) { - return { - content: [ - { - type: 'text', - text: `Component not found: ${componentName}\n\nAvailable components: ${components - .map((c) => c.name) - .join(', ')}`, - }, - ], - }; - } - return { - content: [ - { - type: 'text', - text: JSON.stringify(component, null, 2), - }, - ], - }; - } -); - -/** - * Tool: List Components - */ -server.registerTool( - 'list_components', - { - title: 'List Components', - description: 'List all available components in the design system', - inputSchema: {}, - }, - async () => { - const componentList = components.map((c) => ({ - name: c.name, - description: c.description, - tokenCount: c.tokens.length, - })); - - return { - content: [ - { - type: 'text', - text: JSON.stringify(componentList, null, 2), - }, - ], - }; - } -); +const tools = [ + // Original tools + new GetTokenTool(), + new GetTokenUsageStatsTool(), + new SearchTokensTool(), + new ListComponentsTool(), + new GetComponentInfoTool(), + new GetComponentTokensTool(), + new SearchDocumentationTool(), + new GenerateThemeTool(), + new ValidateTokenUsageTool(), + new ReplaceHardCodedValuesTool(), + new CheckContrastTool(), + new SuggestTokenMigrationTool(), + new GenerateComponentScaffoldTool(), + new GenerateStickerSheetTool(), + // New tools (Phase 1) + new ValidateColorPairingTool(), + new DetectRedundantCssTool(), + new GetComponentHtmlTool(), + new GetLayoutUtilityTool(), + new GetColorScaleTool(), + new CalculateContrastTool(), + new CalculateHslTokensTool(), +] -/** - * Tool: Get Component Tokens - */ -server.registerTool( - 'get_component_tokens', - { - title: 'Get Component Tokens', - description: 'Get all design tokens used by a specific component', - inputSchema: { - componentName: z.string().describe('The name of the component'), +tools.forEach((tool) => { + server.registerTool( + tool.metadata.name, + { + title: tool.metadata.title, + description: tool.metadata.description, + inputSchema: tool.inputSchema, }, - }, - async ({ componentName }) => { - const deps = getComponentTokenDependencies(componentName); + async (args: any) => { + const content = await tool.handler(args) - if (!deps) { return { content: [ { type: 'text', - text: `Component not found: ${componentName}`, + text: content, }, ], - }; - } - - return { - content: [ - { - type: 'text', - text: JSON.stringify(deps, null, 2), - }, - ], - }; - } -); - -/** - * Tool: Search Documentation - */ -server.registerTool( - 'search_documentation', - { - title: 'Search Documentation', - description: 'Search through Optics documentation', - inputSchema: { - query: z.string().describe('Search query for documentation content'), + } }, - }, - async ({ query }) => { - const results = documentation.filter( - (doc) => - doc.title.toLowerCase().includes(query.toLowerCase()) || - doc.content.toLowerCase().includes(query.toLowerCase()) || - doc.section.toLowerCase().includes(query.toLowerCase()) - ); - - return { - content: [ - { - type: 'text', - text: JSON.stringify(results, null, 2), - }, - ], - }; - } -); + ) +}) /** +======= * Tool: Generate Theme */ server.registerTool( @@ -471,33 +322,7 @@ server.registerTool( } ); -/** - * Tool: Replace Hard-Coded Values - */ -server.registerTool( - 'replace_hard_coded_values', - { - title: 'Replace Hard-Coded Values', - description: 'Replace hard-coded values with design tokens', - inputSchema: { - code: z.string().describe('Code containing hard-coded values'), - autofix: z.boolean().optional().describe('Whether to automatically fix the code (default: false)'), - }, - }, - async ({ code, autofix }) => { - const result = replaceHardCodedValues(code, designTokens, autofix ?? false); - const formatted = formatReplacementSuggestions(result); - return { - content: [ - { - type: 'text', - text: formatted, - }, - ], - }; - } -); /** * Tool: Check Contrast diff --git a/src/optics-data.ts b/src/optics-data.ts index 619b353..7673be6 100644 --- a/src/optics-data.ts +++ b/src/optics-data.ts @@ -1,1617 +1,1595 @@ /** * Optics Design System Data - * This file contains the core design tokens, components, and documentation - * structure for the Optics design system. + * AUTO-GENERATED - Run: npm run sync-data + * Version: 2.3.0 | Generated: 2026-02-05T21:51:53.056Z */ +export type TokenCategory = 'animation' | 'border' | 'breakpoint' | 'color' | 'encoded-image' | 'input' | 'opacity' | 'shadow' | 'sizing' | 'spacing' | 'typography' | 'z-index'; + export interface DesignToken { name: string; + cssVar: string; value: string; - category: string; - description?: string; + category: TokenCategory; + description: string; } -export interface Component { +export interface CSSPattern { name: string; description: string; + className: string; + type: 'component' | 'layout' | 'utility'; + modifiers: string[]; + elements: string[]; + exampleHtml: string; + docsUrl: string; +} + +export interface Component extends CSSPattern { tokens: string[]; usage: string; - examples?: string[]; + examples: string[]; } export interface Documentation { section: string; title: string; content: string; - tokens?: string[]; + tokens: string[]; } -/** - * Design Tokens - Core visual design elements from Optics Design System - * Source: https://docs.optics.rolemodel.design - */ export const designTokens: DesignToken[] = [ - // Base Color HSL Values (These are the foundation - all other colors are derived from these) - { - name: 'op-color-primary-h', - value: '216', - category: 'color', - description: 'Primary color hue (HSL)' - }, - { - name: 'op-color-primary-s', - value: '58%', - category: 'color', - description: 'Primary color saturation (HSL)' - }, - { - name: 'op-color-primary-l', - value: '48%', - category: 'color', - description: 'Primary color lightness (HSL)' - }, - { - name: 'op-color-neutral-h', - value: '216', - category: 'color', - description: 'Neutral color hue (HSL, inherits from primary)' - }, - { - name: 'op-color-neutral-s', - value: '4%', - category: 'color', - description: 'Neutral color saturation (HSL)' - }, - { - name: 'op-color-neutral-l', - value: '48%', - category: 'color', - description: 'Neutral color lightness (HSL)' - }, - // Alert Colors HSL - { - name: 'op-color-alerts-warning-h', - value: '47', - category: 'color', - description: 'Warning alert hue (HSL)' - }, - { - name: 'op-color-alerts-warning-s', - value: '100%', - category: 'color', - description: 'Warning alert saturation (HSL)' - }, - { - name: 'op-color-alerts-warning-l', - value: '61%', - category: 'color', - description: 'Warning alert lightness (HSL)' - }, - { - name: 'op-color-alerts-danger-h', - value: '0', - category: 'color', - description: 'Danger alert hue (HSL)' - }, { - name: 'op-color-alerts-danger-s', - value: '99%', - category: 'color', - description: 'Danger alert saturation (HSL)' + "name": "color-white", + "cssVar": "--op-color-white", + "value": "hsl(0deg 100% 100%)", + "category": "color", + "description": "color token: color-white" }, { - name: 'op-color-alerts-danger-l', - value: '76%', - category: 'color', - description: 'Danger alert lightness (HSL)' + "name": "color-black", + "cssVar": "--op-color-black", + "value": "hsl(0deg 0% 0%)", + "category": "color", + "description": "color token: color-black" }, { - name: 'op-color-alerts-info-h', - value: '216', - category: 'color', - description: 'Info alert hue (HSL)' + "name": "color-primary-h", + "cssVar": "--op-color-primary-h", + "value": "216", + "category": "color", + "description": "Hue component (HSL) for color-primary" }, { - name: 'op-color-alerts-info-s', - value: '58%', - category: 'color', - description: 'Info alert saturation (HSL)' + "name": "color-primary-s", + "cssVar": "--op-color-primary-s", + "value": "58%", + "category": "color", + "description": "Saturation component (HSL) for color-primary" }, { - name: 'op-color-alerts-info-l', - value: '48%', - category: 'color', - description: 'Info alert lightness (HSL)' + "name": "color-primary-l", + "cssVar": "--op-color-primary-l", + "value": "48%", + "category": "color", + "description": "Lightness component (HSL) for color-primary" }, { - name: 'op-color-alerts-notice-h', - value: '130', - category: 'color', - description: 'Notice alert hue (HSL)' + "name": "color-primary-original", + "cssVar": "--op-color-primary-original", + "value": "hsl(var(--op-color-primary-h) var(--op-color-primary-s) var(--op-color-primary-l))", + "category": "color", + "description": "color token: color-primary-original" }, { - name: 'op-color-alerts-notice-s', - value: '61%', - category: 'color', - description: 'Notice alert saturation (HSL)' + "name": "color-neutral-h", + "cssVar": "--op-color-neutral-h", + "value": "var(--op-color-primary-h)", + "category": "color", + "description": "Hue component (HSL) for color-neutral" }, { - name: 'op-color-alerts-notice-l', - value: '64%', - category: 'color', - description: 'Notice alert lightness (HSL)' + "name": "color-neutral-s", + "cssVar": "--op-color-neutral-s", + "value": "4%", + "category": "color", + "description": "Saturation component (HSL) for color-neutral" }, - // Primary Color Scale - Main Scale - // Note: Same pattern applies to neutral, alerts-warning, alerts-danger, alerts-info, alerts-notice { - name: 'op-color-primary-plus-max', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 100%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 12%))', - category: 'color', - description: 'Primary color lightest - light mode: 100%, dark mode: 12%' + "name": "color-neutral-l", + "cssVar": "--op-color-neutral-l", + "value": "var(--op-color-primary-l)", + "category": "color", + "description": "Lightness component (HSL) for color-neutral" }, { - name: 'op-color-primary-plus-eight', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 98%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 14%))', - category: 'color', - description: 'Primary color scale +8' + "name": "color-neutral-original", + "cssVar": "--op-color-neutral-original", + "value": "hsl(var(--op-color-neutral-h) var(--op-color-neutral-s) var(--op-color-neutral-l))", + "category": "color", + "description": "color token: color-neutral-original" }, { - name: 'op-color-primary-plus-seven', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 96%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 16%))', - category: 'color', - description: 'Primary color scale +7' + "name": "color-alerts-warning-h", + "cssVar": "--op-color-alerts-warning-h", + "value": "47", + "category": "color", + "description": "Hue component (HSL) for color-alerts-warning" }, { - name: 'op-color-primary-plus-six', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 94%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 20%))', - category: 'color', - description: 'Primary color scale +6' + "name": "color-alerts-warning-s", + "cssVar": "--op-color-alerts-warning-s", + "value": "100%", + "category": "color", + "description": "Saturation component (HSL) for color-alerts-warning" }, { - name: 'op-color-primary-plus-five', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 90%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 24%))', - category: 'color', - description: 'Primary color scale +5' + "name": "color-alerts-warning-l", + "cssVar": "--op-color-alerts-warning-l", + "value": "61%", + "category": "color", + "description": "Lightness component (HSL) for color-alerts-warning" }, { - name: 'op-color-primary-plus-four', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 84%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 26%))', - category: 'color', - description: 'Primary color scale +4' + "name": "color-alerts-warning-original", + "cssVar": "--op-color-alerts-warning-original", + "value": "hsl(var(--op-color-alerts-warning-h) var(--op-color-alerts-warning-s) var(--op-color-alerts-warning-l))", + "category": "color", + "description": "color token: color-alerts-warning-original" }, { - name: 'op-color-primary-plus-three', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 70%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 29%))', - category: 'color', - description: 'Primary color scale +3' + "name": "color-alerts-danger-h", + "cssVar": "--op-color-alerts-danger-h", + "value": "0", + "category": "color", + "description": "Hue component (HSL) for color-alerts-danger" }, { - name: 'op-color-primary-plus-two', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 64%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 32%))', - category: 'color', - description: 'Primary color scale +2' + "name": "color-alerts-danger-s", + "cssVar": "--op-color-alerts-danger-s", + "value": "99%", + "category": "color", + "description": "Saturation component (HSL) for color-alerts-danger" }, { - name: 'op-color-primary-plus-one', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 45%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 35%))', - category: 'color', - description: 'Primary color scale +1' + "name": "color-alerts-danger-l", + "cssVar": "--op-color-alerts-danger-l", + "value": "76%", + "category": "color", + "description": "Lightness component (HSL) for color-alerts-danger" }, { - name: 'op-color-primary-base', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 40%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 38%))', - category: 'color', - description: 'Primary color base' + "name": "color-alerts-danger-original", + "cssVar": "--op-color-alerts-danger-original", + "value": "hsl(var(--op-color-alerts-danger-h) var(--op-color-alerts-danger-s) var(--op-color-alerts-danger-l))", + "category": "color", + "description": "color token: color-alerts-danger-original" }, { - name: 'op-color-primary-minus-one', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 36%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 40%))', - category: 'color', - description: 'Primary color scale -1' + "name": "color-alerts-info-h", + "cssVar": "--op-color-alerts-info-h", + "value": "216", + "category": "color", + "description": "Hue component (HSL) for color-alerts-info" }, { - name: 'op-color-primary-minus-two', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 32%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 45%))', - category: 'color', - description: 'Primary color scale -2' + "name": "color-alerts-info-s", + "cssVar": "--op-color-alerts-info-s", + "value": "58%", + "category": "color", + "description": "Saturation component (HSL) for color-alerts-info" }, { - name: 'op-color-primary-minus-three', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 28%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 48%))', - category: 'color', - description: 'Primary color scale -3' + "name": "color-alerts-info-l", + "cssVar": "--op-color-alerts-info-l", + "value": "48%", + "category": "color", + "description": "Lightness component (HSL) for color-alerts-info" }, { - name: 'op-color-primary-minus-four', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 24%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 52%))', - category: 'color', - description: 'Primary color scale -4' + "name": "color-alerts-info-original", + "cssVar": "--op-color-alerts-info-original", + "value": "hsl(var(--op-color-alerts-info-h) var(--op-color-alerts-info-s) var(--op-color-alerts-info-l))", + "category": "color", + "description": "color token: color-alerts-info-original" }, { - name: 'op-color-primary-minus-five', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 20%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 64%))', - category: 'color', - description: 'Primary color scale -5' + "name": "color-alerts-notice-h", + "cssVar": "--op-color-alerts-notice-h", + "value": "130", + "category": "color", + "description": "Hue component (HSL) for color-alerts-notice" }, { - name: 'op-color-primary-minus-six', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 16%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 72%))', - category: 'color', - description: 'Primary color scale -6' + "name": "color-alerts-notice-s", + "cssVar": "--op-color-alerts-notice-s", + "value": "61%", + "category": "color", + "description": "Saturation component (HSL) for color-alerts-notice" }, { - name: 'op-color-primary-minus-seven', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 8%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 80%))', - category: 'color', - description: 'Primary color scale -7' + "name": "color-alerts-notice-l", + "cssVar": "--op-color-alerts-notice-l", + "value": "64%", + "category": "color", + "description": "Lightness component (HSL) for color-alerts-notice" }, { - name: 'op-color-primary-minus-eight', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 4%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 88%))', - category: 'color', - description: 'Primary color scale -8' + "name": "color-alerts-notice-original", + "cssVar": "--op-color-alerts-notice-original", + "value": "hsl(var(--op-color-alerts-notice-h) var(--op-color-alerts-notice-s) var(--op-color-alerts-notice-l))", + "category": "color", + "description": "color token: color-alerts-notice-original" }, { - name: 'op-color-primary-minus-max', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 0%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 100%))', - category: 'color', - description: 'Primary color darkest - light mode: 0%, dark mode: 100%' - }, - - // Primary Color Scale - "On" Scale (for text/content colors that appear on the main scale colors) - // Note: Each has a base and "-alt" variant. Same pattern applies to neutral, alerts-warning, alerts-danger, alerts-info, alerts-notice - { - name: 'op-color-primary-on-plus-max', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 0%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 100%))', - category: 'color', - description: 'Text color for primary-plus-max backgrounds' + "name": "color-border", + "cssVar": "--op-color-border", + "value": "var(--op-color-neutral-plus-five)", + "category": "color", + "description": "color token: color-border" }, { - name: 'op-color-primary-on-plus-max-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 20%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 78%))', - category: 'color', - description: 'Alt text color for primary-plus-max backgrounds' + "name": "color-background", + "cssVar": "--op-color-background", + "value": "var(--op-color-neutral-plus-eight)", + "category": "color", + "description": "color token: color-background" }, { - name: 'op-color-primary-on-plus-eight', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 4%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 88%))', - category: 'color', - description: 'Text color for primary-plus-eight backgrounds' + "name": "color-on-background", + "cssVar": "--op-color-on-background", + "value": "var(--op-color-neutral-on-plus-eight)", + "category": "color", + "description": "Text color for use ON background background. MUST be paired with matching background color." }, { - name: 'op-color-primary-on-plus-eight-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 24%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 70%))', - category: 'color', - description: 'Alt text color for primary-plus-eight backgrounds' + "name": "opacity-none", + "cssVar": "--op-opacity-none", + "value": "0", + "category": "opacity", + "description": "opacity token: opacity-none" }, { - name: 'op-color-primary-on-plus-seven', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 8%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 80%))', - category: 'color', - description: 'Text color for primary-plus-seven backgrounds' + "name": "opacity-overlay", + "cssVar": "--op-opacity-overlay", + "value": "0.2", + "category": "opacity", + "description": "opacity token: opacity-overlay" }, { - name: 'op-color-primary-on-plus-seven-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 28%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 64%))', - category: 'color', - description: 'Alt text color for primary-plus-seven backgrounds' + "name": "opacity-disabled", + "cssVar": "--op-opacity-disabled", + "value": "0.4", + "category": "opacity", + "description": "opacity token: opacity-disabled" }, { - name: 'op-color-primary-on-plus-six', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 16%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 72%))', - category: 'color', - description: 'Text color for primary-plus-six backgrounds' + "name": "opacity-half", + "cssVar": "--op-opacity-half", + "value": "0.5", + "category": "opacity", + "description": "opacity token: opacity-half" }, { - name: 'op-color-primary-on-plus-six-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 26%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 96%))', - category: 'color', - description: 'Alt text color for primary-plus-six backgrounds' + "name": "opacity-full", + "cssVar": "--op-opacity-full", + "value": "1", + "category": "opacity", + "description": "opacity token: opacity-full" }, { - name: 'op-color-primary-on-plus-five', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 20%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 72%))', - category: 'color', - description: 'Text color for primary-plus-five backgrounds' + "name": "breakpoint-x-small", + "cssVar": "--op-breakpoint-x-small", + "value": "512px", + "category": "breakpoint", + "description": "breakpoint token: breakpoint-x-small" }, { - name: 'op-color-primary-on-plus-five-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 40%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 86%))', - category: 'color', - description: 'Alt text color for primary-plus-five backgrounds' + "name": "breakpoint-small", + "cssVar": "--op-breakpoint-small", + "value": "768px", + "category": "breakpoint", + "description": "breakpoint token: breakpoint-small" }, { - name: 'op-color-primary-on-plus-four', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 24%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 80%))', - category: 'color', - description: 'Text color for primary-plus-four backgrounds' + "name": "breakpoint-medium", + "cssVar": "--op-breakpoint-medium", + "value": "1024px", + "category": "breakpoint", + "description": "breakpoint token: breakpoint-medium" }, { - name: 'op-color-primary-on-plus-four-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 4%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 92%))', - category: 'color', - description: 'Alt text color for primary-plus-four backgrounds' + "name": "breakpoint-large", + "cssVar": "--op-breakpoint-large", + "value": "1280px", + "category": "breakpoint", + "description": "breakpoint token: breakpoint-large" }, { - name: 'op-color-primary-on-plus-three', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 20%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 78%))', - category: 'color', - description: 'Text color for primary-plus-three backgrounds' + "name": "breakpoint-x-large", + "cssVar": "--op-breakpoint-x-large", + "value": "1440px", + "category": "breakpoint", + "description": "breakpoint token: breakpoint-x-large" }, { - name: 'op-color-primary-on-plus-three-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 10%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 98%))', - category: 'color', - description: 'Alt text color for primary-plus-three backgrounds' + "name": "radius-small", + "cssVar": "--op-radius-small", + "value": "2px", + "category": "border", + "description": "border token: radius-small" }, { - name: 'op-color-primary-on-plus-two', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 16%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 80%))', - category: 'color', - description: 'Text color for primary-plus-two backgrounds' + "name": "radius-medium", + "cssVar": "--op-radius-medium", + "value": "4px", + "category": "border", + "description": "border token: radius-medium" }, { - name: 'op-color-primary-on-plus-two-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 6%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 92%))', - category: 'color', - description: 'Alt text color for primary-plus-two backgrounds' + "name": "radius-large", + "cssVar": "--op-radius-large", + "value": "8px", + "category": "border", + "description": "border token: radius-large" }, { - name: 'op-color-primary-on-plus-one', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 100%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 80%))', - category: 'color', - description: 'Text color for primary-plus-one backgrounds' + "name": "radius-x-large", + "cssVar": "--op-radius-x-large", + "value": "12px", + "category": "border", + "description": "border token: radius-x-large" }, { - name: 'op-color-primary-on-plus-one-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 95%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 98%))', - category: 'color', - description: 'Alt text color for primary-plus-one backgrounds' + "name": "radius-2x-large", + "cssVar": "--op-radius-2x-large", + "value": "16px", + "category": "border", + "description": "border token: radius-2x-large" }, { - name: 'op-color-primary-on-base', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 100%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 100%))', - category: 'color', - description: 'Text color for primary-base backgrounds' + "name": "radius-circle", + "cssVar": "--op-radius-circle", + "value": "50%", + "category": "border", + "description": "border token: radius-circle" }, { - name: 'op-color-primary-on-base-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 88%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 84%))', - category: 'color', - description: 'Alt text color for primary-base backgrounds' + "name": "radius-pill", + "cssVar": "--op-radius-pill", + "value": "9999px", + "category": "border", + "description": "border token: radius-pill" }, { - name: 'op-color-primary-on-minus-one', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 94%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 98%))', - category: 'color', - description: 'Text color for primary-minus-one backgrounds' + "name": "border-width", + "cssVar": "--op-border-width", + "value": "1px", + "category": "border", + "description": "border token: border-width" }, { - name: 'op-color-primary-on-minus-one-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 82%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 90%))', - category: 'color', - description: 'Alt text color for primary-minus-one backgrounds' + "name": "border-width-large", + "cssVar": "--op-border-width-large", + "value": "2px", + "category": "border", + "description": "border token: border-width-large" }, { - name: 'op-color-primary-on-minus-two', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 90%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 98%))', - category: 'color', - description: 'Text color for primary-minus-two backgrounds' + "name": "border-width-x-large", + "cssVar": "--op-border-width-x-large", + "value": "4px", + "category": "border", + "description": "border token: border-width-x-large" }, { - name: 'op-color-primary-on-minus-two-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 78%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 92%))', - category: 'color', - description: 'Alt text color for primary-minus-two backgrounds' + "name": "border-none", + "cssVar": "--op-border-none", + "value": "0 0 0 0", + "category": "border", + "description": "border token: border-none" }, { - name: 'op-color-primary-on-minus-three', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 86%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 98%))', - category: 'color', - description: 'Text color for primary-minus-three backgrounds' + "name": "border-all", + "cssVar": "--op-border-all", + "value": "0 0 0 var(--op-border-width)", + "category": "border", + "description": "border token: border-all" }, { - name: 'op-color-primary-on-minus-three-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 74%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 96%))', - category: 'color', - description: 'Alt text color for primary-minus-three backgrounds' + "name": "border-top", + "cssVar": "--op-border-top", + "value": "0 calc(-1 * var(--op-border-width)) 0 0", + "category": "border", + "description": "border token: border-top" }, { - name: 'op-color-primary-on-minus-four', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 84%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 2%))', - category: 'color', - description: 'Text color for primary-minus-four backgrounds' + "name": "border-right", + "cssVar": "--op-border-right", + "value": "var(--op-border-width) 0 0 0", + "category": "border", + "description": "border token: border-right" }, { - name: 'op-color-primary-on-minus-four-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 72%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 2%))', - category: 'color', - description: 'Alt text color for primary-minus-four backgrounds' + "name": "border-bottom", + "cssVar": "--op-border-bottom", + "value": "0 var(--op-border-width) 0 0", + "category": "border", + "description": "border token: border-bottom" }, { - name: 'op-color-primary-on-minus-five', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 88%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 2%))', - category: 'color', - description: 'Text color for primary-minus-five backgrounds' + "name": "border-left", + "cssVar": "--op-border-left", + "value": "calc(-1 * var(--op-border-width)) 0 0 0", + "category": "border", + "description": "border token: border-left" }, { - name: 'op-color-primary-on-minus-five-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 78%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 20%))', - category: 'color', - description: 'Alt text color for primary-minus-five backgrounds' + "name": "border-y", + "cssVar": "--op-border-y", + "value": "var(--op-border-top) var(--op-color-border), var(--op-border-bottom) var(--op-color-border)", + "category": "border", + "description": "border token: border-y" }, { - name: 'op-color-primary-on-minus-six', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 94%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 8%))', - category: 'color', - description: 'Text color for primary-minus-six backgrounds' + "name": "border-x", + "cssVar": "--op-border-x", + "value": "var(--op-border-left) var(--op-color-border), var(--op-border-right) var(--op-color-border)", + "category": "border", + "description": "border token: border-x" }, { - name: 'op-color-primary-on-minus-six-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 82%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 26%))', - category: 'color', - description: 'Alt text color for primary-minus-six backgrounds' + "name": "font-scale-unit", + "cssVar": "--op-font-scale-unit", + "value": "1rem", + "category": "typography", + "description": "typography token: font-scale-unit" }, { - name: 'op-color-primary-on-minus-seven', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 96%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 8%))', - category: 'color', - description: 'Text color for primary-minus-seven backgrounds' + "name": "font-2x-small", + "cssVar": "--op-font-2x-small", + "value": "calc(var(--op-font-scale-unit) * 1)", + "category": "typography", + "description": "typography token: font-2x-small" }, { - name: 'op-color-primary-on-minus-seven-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 84%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 34%))', - category: 'color', - description: 'Alt text color for primary-minus-seven backgrounds' + "name": "font-x-small", + "cssVar": "--op-font-x-small", + "value": "calc(var(--op-font-scale-unit) * 1.2)", + "category": "typography", + "description": "typography token: font-x-small" }, { - name: 'op-color-primary-on-minus-eight', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 98%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 4%))', - category: 'color', - description: 'Text color for primary-minus-eight backgrounds' + "name": "font-small", + "cssVar": "--op-font-small", + "value": "calc(var(--op-font-scale-unit) * 1.4)", + "category": "typography", + "description": "typography token: font-small" }, { - name: 'op-color-primary-on-minus-eight-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 86%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 38%))', - category: 'color', - description: 'Alt text color for primary-minus-eight backgrounds' + "name": "font-medium", + "cssVar": "--op-font-medium", + "value": "calc(var(--op-font-scale-unit) * 1.6)", + "category": "typography", + "description": "typography token: font-medium" }, { - name: 'op-color-primary-on-minus-max', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 100%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 0%))', - category: 'color', - description: 'Text color for primary-minus-max backgrounds' + "name": "font-large", + "cssVar": "--op-font-large", + "value": "calc(var(--op-font-scale-unit) * 1.8)", + "category": "typography", + "description": "typography token: font-large" }, { - name: 'op-color-primary-on-minus-max-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 88%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 38%))', - category: 'color', - description: 'Alt text color for primary-minus-max backgrounds' + "name": "font-x-large", + "cssVar": "--op-font-x-large", + "value": "calc(var(--op-font-scale-unit) * 2)", + "category": "typography", + "description": "typography token: font-x-large" }, - - // Core semantic colors (most commonly used) { - name: 'op-color-white', - value: 'hsl(0deg 100% 100%)', - category: 'color', - description: 'Pure white' + "name": "font-2x-large", + "cssVar": "--op-font-2x-large", + "value": "calc(var(--op-font-scale-unit) * 2.4)", + "category": "typography", + "description": "typography token: font-2x-large" }, { - name: 'op-color-black', - value: 'hsl(0deg 0% 0%)', - category: 'color', - description: 'Pure black' + "name": "font-3x-large", + "cssVar": "--op-font-3x-large", + "value": "calc(var(--op-font-scale-unit) * 2.8)", + "category": "typography", + "description": "typography token: font-3x-large" }, - - // Spacing Tokens { - name: 'op-space-scale-unit', - value: '1rem', - category: 'spacing', - description: 'Base unit for spacing scale (10px)' + "name": "font-4x-large", + "cssVar": "--op-font-4x-large", + "value": "calc(var(--op-font-scale-unit) * 3.2)", + "category": "typography", + "description": "typography token: font-4x-large" }, { - name: 'op-space-3x-small', - value: 'calc(var(--op-space-scale-unit) * 0.2)', - category: 'spacing', - description: '2px spacing' + "name": "font-5x-large", + "cssVar": "--op-font-5x-large", + "value": "calc(var(--op-font-scale-unit) * 3.6)", + "category": "typography", + "description": "typography token: font-5x-large" }, { - name: 'op-space-2x-small', - value: 'calc(var(--op-space-scale-unit) * 0.4)', - category: 'spacing', - description: '4px spacing' + "name": "font-6x-large", + "cssVar": "--op-font-6x-large", + "value": "calc(var(--op-font-scale-unit) * 4.8)", + "category": "typography", + "description": "typography token: font-6x-large" }, { - name: 'op-space-x-small', - value: 'calc(var(--op-space-scale-unit) * 0.8)', - category: 'spacing', - description: '8px spacing' + "name": "font-weight-thin", + "cssVar": "--op-font-weight-thin", + "value": "100", + "category": "typography", + "description": "typography token: font-weight-thin" }, { - name: 'op-space-small', - value: 'calc(var(--op-space-scale-unit) * 1.2)', - category: 'spacing', - description: '12px spacing' + "name": "font-weight-extra-light", + "cssVar": "--op-font-weight-extra-light", + "value": "200", + "category": "typography", + "description": "typography token: font-weight-extra-light" }, { - name: 'op-space-medium', - value: 'calc(var(--op-space-scale-unit) * 1.6)', - category: 'spacing', - description: '16px spacing' + "name": "font-weight-light", + "cssVar": "--op-font-weight-light", + "value": "300", + "category": "typography", + "description": "typography token: font-weight-light" }, { - name: 'op-space-large', - value: 'calc(var(--op-space-scale-unit) * 2)', - category: 'spacing', - description: '20px spacing' + "name": "font-weight-normal", + "cssVar": "--op-font-weight-normal", + "value": "400", + "category": "typography", + "description": "typography token: font-weight-normal" }, { - name: 'op-space-x-large', - value: 'calc(var(--op-space-scale-unit) * 2.4)', - category: 'spacing', - description: '24px spacing' + "name": "font-weight-medium", + "cssVar": "--op-font-weight-medium", + "value": "500", + "category": "typography", + "description": "typography token: font-weight-medium" }, { - name: 'op-space-2x-large', - value: 'calc(var(--op-space-scale-unit) * 2.8)', - category: 'spacing', - description: '28px spacing' + "name": "font-weight-semi-bold", + "cssVar": "--op-font-weight-semi-bold", + "value": "600", + "category": "typography", + "description": "typography token: font-weight-semi-bold" }, { - name: 'op-space-3x-large', - value: 'calc(var(--op-space-scale-unit) * 4)', - category: 'spacing', - description: '40px spacing' + "name": "font-weight-bold", + "cssVar": "--op-font-weight-bold", + "value": "700", + "category": "typography", + "description": "typography token: font-weight-bold" }, { - name: 'op-space-4x-large', - value: 'calc(var(--op-space-scale-unit) * 8)', - category: 'spacing', - description: '80px spacing' + "name": "font-weight-extra-bold", + "cssVar": "--op-font-weight-extra-bold", + "value": "800", + "category": "typography", + "description": "typography token: font-weight-extra-bold" }, - - // Typography Tokens - Font Family { - name: 'op-font-family', - value: "'Noto Sans', 'Noto Serif', sans-serif", - category: 'typography', - description: 'Font family for all text' + "name": "font-weight-black", + "cssVar": "--op-font-weight-black", + "value": "900", + "category": "typography", + "description": "typography token: font-weight-black" }, - - // Font Sizes { - name: 'op-font-scale-unit', - value: '1rem', - category: 'typography', - description: 'Base unit for font scale (10px)' + "name": "font-family", + "cssVar": "--op-font-family", + "value": "'Noto Sans', sans-serif", + "category": "typography", + "description": "typography token: font-family" }, { - name: 'op-font-2x-small', - value: 'calc(var(--op-font-scale-unit) * 1)', - category: 'typography', - description: '10px font size' + "name": "line-height-none", + "cssVar": "--op-line-height-none", + "value": "0", + "category": "sizing", + "description": "sizing token: line-height-none" }, { - name: 'op-font-x-small', - value: 'calc(var(--op-font-scale-unit) * 1.2)', - category: 'typography', - description: '12px font size' + "name": "line-height-densest", + "cssVar": "--op-line-height-densest", + "value": "1", + "category": "sizing", + "description": "sizing token: line-height-densest" }, { - name: 'op-font-small', - value: 'calc(var(--op-font-scale-unit) * 1.4)', - category: 'typography', - description: '14px font size' + "name": "line-height-denser", + "cssVar": "--op-line-height-denser", + "value": "1.15", + "category": "sizing", + "description": "sizing token: line-height-denser" }, { - name: 'op-font-medium', - value: 'calc(var(--op-font-scale-unit) * 1.6)', - category: 'typography', - description: '16px font size' + "name": "line-height-dense", + "cssVar": "--op-line-height-dense", + "value": "1.3", + "category": "sizing", + "description": "sizing token: line-height-dense" }, { - name: 'op-font-large', - value: 'calc(var(--op-font-scale-unit) * 1.8)', - category: 'typography', - description: '18px font size' + "name": "line-height-base", + "cssVar": "--op-line-height-base", + "value": "1.5", + "category": "sizing", + "description": "sizing token: line-height-base" }, { - name: 'op-font-x-large', - value: 'calc(var(--op-font-scale-unit) * 2)', - category: 'typography', - description: '20px font size' + "name": "line-height-loose", + "cssVar": "--op-line-height-loose", + "value": "1.6", + "category": "sizing", + "description": "sizing token: line-height-loose" }, { - name: 'op-font-2x-large', - value: 'calc(var(--op-font-scale-unit) * 2.4)', - category: 'typography', - description: '24px font size' + "name": "line-height-looser", + "cssVar": "--op-line-height-looser", + "value": "1.7", + "category": "sizing", + "description": "sizing token: line-height-looser" }, { - name: 'op-font-3x-large', - value: 'calc(var(--op-font-scale-unit) * 2.8)', - category: 'typography', - description: '28px font size' + "name": "line-height-loosest", + "cssVar": "--op-line-height-loosest", + "value": "1.8", + "category": "sizing", + "description": "sizing token: line-height-loosest" }, { - name: 'op-font-4x-large', - value: 'calc(var(--op-font-scale-unit) * 3.2)', - category: 'typography', - description: '32px font size' + "name": "letter-spacing-navigation", + "cssVar": "--op-letter-spacing-navigation", + "value": "0.01rem", + "category": "typography", + "description": "typography token: letter-spacing-navigation" }, { - name: 'op-font-5x-large', - value: 'calc(var(--op-font-scale-unit) * 3.6)', - category: 'typography', - description: '36px font size' + "name": "letter-spacing-label", + "cssVar": "--op-letter-spacing-label", + "value": "0.04rem", + "category": "typography", + "description": "typography token: letter-spacing-label" }, { - name: 'op-font-6x-large', - value: 'calc(var(--op-font-scale-unit) * 4.8)', - category: 'typography', - description: '48px font size' + "name": "transition-accordion", + "cssVar": "--op-transition-accordion", + "value": "rotate 120ms ease-in", + "category": "animation", + "description": "animation token: transition-accordion" }, - - // Font Weights { - name: 'op-font-weight-thin', - value: '100', - category: 'typography', - description: 'Thin font weight' + "name": "transition-input", + "cssVar": "--op-transition-input", + "value": "all 120ms ease-in", + "category": "animation", + "description": "animation token: transition-input" }, { - name: 'op-font-weight-extra-light', - value: '200', - category: 'typography', - description: 'Extra light font weight' + "name": "transition-sidebar", + "cssVar": "--op-transition-sidebar", + "value": "all 200ms ease-in-out", + "category": "animation", + "description": "animation token: transition-sidebar" }, { - name: 'op-font-weight-light', - value: '300', - category: 'typography', - description: 'Light font weight' + "name": "transition-modal", + "cssVar": "--op-transition-modal", + "value": "all var(--op-transition-modal-time) ease-in", + "category": "animation", + "description": "animation token: transition-modal" }, { - name: 'op-font-weight-normal', - value: '400', - category: 'typography', - description: 'Normal font weight' + "name": "transition-panel", + "cssVar": "--op-transition-panel", + "value": "right 400ms ease-in", + "category": "animation", + "description": "animation token: transition-panel" }, { - name: 'op-font-weight-medium', - value: '500', - category: 'typography', - description: 'Medium font weight' + "name": "transition-tooltip", + "cssVar": "--op-transition-tooltip", + "value": "all 300ms ease-in 300ms", + "category": "animation", + "description": "animation token: transition-tooltip" }, { - name: 'op-font-weight-semi-bold', - value: '600', - category: 'typography', - description: 'Semi-bold font weight' + "name": "animation-flash", + "cssVar": "--op-animation-flash", + "value": "rm-slide-in-out-flash 5s normal forwards", + "category": "animation", + "description": "animation token: animation-flash" }, { - name: 'op-font-weight-bold', - value: '700', - category: 'typography', - description: 'Bold font weight' + "name": "encoded-images-dropdown-arrow", + "cssVar": "--op-encoded-images-dropdown-arrow", + "value": "url('data", + "category": "encoded-image", + "description": "encoded-image token: encoded-images-dropdown-arrow" }, { - name: 'op-font-weight-extra-bold', - value: '800', - category: 'typography', - description: 'Extra bold font weight' + "name": "size-unit", + "cssVar": "--op-size-unit", + "value": "0.4rem", + "category": "sizing", + "description": "sizing token: size-unit" }, { - name: 'op-font-weight-black', - value: '900', - category: 'typography', - description: 'Black font weight' + "name": "space-scale-unit", + "cssVar": "--op-space-scale-unit", + "value": "1rem", + "category": "spacing", + "description": "spacing token: space-scale-unit" }, - - // Line Heights { - name: 'op-line-height-none', - value: '0', - category: 'typography', - description: 'No line height' + "name": "space-3x-small", + "cssVar": "--op-space-3x-small", + "value": "calc(var(--op-space-scale-unit) * 0.2)", + "category": "spacing", + "description": "spacing token: space-3x-small" }, { - name: 'op-line-height-densest', - value: '1', - category: 'typography', - description: 'Densest line height' + "name": "space-2x-small", + "cssVar": "--op-space-2x-small", + "value": "calc(var(--op-space-scale-unit) * 0.4)", + "category": "spacing", + "description": "spacing token: space-2x-small" }, { - name: 'op-line-height-denser', - value: '1.15', - category: 'typography', - description: 'Denser line height' + "name": "space-x-small", + "cssVar": "--op-space-x-small", + "value": "calc(var(--op-space-scale-unit) * 0.8)", + "category": "spacing", + "description": "spacing token: space-x-small" }, { - name: 'op-line-height-dense', - value: '1.3', - category: 'typography', - description: 'Dense line height' + "name": "space-small", + "cssVar": "--op-space-small", + "value": "calc(var(--op-space-scale-unit) * 1.2)", + "category": "spacing", + "description": "spacing token: space-small" }, { - name: 'op-line-height-base', - value: '1.5', - category: 'typography', - description: 'Base line height' + "name": "space-medium", + "cssVar": "--op-space-medium", + "value": "calc(var(--op-space-scale-unit) * 1.6)", + "category": "spacing", + "description": "spacing token: space-medium" }, { - name: 'op-line-height-loose', - value: '1.6', - category: 'typography', - description: 'Loose line height' + "name": "space-large", + "cssVar": "--op-space-large", + "value": "calc(var(--op-space-scale-unit) * 2)", + "category": "spacing", + "description": "spacing token: space-large" }, { - name: 'op-line-height-looser', - value: '1.7', - category: 'typography', - description: 'Looser line height' + "name": "space-x-large", + "cssVar": "--op-space-x-large", + "value": "calc(var(--op-space-scale-unit) * 2.4)", + "category": "spacing", + "description": "spacing token: space-x-large" }, { - name: 'op-line-height-loosest', - value: '1.8', - category: 'typography', - description: 'Loosest line height' + "name": "space-2x-large", + "cssVar": "--op-space-2x-large", + "value": "calc(var(--op-space-scale-unit) * 2.8)", + "category": "spacing", + "description": "spacing token: space-2x-large" }, - - // Letter Spacing { - name: 'op-letter-spacing-navigation', - value: '0.01rem', - category: 'typography', - description: 'Letter spacing for navigation' + "name": "space-3x-large", + "cssVar": "--op-space-3x-large", + "value": "calc(var(--op-space-scale-unit) * 4)", + "category": "spacing", + "description": "spacing token: space-3x-large" }, { - name: 'op-letter-spacing-label', - value: '0.04rem', - category: 'typography', - description: 'Letter spacing for labels' + "name": "space-4x-large", + "cssVar": "--op-space-4x-large", + "value": "calc(var(--op-space-scale-unit) * 8)", + "category": "spacing", + "description": "spacing token: space-4x-large" }, - - // Border Radius Tokens { - name: 'op-radius-small', - value: '2px', - category: 'border', - description: 'Small border radius' + "name": "shadow-x-small", + "cssVar": "--op-shadow-x-small", + "value": "0 1px 3px hsl(0deg 0% 0% / 15%), 0 1px 2px hsl(0deg 0% 0% / 30%)", + "category": "shadow", + "description": "shadow token: shadow-x-small" }, { - name: 'op-radius-medium', - value: '4px', - category: 'border', - description: 'Medium border radius' + "name": "shadow-small", + "cssVar": "--op-shadow-small", + "value": "0 2px 6px hsl(0deg 0% 0% / 15%), 0 1px 2px hsl(0deg 0% 0% / 30%)", + "category": "shadow", + "description": "shadow token: shadow-small" }, { - name: 'op-radius-large', - value: '8px', - category: 'border', - description: 'Large border radius' + "name": "shadow-medium", + "cssVar": "--op-shadow-medium", + "value": "0 4px 8px hsl(0deg 0% 0% / 15%), 0 1px 3px hsl(0deg 0% 0% / 30%)", + "category": "shadow", + "description": "shadow token: shadow-medium" }, { - name: 'op-radius-x-large', - value: '12px', - category: 'border', - description: 'Extra large border radius' + "name": "shadow-large", + "cssVar": "--op-shadow-large", + "value": "0 6px 10px hsl(0deg 0% 0% / 15%), 0 2px 3px hsl(0deg 0% 0% / 30%)", + "category": "shadow", + "description": "shadow token: shadow-large" }, { - name: 'op-radius-2x-large', - value: '16px', - category: 'border', - description: '2X large border radius' + "name": "shadow-x-large", + "cssVar": "--op-shadow-x-large", + "value": "0 8px 12px hsl(0deg 0% 0% / 15%), 0 4px 4px hsl(0deg 0% 0% / 30%)", + "category": "shadow", + "description": "shadow token: shadow-x-large" }, { - name: 'op-radius-circle', - value: '50%', - category: 'border', - description: 'Circular border radius' + "name": "z-index-header", + "cssVar": "--op-z-index-header", + "value": "500", + "category": "z-index", + "description": "z-index token: z-index-header" }, { - name: 'op-radius-pill', - value: '9999px', - category: 'border', - description: 'Pill-shaped border radius' + "name": "z-index-footer", + "cssVar": "--op-z-index-footer", + "value": "500", + "category": "z-index", + "description": "z-index token: z-index-footer" }, - - // Border Width Tokens { - name: 'op-border-width', - value: '1px', - category: 'border', - description: 'Standard border width' + "name": "z-index-sidebar", + "cssVar": "--op-z-index-sidebar", + "value": "700", + "category": "z-index", + "description": "z-index token: z-index-sidebar" }, { - name: 'op-border-width-large', - value: '2px', - category: 'border', - description: 'Large border width' + "name": "z-index-dialog", + "cssVar": "--op-z-index-dialog", + "value": "800", + "category": "z-index", + "description": "z-index token: z-index-dialog" }, { - name: 'op-border-width-x-large', - value: '4px', - category: 'border', - description: 'Extra large border width' + "name": "z-index-dialog-backdrop", + "cssVar": "--op-z-index-dialog-backdrop", + "value": "801", + "category": "z-index", + "description": "z-index token: z-index-dialog-backdrop" }, - - // Shadow Tokens { - name: 'op-shadow-x-small', - value: '0 1px 2px hsl(0deg 0% 0% / 3%), 0 1px 3px hsl(0deg 0% 0% / 15%)', - category: 'shadow', - description: 'Extra small shadow' + "name": "z-index-dialog-content", + "cssVar": "--op-z-index-dialog-content", + "value": "802", + "category": "z-index", + "description": "z-index token: z-index-dialog-content" }, { - name: 'op-shadow-small', - value: '0 1px 2px hsl(0deg 0% 0% / 3%), 0 2px 6px hsl(0deg 0% 0% / 15%)', - category: 'shadow', - description: 'Small shadow' + "name": "z-index-dropdown", + "cssVar": "--op-z-index-dropdown", + "value": "900", + "category": "z-index", + "description": "z-index token: z-index-dropdown" }, { - name: 'op-shadow-medium', - value: '0 4px 8px hsl(0deg 0% 0% / 15%), 0 1px 3px hsl(0deg 0% 0% / 3%)', - category: 'shadow', - description: 'Medium shadow' + "name": "z-index-alert-group", + "cssVar": "--op-z-index-alert-group", + "value": "950", + "category": "z-index", + "description": "z-index token: z-index-alert-group" }, { - name: 'op-shadow-large', - value: '0 6px 10px hsl(0deg 0% 0% / 15%), 0 2px 3px hsl(0deg 0% 0% / 3%)', - category: 'shadow', - description: 'Large shadow' + "name": "z-index-tooltip", + "cssVar": "--op-z-index-tooltip", + "value": "1000", + "category": "z-index", + "description": "z-index token: z-index-tooltip" }, { - name: 'op-shadow-x-large', - value: '0 8px 12px hsl(0deg 0% 0% / 15%), 0 4px 4px hsl(0deg 0% 0% / 3%)', - category: 'shadow', - description: 'Extra large shadow' + "name": "input-height-small", + "cssVar": "--op-input-height-small", + "value": "2.8rem", + "category": "input", + "description": "input token: input-height-small" }, - - // Opacity Tokens { - name: 'op-opacity-none', - value: '0', - category: 'color', - description: 'No opacity' + "name": "input-height-medium", + "cssVar": "--op-input-height-medium", + "value": "3.6rem", + "category": "input", + "description": "input token: input-height-medium" }, { - name: 'op-opacity-overlay', - value: '0.2', - category: 'color', - description: 'Overlay opacity' + "name": "input-height-large", + "cssVar": "--op-input-height-large", + "value": "4rem", + "category": "input", + "description": "input token: input-height-large" }, { - name: 'op-opacity-disabled', - value: '0.4', - category: 'color', - description: 'Disabled opacity' + "name": "input-height-x-large", + "cssVar": "--op-input-height-x-large", + "value": "8.4rem", + "category": "input", + "description": "input token: input-height-x-large" }, { - name: 'op-opacity-half', - value: '0.5', - category: 'color', - description: 'Half opacity' + "name": "input-inner-focus", + "cssVar": "--op-input-inner-focus", + "value": "inset 0 0 0 var(--op-border-width-large)", + "category": "input", + "description": "input token: input-inner-focus" }, { - name: 'op-opacity-full', - value: '1', - category: 'color', - description: 'Full opacity' + "name": "input-outer-focus", + "cssVar": "--op-input-outer-focus", + "value": "0 0 0 var(--op-border-width-x-large)", + "category": "input", + "description": "input token: input-outer-focus" } ]; -/** - * Components - Reusable UI components with their design token usage - * Real components from the Optics Design System - */ -export const components: Component[] = [ - { - name: 'Accordion', - description: 'Collapsible content panel with expand/collapse animation', - tokens: [ - '--op-color-neutral-on-plus-max', - '--op-font-weight-semi-bold', - '--op-font-x-large', - '--op-font-x-small', - '--op-mso-optical-sizing', - '--op-size-unit', - '--op-space-2x-small', - '--op-transition-accordion' +export const cssPatterns: CSSPattern[] = [ + { + "name": "Accordion", + "description": "Accordion classes are built on the `details` and `summary` html elements. They provide consistent and composable styling for disclosure widgets.", + "className": "accordion", + "type": "component", + "modifiers": [ + "accordion--disable-animation" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Alert', - description: 'Notification component for displaying important messages (warning, danger, info, notice)', - tokens: [ - '--op-animation-flash', - '--op-border-all', - '--op-color-alerts-danger-base', - '--op-color-alerts-danger-on-base', - '--op-color-alerts-danger-on-base-alt', - '--op-color-alerts-danger-on-plus-eight', - '--op-color-alerts-danger-on-plus-eight-alt', - '--op-color-alerts-danger-on-plus-five', - '--op-color-alerts-danger-on-plus-five-alt', - '--op-color-alerts-danger-plus-eight', - '--op-color-alerts-danger-plus-five', - '--op-color-alerts-info-base', - '--op-color-alerts-info-on-base', - '--op-color-alerts-info-on-base-alt', - '--op-color-alerts-info-on-plus-eight', - '--op-color-alerts-info-on-plus-eight-alt', - '--op-color-alerts-info-on-plus-five', - '--op-color-alerts-info-on-plus-five-alt', - '--op-color-alerts-info-plus-eight', - '--op-color-alerts-info-plus-five', - '--op-color-alerts-notice-base', - '--op-color-alerts-notice-on-base', - '--op-color-alerts-notice-on-base-alt', - '--op-color-alerts-notice-on-plus-eight', - '--op-color-alerts-notice-on-plus-eight-alt', - '--op-color-alerts-notice-on-plus-five', - '--op-color-alerts-notice-on-plus-five-alt', - '--op-color-alerts-notice-plus-eight', - '--op-color-alerts-notice-plus-five', - '--op-color-alerts-warning-base', - '--op-color-alerts-warning-on-base', - '--op-color-alerts-warning-on-base-alt', - '--op-color-alerts-warning-on-plus-eight', - '--op-color-alerts-warning-on-plus-eight-alt', - '--op-color-alerts-warning-on-plus-five', - '--op-color-alerts-warning-on-plus-five-alt', - '--op-color-alerts-warning-plus-eight', - '--op-color-alerts-warning-plus-five', - '--op-font-medium', - '--op-font-small', - '--op-font-weight-medium', - '--op-line-height-dense', - '--op-radius-medium', - '--op-space-2x-small', - '--op-space-large', - '--op-space-medium', - '--op-space-small', - '--op-space-x-small', - '--op-z-index-alert-group' + "elements": [ + "accordion__label", + "accordion__marker" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Avatar', - description: 'User profile picture component with multiple sizes and states', - tokens: [ - '--op-border-width', - '--op-border-width-large', - '--op-color-neutral-base', - '--op-color-neutral-minus-max', - '--op-color-primary-base', - '--op-color-primary-plus-one', - '--op-opacity-disabled', - '--op-opacity-overlay', - '--op-radius-circle', - '--op-size-unit' + "exampleHtml": "
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-accordion--docs" + }, + { + "name": "Alert", + "description": "Alert classes can be used to create a highlighted message or callout in your application.", + "className": "alert", + "type": "component", + "modifiers": [ + "alert--alert", + "alert--danger", + "alert--filled", + "alert--flash", + "alert--info", + "alert--muted", + "alert--notice", + "alert--warning" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Badge', - description: 'Small status indicator or label with multiple color variants', - tokens: [ - '--op-border-width-large', - '--op-color-alerts-danger-base', - '--op-color-alerts-danger-on-base', - '--op-color-alerts-info-base', - '--op-color-alerts-info-on-base', - '--op-color-alerts-notice-base', - '--op-color-alerts-notice-on-base', - '--op-color-alerts-warning-base', - '--op-color-alerts-warning-on-base', - '--op-color-neutral-base', - '--op-color-neutral-on-base', - '--op-color-neutral-plus-max', - '--op-color-primary-base', - '--op-color-primary-on-base', - '--op-font-small', - '--op-font-weight-bold', - '--op-font-x-small', - '--op-letter-spacing-label', - '--op-line-height-dense', - '--op-radius-medium', - '--op-radius-pill', - '--op-space-2x-small', - '--op-space-x-small' + "elements": [ + "alert__description", + "alert__icon", + "alert__messages", + "alert__title" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Breadcrumbs', - description: 'Navigation component showing the current page location in the site hierarchy', - tokens: [ - '--op-font-small', - '--op-font-weight-bold', - '--op-font-x-small', - '--op-space-x-small' + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-alert--docs" + }, + { + "name": "Avatar", + "description": "Avatar classes can be used on `a` or `div` html elements with an `img` within it. They provide consistent and composable styling for application avatars or profile pictures.", + "className": "avatar", + "type": "component", + "modifiers": [ + "avatar--disabled", + "avatar--large", + "avatar--medium", + "avatar--small" + ], + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-avatar--docs" + }, + { + "name": "Badge", + "description": "The Badge component is similar to the Tag component, however it has a different semantic purpose. Badge is intended to be used for notification and information where Tag is intended to be used for interaction and input. See [Tag](?path=/docs/components-tag--docs) for details on its usage.", + "className": "badge", + "type": "component", + "modifiers": [ + "badge--danger", + "badge--info", + "badge--notice", + "badge--notification-left", + "badge--notification-right", + "badge--pill", + "badge--primary", + "badge--warning", + "btn--with-badge" + ], + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-badge--docs" + }, + { + "name": "Breadcrumbs", + "description": "The breadcrumbs component is used to show the user's current location in a hierarchy of pages.", + "className": "breadcrumbs", + "type": "component", + "modifiers": [ + "breadcrumbs--large", + "breadcrumbs--small" + ], + "elements": [ + "breadcrumbs__link", + "breadcrumbs__separator", + "breadcrumbs__text" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Button', - description: 'Interactive button component with multiple variants (primary, secondary, etc.) and states', - tokens: [ - '--op-border-all', - '--op-color-alerts-danger-base', - '--op-color-alerts-danger-minus-two', - '--op-color-alerts-danger-on-base', - '--op-color-alerts-danger-on-minus-two', - '--op-color-alerts-danger-on-plus-five', - '--op-color-alerts-danger-plus-five', - '--op-color-alerts-danger-plus-three', - '--op-color-alerts-warning-base', - '--op-color-alerts-warning-minus-two', - '--op-color-alerts-warning-on-base', - '--op-color-alerts-warning-on-minus-two', - '--op-color-alerts-warning-on-plus-five', - '--op-color-alerts-warning-plus-five', - '--op-color-alerts-warning-plus-three', - '--op-color-neutral-on-plus-eight', - '--op-color-neutral-plus-eight', - '--op-color-neutral-plus-four', - '--op-color-primary-base', - '--op-color-primary-minus-five', - '--op-color-primary-on-base', - '--op-color-primary-on-minus-five', - '--op-color-primary-on-plus-eight', - '--op-color-primary-on-plus-five', - '--op-color-primary-on-plus-max', - '--op-color-primary-on-plus-one', - '--op-color-primary-plus-eight', - '--op-color-primary-plus-five', - '--op-color-primary-plus-one', - '--op-color-primary-plus-three', - '--op-color-primary-plus-two', - '--op-font-small', - '--op-font-weight-normal', - '--op-font-x-small', - '--op-input-focus-danger', - '--op-input-focus-primary', - '--op-input-focus-warning', - '--op-input-height-large', - '--op-input-height-medium', - '--op-input-height-small', - '--op-opacity-disabled', - '--op-radius-medium', - '--op-radius-pill', - '--op-space-3x-small', - '--op-space-small', - '--op-space-x-small', - '--op-transition-input' + "exampleHtml": "
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-breadcrumbs--docs" + }, + { + "name": "Button", + "description": "Button classes can be used on `button` or `a` html elements. They provide consistent and composable styling that should address most applications basic needs.", + "className": "btn", + "type": "component", + "modifiers": [ + "btn--active", + "btn--delete", + "btn--destructive", + "btn--disabled", + "btn--icon", + "btn--icon-with-label", + "btn--large", + "btn--medium", + "btn--no-border", + "btn--pill", + "btn--primary", + "btn--small", + "btn--warning", + "btn--with-badge" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] + "elements": [], + "exampleHtml": "", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-button--docs" + }, + { + "name": "ButtonGroup", + "description": "ButtonGroup component", + "className": "btn-group", + "type": "component", + "modifiers": [], + "elements": [ + "btn-group-toolbar" + ], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-buttongroup--docs" + }, + { + "name": "Card", + "description": "Card classes can be used to denote bordered sections of an application. They provide simple styles to create sections or \"cards\" for your interface. They can also be used as a starting point for \"row\" or list styles.", + "className": "card", + "type": "component", + "modifiers": [ + "card--condensed", + "card--padded", + "card--shadow-large", + "card--shadow-medium", + "card--shadow-small", + "card--shadow-x-large", + "card--shadow-x-small" + ], + "elements": [ + "card__body", + "card__footer", + "card__header" + ], + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-card--docs" }, { - name: 'ButtonGroup', - description: 'Container for grouping related buttons together', - tokens: [ - '--op-btn-group-active-z-index', - '--op-btn-group-focus-z-index', - '--op-btn-group-hover-z-index' + "name": "ConfirmDialog", + "description": "ConfirmDialog component", + "className": "confirm-dialog-wrapper", + "type": "component", + "modifiers": [ + "confirm-dialog-wrapper--active" + ], + "elements": [ + "confirm-dialog", + "confirm-dialog-wrapper__backdrop", + "confirm-dialog__body", + "confirm-dialog__footer", + "confirm-dialog__header" + ], + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-confirmdialog--docs" + }, + { + "name": "ContentHeader", + "description": "ContentHeader component", + "className": "content-header", + "type": "component", + "modifiers": [], + "elements": [ + "content-header__aside", + "content-header__context", + "content-header__details", + "content-header__subline", + "content-header__title", + "context-header__aside", + "context-header__context", + "context-header__details", + "context-header__subline" + ], + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-contentheader--docs" + }, + { + "name": "Divider", + "description": "Divider classes can be used to create horizontal or vertical visual divides between content.", + "className": "divider", + "type": "component", + "modifiers": [ + "divider--large", + "divider--medium", + "divider--small", + "divider--spacing-large", + "divider--spacing-medium", + "divider--spacing-small", + "divider--vertical" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Card', - description: 'Container component for grouping related content with optional header, body, and footer', - tokens: [ - '--op-border-all', - '--op-color-background', - '--op-color-border', - '--op-color-on-background', - '--op-font-medium', - '--op-line-height-base', - '--op-radius-medium', - '--op-shadow-large', - '--op-shadow-medium', - '--op-shadow-small', - '--op-shadow-x-large', - '--op-shadow-x-small', - '--op-space-medium', - '--op-space-scale-unit' + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-divider--docs" + }, + { + "name": "Form", + "description": "Form classes can be used on a variety of `inputs` or `select` HTML elements.", + "className": "form-label", + "type": "component", + "modifiers": [ + "form-control--large", + "form-control--medium", + "form-control--no-border", + "form-control--small", + "form-group--error", + "form-group--inline" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'ConfirmDialog', - description: 'Modal dialog for confirming user actions', - tokens: [ - '--op-border-all', - '--op-color-background', - '--op-color-black', - '--op-color-border', - '--op-color-on-background', - '--op-font-large', - '--op-font-medium', - '--op-font-weight-semi-bold', - '--op-line-height-base', - '--op-opacity-full', - '--op-opacity-half', - '--op-opacity-none', - '--op-radius-medium', - '--op-size-unit', - '--op-space-medium', - '--op-transition-modal', - '--op-z-index-dialog', - '--op-z-index-dialog-backdrop', - '--op-z-index-dialog-content' + "elements": [ + "form-control", + "form-error", + "form-error-summary", + "form-group", + "form-hint" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Border', - description: 'Visual separator between content sections', - tokens: [ - '--op-border-width', - '--op-border-width-large', - '--op-border-width-x-large', - '--op-color-border', - '--op-space-2x-small', - '--op-space-large', - '--op-space-medium', - '--op-space-x-small' + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-form--docs" + }, + { + "name": "Icon", + "description": "Icon classes are built on top of [Google's Material Symbols Icon Font](https://fonts.google.com/icons). They provide a way to integrate iconography into your application in a flexible and customizable way.", + "className": "ph", + "type": "component", + "modifiers": [ + "icon--filled", + "icon--high-emphasis", + "icon--large", + "icon--low-emphasis", + "icon--medium", + "icon--normal-emphasis", + "icon--outlined", + "icon--small", + "icon--weight-bold", + "icon--weight-light", + "icon--weight-normal", + "icon--weight-semi-bold", + "icon--weight-thin", + "icon--x-large" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Form', - description: 'Form input components including text inputs, textareas, selects, and labels', - tokens: [ - '--op-border-all', - '--op-border-bottom', - '--op-color-alerts-danger-base', - '--op-color-alerts-danger-minus-three', - '--op-color-alerts-danger-minus-two', - '--op-color-alerts-danger-on-plus-eight', - '--op-color-alerts-danger-on-plus-seven', - '--op-color-alerts-danger-plus-eight', - '--op-color-alerts-danger-plus-seven', - '--op-color-neutral-on-plus-eight', - '--op-color-neutral-plus-eight', - '--op-color-neutral-plus-four', - '--op-color-on-background', - '--op-color-primary-base', - '--op-color-primary-on-plus-eight', - '--op-color-primary-on-plus-max', - '--op-color-primary-on-plus-seven', - '--op-color-primary-plus-eight', - '--op-color-primary-plus-seven', - '--op-color-primary-plus-three', - '--op-color-primary-plus-two', - '--op-encoded-images-dropdown-arrow', - '--op-encoded-images-dropdown-arrow-width', - '--op-font-medium', - '--op-font-small', - '--op-font-weight-bold', - '--op-font-weight-normal', - '--op-font-x-small', - '--op-input-focus-danger', - '--op-input-focus-primary', - '--op-input-height-large', - '--op-input-height-medium', - '--op-input-height-small', - '--op-letter-spacing-label', - '--op-line-height-base', - '--op-opacity-disabled', - '--op-radius-large', - '--op-radius-medium', - '--op-space-2x-small', - '--op-space-3x-large', - '--op-space-large', - '--op-space-medium', - '--op-space-small', - '--op-space-x-large', - '--op-space-x-small', - '--op-transition-input' + "elements": [ + "fi", + "icon", + "li", + "ph-duotone", + "ti" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Icon', - description: 'Material Symbols icon component', - tokens: [ - '--op-font-2x-large', - '--op-font-3x-large', - '--op-font-large', - '--op-font-medium', - '--op-font-small', - '--op-font-weight-bold', - '--op-font-weight-light', - '--op-font-weight-normal', - '--op-font-weight-semi-bold', - '--op-line-height-densest', - '--op-mso-fill', - '--op-mso-grade', - '--op-mso-optical-sizing', - '--op-mso-weight' + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-icon--docs" + }, + { + "name": "Modal", + "description": "The Modal classes can be used for styling a custom modal. This can be used alongside the Rails configuration and Javascript implemented by [RoleModel Rails Modal](https://github.com/RoleModel/rolemodel_rails/tree/master/lib/generators/rolemodel/modals)", + "className": "modal-wrapper", + "type": "component", + "modifiers": [ + "modal-wrapper--active" + ], + "elements": [ + "modal", + "modal-wrapper__backdrop", + "modal__body", + "modal__footer", + "modal__header" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Modal', - description: 'Dialog component for focused interactions and content overlays', - tokens: [ - '--op-border-all', - '--op-color-background', - '--op-color-black', - '--op-color-border', - '--op-color-on-background', - '--op-font-large', - '--op-font-medium', - '--op-font-weight-semi-bold', - '--op-line-height-base', - '--op-opacity-full', - '--op-opacity-half', - '--op-opacity-none', - '--op-radius-medium', - '--op-size-unit', - '--op-space-medium', - '--op-space-small', - '--op-transition-modal', - '--op-z-index-dialog', - '--op-z-index-dialog-backdrop', - '--op-z-index-dialog-content' + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-modal--docs" + }, + { + "name": "Navbar", + "description": "Navbar classes provide simple styling for a navigation header.", + "className": "navbar", + "type": "component", + "modifiers": [ + "navbar--primary", + "navbar__content--justify-center", + "navbar__content--justify-end", + "navbar__content--justify-start" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Navbar', - description: 'Top navigation bar component', - tokens: [ - '--op-border-bottom', - '--op-color-neutral-on-plus-eight', - '--op-color-neutral-plus-eight', - '--op-color-neutral-plus-four', - '--op-color-primary-on-plus-six', - '--op-color-primary-plus-four', - '--op-color-primary-plus-six', - '--op-size-unit', - '--op-space-2x-small', - '--op-space-small', - '--op-space-x-large', - '--op-space-x-small' + "elements": [ + "navbar__brand", + "navbar__content", + "navbar__content--justify-center", + "navbar__content--justify-end", + "navbar__content--justify-start" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-navbar--docs" }, { - name: 'Pagination', - description: 'Navigation component for paginated content', - tokens: [ - '--op-space-x-small' + "name": "Pagination", + "description": "Pagination is used to navigate through a series of pages, typically when dealing with tabular data.", + "className": "pagination", + "type": "component", + "modifiers": [], + "elements": [ + "pagination__divider" + ], + "exampleHtml": "
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-pagination--docs" + }, + { + "name": "SegmentedControl", + "description": "Styles are built on css variables scoped to the segmented control.", + "className": "segmented-control", + "type": "component", + "modifiers": [ + "segmented-control--full-width", + "segmented-control--large", + "segmented-control--medium", + "segmented-control--small" + ], + "elements": [ + "segmented-control__input", + "segmented-control__label" + ], + "exampleHtml": "
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-segmentedcontrol--docs" + }, + { + "name": "SidePanel", + "description": "Side Panel classes provide simple styling for a panel of sections with a scrollable body.", + "className": "side-panel", + "type": "component", + "modifiers": [ + "side-panel--border-left", + "side-panel--border-right", + "side-panel__footer--padded", + "side-panel__footer--padded-x", + "side-panel__footer--padded-y", + "side-panel__section--padded", + "side-panel__section--padded-x", + "side-panel__section--padded-y" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'SidePanel', - description: 'Sliding panel from the side of the screen', - tokens: [ - '--op-border-left', - '--op-border-right', - '--op-border-x', - '--op-color-background', - '--op-color-border', - '--op-color-on-background', - '--op-size-unit', - '--op-space-medium' + "elements": [ + "side-panel__body", + "side-panel__body--padded", + "side-panel__body--padded-x", + "side-panel__body--padded-y", + "side-panel__footer", + "side-panel__footer--padded", + "side-panel__footer--padded-x", + "side-panel__footer--padded-y", + "side-panel__header", + "side-panel__header--padded", + "side-panel__header--padded-x", + "side-panel__header--padded-y", + "side-panel__section", + "side-panel__section--padded", + "side-panel__section--padded-x", + "side-panel__section--padded-y" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Sidebar', - description: 'Side navigation panel component', - tokens: [ - '--op-border-right', - '--op-color-neutral-on-plus-eight', - '--op-color-neutral-plus-eight', - '--op-color-neutral-plus-four', - '--op-color-primary-on-plus-six', - '--op-color-primary-plus-four', - '--op-color-primary-plus-six', - '--op-size-unit', - '--op-space-2x-large', - '--op-space-2x-small', - '--op-space-3x-small', - '--op-space-medium', - '--op-space-small', - '--op-space-x-small', - '--op-transition-sidebar' + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-sidepanel--docs" + }, + { + "name": "Sidebar", + "description": "Sidebar classes provide simple styling for a navigation sidebar drawer, compact, or rail.", + "className": "sidebar", + "type": "component", + "modifiers": [ + "sidebar--compact", + "sidebar--drawer", + "sidebar--padded", + "sidebar--primary", + "sidebar--rail", + "sidebar__content--center", + "sidebar__content--end", + "sidebar__content--start" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Spinner', - description: 'Loading indicator component', - tokens: [ - '--op-border-width', - '--op-border-width-large', - '--op-border-width-x-large', - '--op-color-neutral-plus-four', - '--op-color-primary-base', - '--op-size-unit' + "elements": [ + "icon-with-label", + "sidebar__brand", + "sidebar__content", + "sidebar__content--center", + "sidebar__content--end", + "sidebar__content--start" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Switch', - description: 'Toggle switch component', - tokens: [ - '--op-border-all', - '--op-border-width-large', - '--op-color-neutral-base', - '--op-color-neutral-plus-eight', - '--op-color-neutral-plus-five', - '--op-color-neutral-plus-three', - '--op-color-primary-base', - '--op-color-primary-minus-three', - '--op-color-primary-minus-two', - '--op-color-primary-plus-five', - '--op-color-primary-plus-six', - '--op-opacity-disabled', - '--op-radius-circle', - '--op-radius-pill', - '--op-size-unit', - '--op-space-2x-small', - '--op-space-x-small', - '--op-transition-input' + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-sidebar--docs" + }, + { + "name": "Spinner", + "description": "Spinners are CSS loading indicators that should be shown when retrieving data or performing slow computations.", + "className": "spinner", + "type": "component", + "modifiers": [ + "spinner--large", + "spinner--medium", + "spinner--small", + "spinner--x-small" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Tab', - description: 'Tabbed interface component', - tokens: [ - '--op-border-width-large', - '--op-border-width-x-large', - '--op-color-background', - '--op-color-on-background', - '--op-color-primary-base', - '--op-color-primary-on-plus-seven', - '--op-color-primary-plus-one', - '--op-color-primary-plus-seven', - '--op-font-small', - '--op-font-x-small', - '--op-input-focus-primary', - '--op-opacity-disabled', - '--op-space-2x-small', - '--op-space-3x-small', - '--op-space-medium', - '--op-space-small', - '--op-space-x-small' + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-spinner--docs" + }, + { + "name": "Switch", + "description": "Switch classes can be used to create a stylized checkbox or boolean input.", + "className": "switch", + "type": "component", + "modifiers": [ + "switch--large", + "switch--small" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Table', - description: 'Data table component for displaying structured information', - tokens: [ - '--op-border-all', - '--op-border-top', - '--op-color-alerts-danger-on-plus-seven', - '--op-color-alerts-danger-plus-seven', - '--op-color-border', - '--op-color-neutral-on-plus-eight', - '--op-color-neutral-on-plus-max', - '--op-color-neutral-on-plus-seven', - '--op-color-neutral-plus-eight', - '--op-color-neutral-plus-max', - '--op-color-neutral-plus-seven', - '--op-color-primary-on-plus-seven', - '--op-color-primary-plus-seven', - '--op-font-small', - '--op-font-weight-semi-bold', - '--op-radius-medium', - '--op-size-unit', - '--op-space-2x-small', - '--op-space-small' + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-switch--docs" + }, + { + "name": "Tab", + "description": "Tab classes provide simple styling for a tab group navigation.", + "className": "tab-group", + "type": "component", + "modifiers": [ + "tab--active", + "tab--disabled", + "tab--large", + "tab--small" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Tag', - description: 'Small label component for categorizing or tagging content', - tokens: [ - '--op-color-alerts-danger-base', - '--op-color-alerts-danger-minus-three', - '--op-color-alerts-danger-on-base', - '--op-color-alerts-danger-on-minus-three', - '--op-color-alerts-info-base', - '--op-color-alerts-info-minus-three', - '--op-color-alerts-info-on-base', - '--op-color-alerts-info-on-minus-three', - '--op-color-alerts-notice-base', - '--op-color-alerts-notice-minus-three', - '--op-color-alerts-notice-on-base', - '--op-color-alerts-notice-on-minus-three', - '--op-color-alerts-warning-base', - '--op-color-alerts-warning-minus-three', - '--op-color-alerts-warning-on-base', - '--op-color-alerts-warning-on-minus-three', - '--op-color-neutral-base', - '--op-color-neutral-minus-three', - '--op-color-neutral-on-base', - '--op-color-neutral-on-minus-three', - '--op-color-neutral-on-plus-four', - '--op-color-neutral-plus-four', - '--op-color-primary-base', - '--op-color-primary-minus-three', - '--op-color-primary-on-base', - '--op-color-primary-on-minus-three', - '--op-font-medium', - '--op-font-weight-bold', - '--op-font-x-small', - '--op-input-focus-danger', - '--op-input-focus-info', - '--op-input-focus-neutral', - '--op-input-focus-notice', - '--op-input-focus-primary', - '--op-input-focus-warning', - '--op-letter-spacing-label', - '--op-line-height-dense', - '--op-radius-pill', - '--op-space-2x-small' + "elements": [ + "tab" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'TextPair', - description: 'Component for displaying label-value pairs', - tokens: [ - '--op-font-large', - '--op-font-medium', - '--op-font-small', - '--op-font-weight-normal', - '--op-font-weight-semi-bold', - '--op-line-height-dense', - '--op-space-x-small' + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-tab--docs" + }, + { + "name": "Table", + "description": "Table classes provide simple styling for tables and their content.", + "className": "table", + "type": "component", + "modifiers": [ + "table--auto-layout", + "table--comfortable-density", + "table--compact-density", + "table--container", + "table--danger", + "table--default-density", + "table--even-striped", + "table--fixed-layout", + "table--odd-striped", + "table--primary", + "table--sticky-footer", + "table--sticky-header" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Tooltip', - description: 'Contextual information component that appears on hover or focus', - tokens: [ - '--op-color-neutral-minus-max', - '--op-color-neutral-on-minus-max', - '--op-font-family', - '--op-font-small', - '--op-opacity-full', - '--op-opacity-none', - '--op-radius-medium', - '--op-size-unit', - '--op-space-medium', - '--op-space-small', - '--op-space-x-small', - '--op-transition-tooltip', - '--op-z-index-tooltip' + "elements": [ + "table-container" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-table--docs" + }, + { + "name": "Tag", + "description": "The tag component can be applied to an element with a button within it. The Tag component is similar to the Badge component, however it has a different semantic purpose. Tag is intended to be used for interaction and input where Badge is intended to be used for Notification and Information. See [Badge](?path=/docs/components-badge--docs) for details on its usage.", + "className": "tag", + "type": "component", + "modifiers": [ + "tag--danger", + "tag--info", + "tag--notice", + "tag--primary", + "tag--read-only", + "tag--warning" + ], + "elements": [ + "tag__label" + ], + "exampleHtml": "
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-tag--docs" + }, + { + "name": "TextPair", + "description": "TextPair component", + "className": "text-pair", + "type": "component", + "modifiers": [ + "text-pair--inline", + "text-pair__subtitle--large", + "text-pair__subtitle--medium", + "text-pair__subtitle--small", + "text-pair__title--large", + "text-pair__title--medium", + "text-pair__title--small" + ], + "elements": [ + "text-pair__subtitle", + "text-pair__subtitle--large", + "text-pair__subtitle--medium", + "text-pair__subtitle--small", + "text-pair__title", + "text-pair__title--large", + "text-pair__title--medium", + "text-pair__title--small" + ], + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-textpair--docs" + }, + { + "name": "Stack", + "description": "Layout utility: op-stack", + "className": "op-stack", + "type": "layout", + "modifiers": [], + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/layout-stack--docs" + }, + { + "name": "Cluster", + "description": "Layout utility: op-cluster", + "className": "op-cluster", + "type": "layout", + "modifiers": [], + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/layout-cluster--docs" + }, + { + "name": "Split", + "description": "Layout utility: op-split", + "className": "op-split", + "type": "layout", + "modifiers": [], + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/layout-split--docs" + }, + { + "name": "Tooltip", + "description": "CSS-only tooltip using data attributes", + "className": "[data-tooltip-text]", + "type": "component", + "modifiers": [ + "[data-tooltip-position=\"top\"]", + "[data-tooltip-position=\"bottom\"]", + "[data-tooltip-position=\"left\"]", + "[data-tooltip-position=\"right\"]" + ], + "elements": [], + "exampleHtml": "", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-tooltip--docs" } ]; -/** - * Documentation - Organized documentation sections - */ +// Backwards compatibility: components alias with extended interface +export const components: Component[] = cssPatterns.map(p => ({ + ...p, + tokens: p.modifiers, + usage: p.description, + examples: p.exampleHtml ? [p.exampleHtml] : [], +})); + export const documentation: Documentation[] = [ { - section: 'introduction', - title: 'Introduction to Optics', - content: 'Optics is a comprehensive design system that provides a consistent visual language and component library for building user interfaces. It includes design tokens, components, patterns, and guidelines to ensure consistency across all RoleModel products.', - tokens: [] - }, - { - section: 'getting-started', - title: 'Getting Started', - content: 'To get started with Optics, install the design system package and import the tokens and components you need. The system is built with modularity in mind, allowing you to use only what you need.', - tokens: [] - }, - { - section: 'design-tokens', - title: 'Design Tokens', - content: 'Design tokens are the visual design atoms of the design system — specifically, they are named entities that store visual design attributes. They are used in place of hard-coded values to ensure consistency and enable theming.', - tokens: designTokens.map(t => t.name) - }, - { - section: 'color-system', - title: 'Color System', - content: 'The Optics color system provides a comprehensive palette designed for accessibility and visual harmony. Use semantic color tokens (primary, secondary, success, danger, warning, info) rather than specific color values.', - tokens: designTokens.filter(t => t.category === 'color').map(t => t.name) - }, - { - section: 'spacing', - title: 'Spacing System', - content: 'Consistent spacing creates visual rhythm and helps users understand relationships between elements. Optics uses a base-8 spacing system with tokens ranging from xs (4px) to 2xl (48px).', - tokens: designTokens.filter(t => t.category === 'spacing').map(t => t.name) - }, - { - section: 'typography', - title: 'Typography', - content: 'Typography is crucial for creating clear information hierarchy and readability. Optics provides font family, size, weight, and line height tokens to ensure consistent text styling.', - tokens: designTokens.filter(t => t.category === 'typography').map(t => t.name) - }, - { - section: 'components', - title: 'Components', - content: 'Optics components are reusable UI elements built with design tokens. Each component follows accessibility best practices and includes comprehensive documentation on usage and token application.', - tokens: [] - }, - { - section: 'accessibility', - title: 'Accessibility Guidelines', - content: 'Accessibility is a core principle of Optics. All components meet WCAG 2.1 AA standards. Ensure proper color contrast, keyboard navigation, and screen reader support when using Optics components.', - tokens: [] + "section": "overview", + "title": "Optics Overview", + "content": "Optics is a CSS-only design system. It provides CSS custom properties (tokens) and utility classes - NOT JavaScript components. Use the provided CSS classes and tokens; do not write custom CSS for patterns that already exist.", + "tokens": [] + }, + { + "section": "color-pairing", + "title": "Color Pairing Rule", + "content": "CRITICAL: Background and text colors must ALWAYS be paired. Never use --op-color-{family}-{scale} without also setting color to --op-color-{family}-on-{scale}. The \"on\" tokens are calculated for proper contrast against their matching background.", + "tokens": [ + "color-white", + "color-black", + "color-primary-h", + "color-primary-s", + "color-primary-l", + "color-primary-original", + "color-neutral-h", + "color-neutral-s", + "color-neutral-l", + "color-neutral-original", + "color-alerts-warning-h", + "color-alerts-warning-s", + "color-alerts-warning-l", + "color-alerts-warning-original", + "color-alerts-danger-h", + "color-alerts-danger-s", + "color-alerts-danger-l", + "color-alerts-danger-original", + "color-alerts-info-h", + "color-alerts-info-s", + "color-alerts-info-l", + "color-alerts-info-original", + "color-alerts-notice-h", + "color-alerts-notice-s", + "color-alerts-notice-l", + "color-alerts-notice-original", + "color-border", + "color-background", + "color-on-background" + ] + }, + { + "section": "color-system", + "title": "HSL Color System", + "content": "Optics uses HSL-based colors defined by -h (hue), -s (saturation), -l (lightness) tokens. A full scale is generated from plus-max (lightest) to minus-max (darkest). Each scale step has a matching \"on-\" token for text.", + "tokens": [ + "color-white", + "color-black", + "color-primary-h", + "color-primary-s", + "color-primary-l", + "color-primary-original", + "color-neutral-h", + "color-neutral-s", + "color-neutral-l", + "color-neutral-original", + "color-alerts-warning-h", + "color-alerts-warning-s", + "color-alerts-warning-l", + "color-alerts-warning-original", + "color-alerts-danger-h", + "color-alerts-danger-s", + "color-alerts-danger-l", + "color-alerts-danger-original", + "color-alerts-info-h", + "color-alerts-info-s", + "color-alerts-info-l", + "color-alerts-info-original", + "color-alerts-notice-h", + "color-alerts-notice-s", + "color-alerts-notice-l", + "color-alerts-notice-original", + "color-border", + "color-background", + "color-on-background" + ] + }, + { + "section": "use-existing", + "title": "Use Existing Classes", + "content": "Don't write custom CSS for components that already exist. Use .btn for buttons, .card for cards, .op-stack/.op-cluster/.op-split for layouts. Only write custom CSS when truly extending the system.", + "tokens": [] } ]; - -/** - * Get token usage statistics - */ -export function getTokenUsageStats() { - const categoryCount: Record = {}; - - designTokens.forEach(token => { - categoryCount[token.category] = (categoryCount[token.category] || 0) + 1; - }); - - return { - totalTokens: designTokens.length, - categories: categoryCount, - tokens: designTokens - }; -} - -/** - * Get component token dependencies - */ -export function getComponentTokenDependencies(componentName: string) { - const component = components.find(c => - c.name.toLowerCase() === componentName.toLowerCase() - ); - - if (!component) { - return null; - } - - const tokenDetails = component.tokens.map(tokenName => - designTokens.find(t => t.name === tokenName) - ).filter((token): token is DesignToken => token !== undefined); - - return { - component: component.name, - description: component.description, - tokenCount: component.tokens.length, - tokens: tokenDetails - }; -} diff --git a/src/prompts/accessible-color-combo-prompt.md b/src/prompts/_templates/accessible-color-combo-prompt.md similarity index 100% rename from src/prompts/accessible-color-combo-prompt.md rename to src/prompts/_templates/accessible-color-combo-prompt.md diff --git a/src/prompts/_templates/build-component-prompt.md b/src/prompts/_templates/build-component-prompt.md new file mode 100644 index 0000000..93497ee --- /dev/null +++ b/src/prompts/_templates/build-component-prompt.md @@ -0,0 +1,48 @@ +# Build {{TYPE}} Component + +## Task +Create a {{VARIANT}} {{TYPE}} component using Optics design system. + +## Optics Component Information + +### HTML Structure +```html +{{EXAMPLE_HTML}} +``` + +### CSS Class +- **Base class**: `{{CLASS_NAME}}` +- **Modifiers**: {{MODIFIERS}} + +### Documentation +{{DOCS_URL}} + +## Design Token Requirements +{{TOKENS}} + +## Usage Guidelines +{{USAGE}} + +## Critical Rules (MUST FOLLOW) + +### 1. Use Existing Optics Classes +- DO NOT write custom CSS for patterns that already exist in Optics +- Use the `{{CLASS_NAME}}` class as shown above +- Apply modifiers using BEM syntax: `{{CLASS_NAME}}--modifier` + +### 2. Color Pairing Rule +- ALWAYS pair background and text colors correctly +- If using `--op-color-{family}-{scale}` for background, MUST use `--op-color-{family}-on-{scale}` for text +- Use `validate_color_pairing` tool to verify any custom color combinations + +### 3. Validate Before Finalizing +- Run `detect_redundant_css` on any custom CSS to ensure you're not duplicating Optics patterns +- Check color contrast with `calculate_contrast` for accessibility + +## Tools to Use +1. `get_component_html` - Get exact HTML structure for this component +2. `validate_color_pairing` - Verify color combinations meet requirements +3. `detect_redundant_css` - Check for unnecessary custom CSS + +## Output Requirements +Provide the complete component implementation following the Optics patterns shown above. diff --git a/src/prompts/_templates/create-brand-theme-prompt.md b/src/prompts/_templates/create-brand-theme-prompt.md new file mode 100644 index 0000000..4960169 --- /dev/null +++ b/src/prompts/_templates/create-brand-theme-prompt.md @@ -0,0 +1,49 @@ +# Create Brand Theme: {{BRAND_NAME}} + +## Task +Generate a complete Optics theme customized for {{BRAND_NAME}} using the provided brand colors. + +## Brand Colors +- **Primary**: {{PRIMARY_COLOR}} +{{NEUTRAL_COLOR_SECTION}} + +## Theme Generation Process + +### Step 1: Convert Colors to HSL Tokens +Use `calculate_hsl_tokens` to convert the hex colors to Optics HSL format: +- Primary color → `--op-color-primary-h`, `--op-color-primary-s`, `--op-color-primary-l` +- Neutral color → `--op-color-neutral-h`, `--op-color-neutral-s`, `--op-color-neutral-l` + +### Step 2: View Generated Color Scale +Use `get_color_scale` with "primary" to see the full palette that will be generated from your primary color. + +### Step 3: Validate Accessibility +Use `calculate_contrast` to verify key color combinations meet WCAG requirements: +- Primary background with white text +- Primary text on white background +- Neutral variations for UI elements + +## Critical Rules + +### Color Pairing +All background colors MUST be paired with their matching "on" text colors: +- `--op-color-primary-plus-two` background → `--op-color-primary-on-plus-two` text +- `--op-color-neutral-plus-five` background → `--op-color-neutral-on-plus-five` text + +### HSL System +Optics uses HSL (Hue, Saturation, Lightness) for colors: +- Only the H, S, L base values need to be customized +- The scale system (plus-1, plus-2, minus-1, etc.) is calculated automatically +- This ensures consistent color relationships across the palette + +## Tools to Use +1. `calculate_hsl_tokens` - Convert hex to HSL and generate theme +2. `get_color_scale` - View the full color palette +3. `calculate_contrast` - Verify accessibility +4. `validate_color_pairing` - Check color pair validity + +## Expected Output +A complete CSS file with: +1. HSL base token overrides for brand colors +2. Any additional customizations needed +3. Documentation of the color system diff --git a/src/prompts/create-themed-component-prompt.md b/src/prompts/_templates/create-themed-component-prompt.md similarity index 100% rename from src/prompts/create-themed-component-prompt.md rename to src/prompts/_templates/create-themed-component-prompt.md diff --git a/src/prompts/design-review-prompt.md b/src/prompts/_templates/design-review-prompt.md similarity index 100% rename from src/prompts/design-review-prompt.md rename to src/prompts/_templates/design-review-prompt.md diff --git a/src/prompts/explain-token-system-prompt.md b/src/prompts/_templates/explain-token-system-prompt.md similarity index 100% rename from src/prompts/explain-token-system-prompt.md rename to src/prompts/_templates/explain-token-system-prompt.md diff --git a/src/prompts/get-token-reference-prompt.md b/src/prompts/_templates/get-token-reference-prompt.md similarity index 100% rename from src/prompts/get-token-reference-prompt.md rename to src/prompts/_templates/get-token-reference-prompt.md diff --git a/src/prompts/get_token_reference_prompt_partials/get-token-reference-border.md b/src/prompts/_templates/get_token_reference_prompt_partials/get-token-reference-border.md similarity index 100% rename from src/prompts/get_token_reference_prompt_partials/get-token-reference-border.md rename to src/prompts/_templates/get_token_reference_prompt_partials/get-token-reference-border.md diff --git a/src/prompts/get_token_reference_prompt_partials/get-token-reference-shadow.md b/src/prompts/_templates/get_token_reference_prompt_partials/get-token-reference-shadow.md similarity index 100% rename from src/prompts/get_token_reference_prompt_partials/get-token-reference-shadow.md rename to src/prompts/_templates/get_token_reference_prompt_partials/get-token-reference-shadow.md diff --git a/src/prompts/get_token_reference_prompt_partials/get-token-reference-spacing.md b/src/prompts/_templates/get_token_reference_prompt_partials/get-token-reference-spacing.md similarity index 100% rename from src/prompts/get_token_reference_prompt_partials/get-token-reference-spacing.md rename to src/prompts/_templates/get_token_reference_prompt_partials/get-token-reference-spacing.md diff --git a/src/prompts/get_token_reference_prompt_partials/get-token-reference-typography.md b/src/prompts/_templates/get_token_reference_prompt_partials/get-token-reference-typography.md similarity index 100% rename from src/prompts/get_token_reference_prompt_partials/get-token-reference-typography.md rename to src/prompts/_templates/get_token_reference_prompt_partials/get-token-reference-typography.md diff --git a/src/prompts/migrate-to-tokens-prompt.md b/src/prompts/_templates/migrate-to-tokens-prompt.md similarity index 100% rename from src/prompts/migrate-to-tokens-prompt.md rename to src/prompts/_templates/migrate-to-tokens-prompt.md diff --git a/src/prompts/_templates/review-code-prompt.md b/src/prompts/_templates/review-code-prompt.md new file mode 100644 index 0000000..30ac57c --- /dev/null +++ b/src/prompts/_templates/review-code-prompt.md @@ -0,0 +1,57 @@ +# Code Review: {{COMPONENT_TYPE}} + +## Code to Review +``` +{{CODE}} +``` + +## Review Checklist + +### 1. Color Pairing Validation +Use `validate_color_pairing` to check all background/text color combinations: +- Every background color must have a matching "on" text color +- Verify contrast ratios meet WCAG AA (4.5:1 for normal text) + +### 2. Redundant CSS Detection +Use `detect_redundant_css` to identify: +- Custom CSS that duplicates existing Optics components +- Patterns that could use Optics utility classes +- Unnecessary style overrides + +### 3. Component Usage Validation +Check that Optics components are used correctly: +- Correct class names (e.g., `btn` not `button`) +- Proper BEM modifier syntax (e.g., `btn--primary`) +- Required sub-elements present + +### 4. Token Usage +Verify design tokens are used instead of hard-coded values: +- Colors should use `var(--op-color-*)` not hex/rgb +- Spacing should use `var(--op-spacing-*)` not px values +- Typography should use `var(--op-font-*)` tokens + +## Tools to Use +1. `validate_color_pairing` - Check all color combinations +2. `detect_redundant_css` - Find unnecessary custom CSS +3. `validate_token_usage` - Find hard-coded values +4. `calculate_contrast` - Verify specific color pairs + +## Review Output Format + +### Issues Found +List each issue with: +- **Severity**: Error / Warning / Info +- **Location**: Line number or selector +- **Issue**: Description of the problem +- **Fix**: How to resolve it + +### Recommendations +- Optics components that could replace custom code +- Token suggestions for hard-coded values +- Accessibility improvements + +### Summary +- Total issues: X +- Errors: X +- Warnings: X +- Overall assessment: Pass / Needs Work / Fail diff --git a/src/prompts/building/build-component.ts b/src/prompts/building/build-component.ts new file mode 100644 index 0000000..668a886 --- /dev/null +++ b/src/prompts/building/build-component.ts @@ -0,0 +1,113 @@ +/** + * Build Component Prompt + * Orchestrates component creation using Optics patterns + * Combines: get_component_html, validate_color_pairing, detect_redundant_css + */ + +import { z } from 'zod'; +import { components } from '../../optics-data.js'; +import { readPromptFile } from '../../_internal/resource-path.js'; + +type BuildComponentPromptArgs = { + componentType: string; + variant?: string; +}; + +export const inputSchema = { + componentType: z.string().describe('Type of component to build (button, card, alert, modal, form, etc.)'), + variant: z.string().optional().describe('Component variant (primary, secondary, danger, filled, etc.)'), +}; + +export const metadata = { + name: 'build-component', + title: 'Build Component', + description: 'Build a component using Optics design system patterns. Enforces correct class usage and color pairing.', + role: 'user', +}; + +/** + * Find component by name (case-insensitive) + */ +const findComponent = (componentType: string) => { + return components.find( + (c) => c.name.toLowerCase() === componentType.toLowerCase() + ); +}; + +/** + * Find similar components for suggestions + */ +const findSimilarComponents = (componentType: string) => { + const searchTerm = componentType.toLowerCase(); + return components.filter( + (c) => + c.name.toLowerCase().includes(searchTerm) || + c.description.toLowerCase().includes(searchTerm) + ); +}; + +export async function handler(args: BuildComponentPromptArgs): Promise { + const componentType = args.componentType || 'button'; + const variant = args.variant || 'default'; + + const component = findComponent(componentType); + + if (!component) { + // Try to find similar components + const similar = findSimilarComponents(componentType); + const availableComponents = components.map((c) => c.name).join(', '); + + if (similar.length > 0) { + return `# Component Not Found + +The component "${componentType}" was not found in Optics. + +## Did you mean? +${similar.map((c) => `- **${c.name}**: ${c.description}`).join('\n')} + +## Available Components +${availableComponents} + +## Tools to Use +- Use \`list_components\` to see all available components +- Use \`get_component_html\` with a valid component name to get the HTML structure`; + } + + return `# Component Not Found + +The component "${componentType}" was not found in Optics. + +## Available Components +${availableComponents} + +## Tools to Use +- Use \`list_components\` to see all available components with descriptions +- Use \`search_components\` to find components by description`; + } + + // Load and populate the template + let promptTemplate = await readPromptFile('build-component-prompt.md'); + + // Format modifiers list + const modifiersText = component.modifiers.length > 0 + ? component.modifiers.map((m) => `\`${m}\``).join(', ') + : 'None'; + + // Format tokens list + const tokensText = component.tokens.length > 0 + ? component.tokens.map((t) => `- \`${t}\``).join('\n') + : 'No specific tokens required'; + + // Replace placeholders + promptTemplate = promptTemplate + .replace(/{{TYPE}}/g, component.name) + .replace(/{{VARIANT}}/g, variant) + .replace(/{{EXAMPLE_HTML}}/g, component.exampleHtml || '
...
') + .replace(/{{CLASS_NAME}}/g, component.className) + .replace(/{{MODIFIERS}}/g, modifiersText) + .replace(/{{DOCS_URL}}/g, component.docsUrl || 'No documentation URL available') + .replace(/{{TOKENS}}/g, tokensText) + .replace(/{{USAGE}}/g, component.usage || component.description); + + return promptTemplate; +} diff --git a/src/prompts/configure-icons.ts b/src/prompts/configure-icons.ts new file mode 100644 index 0000000..fda73dc --- /dev/null +++ b/src/prompts/configure-icons.ts @@ -0,0 +1,74 @@ +import { z } from 'zod' +import { readResourceFile } from "../_internal/resource-path.js" +import { iconLibraries, suggestLibrary, formatLibraryComparison, formatIconConfig } from '../tools/icons.js' + +type ConfigureIconsPromptArgs = { + library?: string + needsFill?: boolean + needsWeight?: boolean + needsEmphasis?: boolean + needsDuotone?: boolean + preferSmallBundle?: boolean +} + +export const inputSchema = { + library: z.string().optional().describe('Icon library name (material, phosphor, tabler, feather, lucide)'), + needsFill: z.boolean().optional().describe('Requires fill/outlined toggle'), + needsWeight: z.boolean().optional().describe('Requires weight variations'), + needsEmphasis: z.boolean().optional().describe('Requires emphasis variations'), + needsDuotone: z.boolean().optional().describe('Requires duotone icons'), + preferSmallBundle: z.boolean().optional().describe('Prefer smaller bundle size'), +} + +export const metadata = { + name: "configure-icons", + title: "Configure Icons", + description: "Select and configure an icon library for your Optics project", + role: "user", +} + +export async function handler(args: ConfigureIconsPromptArgs) { + // If specific library requested, return its config + if (args.library) { + const lib = iconLibraries.find(l => + l.name.toLowerCase().includes(args.library!.toLowerCase()) || + l.prefix.toLowerCase().includes(args.library!.toLowerCase()) + ) + + if (lib) { + return formatIconConfig({ + library: lib.name, + name: 'settings' // example icon + }) + } + } + + // If requirements specified, suggest libraries + if (args.needsFill || args.needsWeight || args.needsEmphasis || args.needsDuotone || args.preferSmallBundle) { + const suggestions = suggestLibrary({ + needsFill: args.needsFill, + needsWeight: args.needsWeight, + needsEmphasis: args.needsEmphasis, + needsDuotone: args.needsDuotone, + preferSmallBundle: args.preferSmallBundle, + }) + + if (suggestions.length === 0) { + return `No icon libraries match all your requirements.\n\n${formatLibraryComparison()}` + } + + let output = `## Recommended Libraries\n\n` + for (const lib of suggestions) { + output += `### ${lib.name}\n` + output += `- Import: \`${lib.import}\`\n` + output += `- Usage: \`${lib.usage}\`\n` + if (lib.notes) output += `- Notes: ${lib.notes}\n` + output += `\n` + } + return output + } + + // Default: return full comparison from prompt template + const promptTemplate = await readResourceFile("prompts/configure-icons-prompt.md") + return promptTemplate +} diff --git a/src/prompts/review/review-code.ts b/src/prompts/review/review-code.ts new file mode 100644 index 0000000..b2fb28c --- /dev/null +++ b/src/prompts/review/review-code.ts @@ -0,0 +1,61 @@ +/** + * Review Code Prompt + * Comprehensive code review for Optics compliance + * Combines: validate_color_pairing, detect_redundant_css, validate_token_usage, calculate_contrast + */ + +import { z } from 'zod'; +import { readPromptFile } from '../../_internal/resource-path.js'; + +type ReviewCodePromptArgs = { + code: string; + componentType?: string; +}; + +export const inputSchema = { + code: z.string().describe('CSS, HTML, or component code to review for Optics compliance'), + componentType: z.string().optional().describe('Type of component being reviewed (e.g., "button", "card", "form")'), +}; + +export const metadata = { + name: 'review-code', + title: 'Review Code', + description: 'Review code for Optics design system compliance. Checks color pairing, redundant CSS, token usage, and accessibility.', + role: 'user', +}; + +export async function handler(args: ReviewCodePromptArgs): Promise { + const code = args.code || ''; + const componentType = args.componentType || 'component'; + + if (!code.trim()) { + return `# Code Review Error + +No code provided for review. + +## How to Use +Provide the CSS, HTML, or component code you want reviewed: + +\`\`\` +review-code({ + code: "your code here", + componentType: "button" // optional +}) +\`\`\` + +## What Gets Reviewed +- Color pairing compliance +- Redundant CSS patterns +- Hard-coded values that should use tokens +- Accessibility (contrast ratios) +- Correct Optics class usage`; + } + + let promptTemplate = await readPromptFile('review-code-prompt.md'); + + promptTemplate = promptTemplate + .replace(/{{COMPONENT_TYPE}}/g, componentType) + .replace(/{{CODE}}/g, code); + + return promptTemplate; +} diff --git a/src/prompts/theming/create-brand-theme.ts b/src/prompts/theming/create-brand-theme.ts new file mode 100644 index 0000000..1a4cbd5 --- /dev/null +++ b/src/prompts/theming/create-brand-theme.ts @@ -0,0 +1,97 @@ +/** + * Create Brand Theme Prompt + * Orchestrates theme creation with brand colors + * Combines: calculate_hsl_tokens, get_color_scale, calculate_contrast + */ + +import { z } from 'zod'; +import { readPromptFile } from '../../_internal/resource-path.js'; + +type CreateBrandThemePromptArgs = { + brandName: string; + primaryColor: string; + neutralColor?: string; +}; + +export const inputSchema = { + brandName: z.string().describe('Name of the brand (e.g., "Acme Corp", "TechStartup")'), + primaryColor: z.string().describe('Primary brand color in hex format (e.g., "#FF5733", "#2D6FDB")'), + neutralColor: z.string().optional().describe('Optional neutral/gray color in hex format'), +}; + +export const metadata = { + name: 'create-brand-theme', + title: 'Create Brand Theme', + description: 'Generate a complete Optics theme customized with brand colors. Includes HSL conversion and accessibility validation.', + role: 'user', +}; + +/** + * Validate hex color format + */ +const isValidHexColor = (color: string): boolean => { + return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color); +}; + +/** + * Normalize hex color (add # if missing) + */ +const normalizeHexColor = (color: string): string => { + if (!color.startsWith('#')) { + return '#' + color; + } + return color; +}; + +export async function handler(args: CreateBrandThemePromptArgs): Promise { + const brandName = args.brandName || 'My Brand'; + let primaryColor = args.primaryColor || '#2D6FDB'; + let neutralColor = args.neutralColor; + + // Normalize colors + primaryColor = normalizeHexColor(primaryColor); + if (neutralColor) { + neutralColor = normalizeHexColor(neutralColor); + } + + // Validate colors + if (!isValidHexColor(primaryColor)) { + return `# Invalid Color Format + +The primary color "${primaryColor}" is not a valid hex color. + +## Valid Formats +- 6-digit hex: \`#FF5733\`, \`#2D6FDB\` +- 3-digit hex: \`#F53\`, \`#26D\` + +Please provide a valid hex color and try again.`; + } + + if (neutralColor && !isValidHexColor(neutralColor)) { + return `# Invalid Color Format + +The neutral color "${neutralColor}" is not a valid hex color. + +## Valid Formats +- 6-digit hex: \`#757882\`, \`#808080\` +- 3-digit hex: \`#888\`, \`#666\` + +Please provide a valid hex color and try again.`; + } + + // Load and populate the template + let promptTemplate = await readPromptFile('create-brand-theme-prompt.md'); + + // Build neutral color section + const neutralColorSection = neutralColor + ? `- **Neutral**: ${neutralColor}` + : '- **Neutral**: Using default Optics neutral (will inherit primary hue)'; + + // Replace placeholders + promptTemplate = promptTemplate + .replace(/{{BRAND_NAME}}/g, brandName) + .replace(/{{PRIMARY_COLOR}}/g, primaryColor) + .replace(/{{NEUTRAL_COLOR_SECTION}}/g, neutralColorSection); + + return promptTemplate; +} diff --git a/src/prompts/use-recipe.ts b/src/prompts/use-recipe.ts new file mode 100644 index 0000000..e6fe6e8 --- /dev/null +++ b/src/prompts/use-recipe.ts @@ -0,0 +1,47 @@ +import { z } from 'zod' +import { getRecipe, searchRecipes, formatRecipe, formatRecipeList, getRecipeCategories } from '../tools/recipes.js' +import { readResourceFile } from "../_internal/resource-path.js" + +type UseRecipePromptArgs = { + slug?: string + category?: string + query?: string +} + +export const inputSchema = { + slug: z.string().optional().describe('Specific recipe slug to retrieve'), + category: z.string().optional().describe('Filter by category (layout, sidebar, header, component)'), + query: z.string().optional().describe('Search term for recipe name or description'), +} + +export const metadata = { + name: "use-recipe", + title: "Use Recipe", + description: "Get Optics customization recipes with CSS and HTML patterns", + role: "user", +} + +export async function handler(args: UseRecipePromptArgs) { + // If specific recipe requested + if (args.slug) { + const recipe = getRecipe(args.slug) + + if (recipe) { + return formatRecipe(recipe) + } + + // Recipe not found - show available + const allRecipes = searchRecipes() + return `Recipe not found: ${args.slug}\n\n${formatRecipeList(allRecipes)}` + } + + // If searching/filtering + if (args.category || args.query) { + const results = searchRecipes(args.query, args.category) + return formatRecipeList(results) + } + + // Default: return overview from template + const promptTemplate = await readResourceFile("prompts/use-recipe-prompt.md") + return promptTemplate +} diff --git a/src/resources/00-system-overview.md b/src/resources/00-system-overview.md index c5f8fd0..80489c4 100644 --- a/src/resources/00-system-overview.md +++ b/src/resources/00-system-overview.md @@ -269,6 +269,132 @@ When you call `get_component_info` for "Button", you'll see tokens like: } ``` +## ⚠️ CRITICAL: Color Pairing Rule + +**This is the #1 rule AI gets wrong. Read carefully.** + +In Optics, background colors and text colors are ALWAYS paired. You cannot use one without the other. + +### The Rule + +| When you set... | You MUST also set... | +|-----------------|----------------------| +| `background-color: var(--op-color-primary-base)` | `color: var(--op-color-primary-on-base)` | +| `background-color: var(--op-color-danger-minus-1)` | `color: var(--op-color-danger-on-minus-1)` | +| `color: var(--op-color-neutral-on-plus-eight)` | `background-color: var(--op-color-neutral-plus-eight)` | + +### Why? + +The `-on-` tokens are calculated for proper contrast against their matching background. Using them separately: +- Breaks accessibility +- Creates unreadable text +- Defeats the purpose of the system + +### ❌ WRONG - Unpaired Colors + +```css +/* Missing the text color! */ +.card { + background-color: var(--op-color-primary-base); +} + +/* Missing the background! */ +.label { + color: var(--op-color-danger-on-base); +} + +/* Using mismatched tokens! */ +.badge { + background-color: var(--op-color-primary-base); + color: var(--op-color-danger-on-base); /* WRONG - mismatched family */ +} +``` + +### ✅ CORRECT - Paired Colors + +```css +.card { + background-color: var(--op-color-primary-base); + color: var(--op-color-primary-on-base); +} + +.label { + background-color: var(--op-color-danger-base); + color: var(--op-color-danger-on-base); +} + +.badge-light { + background-color: var(--op-color-primary-plus-five); + color: var(--op-color-primary-on-plus-five); +} +``` + +### The Pattern + +For ANY color usage: +1. Pick your background: `--op-color-{family}-{scale}` +2. Add matching text: `--op-color-{family}-on-{scale}` +3. For secondary text, use: `--op-color-{family}-on-{scale}-alt` + +**Never use background colors alone. Never use text colors alone. They are a pair.** + +--- + +## ⚠️ CRITICAL: Use Existing Components + +**Don't write CSS for things that already exist.** + +Optics has pre-built components with established class names. AI should use these, not create new ones. + +### ❌ WRONG - Writing New CSS + +```css +/* DON'T DO THIS - buttons already exist */ +.my-button { + padding: var(--op-space-small) var(--op-space-medium); + background-color: var(--op-color-primary-base); + color: var(--op-color-primary-on-base); + border-radius: var(--op-radius-medium); +} + +/* DON'T DO THIS - cards already exist */ +.custom-card { + padding: var(--op-space-large); + background: var(--op-color-neutral-plus-eight); + border-radius: var(--op-radius-large); + box-shadow: var(--op-shadow-medium); +} +``` + +### ✅ CORRECT - Use Existing Classes + +```html + + + + +
+
...
+
+``` + +### When to Write Custom CSS + +Only write custom CSS when: +1. **Extending** an existing component with a modifier (following BEM conventions) +2. Creating something that **truly doesn't exist** in Optics +3. Overriding specific tokens for **theming purposes** + +### Before Writing CSS, Ask: + +1. Does this component exist in Optics? → Check https://docs.optics.rolemodel.design +2. Can I use existing utility classes? → `.stack`, `.cluster`, `.split`, etc. +3. Am I just recreating something that exists? → Use the existing class + +**The whole point of a design system is to NOT write custom CSS for common patterns.** + +--- + ## 🚨 Common Mistakes ### Mistake 1: Looking for Simple Color Names diff --git a/src/resources/prompts/configure-icons-prompt.md b/src/resources/prompts/configure-icons-prompt.md new file mode 100644 index 0000000..11f5a17 --- /dev/null +++ b/src/resources/prompts/configure-icons-prompt.md @@ -0,0 +1,47 @@ +# Configure Icons + +Select and configure an icon library for your Optics project. + +## Available Libraries + +**Material Symbols** (default) +- Full variable font: fill, weight, emphasis, size +- Import: `@import '@rolemodel/optics/dist/css/core/fonts';` +- Usage: `settings` + +**Phosphor** +- Supports: fill, weight, size, duotone +- Import: `@import '@rolemodel/optics/dist/css/addons/fonts/phosphor_icons';` +- Usage: `` + +**Tabler** +- Supports: size, filled (via `.ti-{name}-filled`) +- Import: `@import '@rolemodel/optics/dist/css/addons/fonts/tabler_icons';` +- Usage: `` + +**Feather** +- Supports: size only +- Import: `@import '@rolemodel/optics/dist/css/addons/fonts/feather_icons';` +- Usage: `` + +**Lucide** +- Supports: size only (larger library than Feather) +- Import: `@import '@rolemodel/optics/dist/css/addons/fonts/lucide_icons';` +- Usage: `` + +## Modifiers + +- Size: `.icon--small`, `.icon--medium`, `.icon--large`, `.icon--x-large` +- Fill: `.icon--filled`, `.icon--outlined` +- Weight: `.icon--weight-light`, `.icon--weight-normal`, `.icon--weight-semi-bold`, `.icon--weight-bold` +- Emphasis: `.icon--low-emphasis`, `.icon--normal-emphasis`, `.icon--high-emphasis` + +## Selection Guide + +| Need | Recommended | +|------|-------------| +| Full customization | Material Symbols | +| Duotone icons | Phosphor | +| Clean minimal look | Tabler or Feather | +| Largest icon set | Lucide | +| Smallest bundle | Feather | diff --git a/src/resources/prompts/use-recipe-prompt.md b/src/resources/prompts/use-recipe-prompt.md new file mode 100644 index 0000000..de43bb7 --- /dev/null +++ b/src/resources/prompts/use-recipe-prompt.md @@ -0,0 +1,88 @@ +# Optics Recipes + +Optics provides real-world customization recipes showing how to extend and customize components for specific use cases. + +## Available Categories + +- **sidebar** - Custom sidebar configurations with brand colors, drawer modes, and navigation patterns +- **layout** - App shell layouts combining sidebar, header, and content areas +- **header** - Header patterns with aligned sections and navigation +- **component** - Custom component variations and patterns + +## Available Recipes + +### Sidebar + +- **sidebar-domains** - Domain registrar sidebar with wide drawer, no border radius buttons, and custom footer +- **sidebar-16six** - Dark purple sidebar with custom brand colors for performance management software + +### Layout + +- **layout-app-shell** - Standard app layout with sidebar, header, and main content area using CSS Grid + +### Header + +- **aligned-header** - Header with aligned left/center/right sections using CSS Grid + +## How to Use + +Each recipe includes: + +1. **Tokens Used** - Design tokens required for the customization +2. **CSS** - Scoped CSS that extends Optics base styles +3. **HTML** - Example markup structure + +### Example Usage + +To get a specific recipe with full CSS and HTML: + +``` +Use get_recipe with slug "sidebar-domains" +``` + +To search by category: + +``` +Use search_recipes with category "sidebar" +``` + +## Key Patterns + +### Scoping Customizations + +Recipes use modifier classes to scope customizations: + +```css +.sidebar { + &.sidebar--domains { + /* Domain-specific customizations */ + } +} +``` + +### Token Overrides + +Recipes show how to override private tokens (prefixed with `--_op-`): + +```css +.sidebar--custom { + --_op-sidebar-drawer-width: 28rem; + --_op-sidebar-background-color: hsl(256deg 66% 15%); +} +``` + +### Composing with Optics Classes + +Recipes combine with existing Optics utilities: + +```html + +``` + +## Docs Reference + +Full recipe documentation: https://docs.optics.rolemodel.design/?path=/docs/recipes-custom-sidebar--docs diff --git a/src/test.ts b/src/test.ts index a84c583..f4b6237 100644 --- a/src/test.ts +++ b/src/test.ts @@ -4,7 +4,7 @@ * Simple test script to verify the Optics MCP server functionality */ -import { designTokens, components, getTokenUsageStats, getComponentTokenDependencies } from './optics-data.js'; +import { designTokens, components } from './optics-data.js'; console.log('🧪 Testing Optics MCP Server...\n'); @@ -21,18 +21,18 @@ console.log(` Total components: ${components.length}`); console.log(` Components: ${components.map(c => c.name).join(', ')}\n`); // Test 3: Token Usage Stats -console.log('✓ Test 3: Token Usage Statistics'); -const stats = getTokenUsageStats(); -console.log(` Total tokens: ${stats.totalTokens}`); -console.log(` Categories:`, stats.categories, '\n'); +// console.log('✓ Test 3: Token Usage Statistics'); +// const stats = getTokenUsageStats(); +// console.log(` Total tokens: ${stats.totalTokens}`); +// console.log(` Categories:`, stats.categories, '\n'); // Test 4: Component Token Dependencies -console.log('✓ Test 4: Component Token Dependencies'); -const buttonDeps = getComponentTokenDependencies('Button'); -if (buttonDeps) { - console.log(` ${buttonDeps.component}: ${buttonDeps.tokenCount} tokens`); - console.log(` First token: ${buttonDeps.tokens[0]?.name}\n`); -} +// console.log('✓ Test 4: Component Token Dependencies'); +// const buttonDeps = getComponentTokenDependencies('Button'); +// if (buttonDeps) { +// console.log(` ${buttonDeps.component}: ${buttonDeps.tokenCount} tokens`); +// console.log(` First token: ${buttonDeps.tokens[0]?.name}\n`); +// } // Test 5: Search functionality console.log('✓ Test 5: Token Search'); diff --git a/src/tools/_templates/check-contrast-error.md b/src/tools/_templates/check-contrast-error.md new file mode 100644 index 0000000..fbaecf2 --- /dev/null +++ b/src/tools/_templates/check-contrast-error.md @@ -0,0 +1,7 @@ +# Contrast Check Result + +**Foreground**: {{foregroundToken}} (`{{foregroundValue}}`) +**Background**: {{backgroundToken}} (`{{backgroundValue}}`) + +✗ Unable to calculate contrast +**Reason**: {{reason}} diff --git a/src/tools/_templates/check-contrast-result.md b/src/tools/_templates/check-contrast-result.md new file mode 100644 index 0000000..06f3fab --- /dev/null +++ b/src/tools/_templates/check-contrast-result.md @@ -0,0 +1,9 @@ +# Contrast Check Result + +**Foreground**: {{foregroundToken}} (`{{foregroundValue}}`) +**Background**: {{backgroundToken}} (`{{backgroundValue}}`) + +**Contrast Ratio**: {{contrastRatio}}:1 +**WCAG AA**: {{wcagAA}} +**WCAG AAA**: {{wcagAAA}} +**Score**: {{score}} diff --git a/src/tools/_templates/detect-redundant-css-result.md b/src/tools/_templates/detect-redundant-css-result.md new file mode 100644 index 0000000..e0c2c76 --- /dev/null +++ b/src/tools/_templates/detect-redundant-css-result.md @@ -0,0 +1,14 @@ +# Redundant CSS Detection + +## Summary +- **Total patterns analyzed**: {{totalPatterns}} +- **Redundant patterns found**: {{redundantCount}} +- **Optics replacements available**: {{replacementCount}} + +## Findings + +{{findings}} + +## Recommendations + +{{recommendations}} diff --git a/src/tools/_templates/generate-component-scaffold-usage.md b/src/tools/_templates/generate-component-scaffold-usage.md new file mode 100644 index 0000000..8c8abec --- /dev/null +++ b/src/tools/_templates/generate-component-scaffold-usage.md @@ -0,0 +1,23 @@ +# {{componentName}} Usage + +## Import + +```typescript +import { {{componentName}} } from './components/{{componentName}}'; +``` + +## Basic Usage + +```tsx +<{{componentName}}> + Your content here + +``` + +## With Custom ClassName + +```tsx +<{{componentName}} className="custom-class"> + Your content here + +``` diff --git a/src/tools/_templates/generate-sticker-sheet-instructions.md b/src/tools/_templates/generate-sticker-sheet-instructions.md new file mode 100644 index 0000000..f3dfd40 --- /dev/null +++ b/src/tools/_templates/generate-sticker-sheet-instructions.md @@ -0,0 +1,22 @@ +# Optics Sticker Sheet - {{framework}} + +This sticker sheet provides a visual reference for all Optics design tokens and components. + +## Usage + +1. Copy the component code below into your {{framework}} project +2. Copy the CSS styles into your stylesheet +3. Import and use the components in your app +4. Replace placeholders with actual component implementations + +## Files Generated + +- **Component Code**: Ready-to-use {{framework}} components +- **Styles**: CSS using Optics design tokens +- **Examples**: Visual specimens of colors, typography, and components + +## Next Steps + +- Customize the examples to match your specific components +- Add interactive states (hover, focus, disabled) +- Include additional token categories as needed diff --git a/src/tools/_templates/generate-theme-instructions.md b/src/tools/_templates/generate-theme-instructions.md new file mode 100644 index 0000000..ff891d2 --- /dev/null +++ b/src/tools/_templates/generate-theme-instructions.md @@ -0,0 +1,63 @@ +# {{themeName}} Theme + +## Overview +This theme was generated by Optics MCP and uses the Optics Design System tokens. + +## Step 1: Install Optics Design System + +### Option A: Via npm (Recommended) +```bash +npm install @rolemodel/optics +``` + +Then import in your CSS: +```css +@import "@rolemodel/optics/dist/optics.css"; +``` + +### Option B: Via CDN (jsDelivr) +```html + +``` + +### Option C: Via unpkg +```html + +``` + +## Step 2: Add Your Custom Theme Overrides + +Create a `theme.css` file with the custom HSL color values below and load it AFTER Optics: + +```html + + +``` + +## Token Summary + +{{tokenSummary}} + +## Step 3: Using Optics Components + +**IMPORTANT:** Optics has pre-built components with specific HTML structure and CSS classes. + +Do NOT make up CSS classes. Use the actual Optics components and their documentation: + +**Component Documentation:** https://docs.optics.rolemodel.design + +Your custom theme will automatically apply to all Optics components through the HSL base values. + +### Example: Using an Optics Button +Refer to the Optics documentation for the actual button HTML structure and CSS classes. +Your theme colors will apply automatically through the `--op-color-primary-*` variables. + +### Figma Variables +Import the `figma-variables.json` file into Figma: +1. Open your Figma file +2. Go to Variables panel +3. Import → Select `figma-variables.json` + +## Token Categories + +{{tokenCategories}} diff --git a/src/tools/_templates/generate-theme-output.md b/src/tools/_templates/generate-theme-output.md new file mode 100644 index 0000000..a34d95a --- /dev/null +++ b/src/tools/_templates/generate-theme-output.md @@ -0,0 +1,24 @@ +# {{brandName}} Theme Generated + +## CSS Variables + +```css +{{cssVariables}} +``` + +## Figma Variables + +Save this as `figma-variables.json`: + +```json +{{figmaVariables}} +``` + +## Summary + +- **Total tokens**: {{totalTokens}} +- **Colors**: {{colorTokens}} +- **Typography**: {{typographyTokens}} +- **Spacing**: {{spacingTokens}} + +{{documentation}} diff --git a/src/tools/_templates/get-color-scale-result.md b/src/tools/_templates/get-color-scale-result.md new file mode 100644 index 0000000..99c647e --- /dev/null +++ b/src/tools/_templates/get-color-scale-result.md @@ -0,0 +1,14 @@ +# {{colorFamily}} Color Scale + +## Base Color +- **HSL**: `hsl({{hue}} {{saturation}} {{lightness}})` +- **CSS Variable**: `--op-color-{{colorFamily}}-original` + +## Scale Tokens +{{scaleTokens}} + +## Pairing Guidance +{{pairingGuidance}} + +## Usage Examples +{{usageExamples}} diff --git a/src/tools/_templates/get-component-html-result.md b/src/tools/_templates/get-component-html-result.md new file mode 100644 index 0000000..f2f75f7 --- /dev/null +++ b/src/tools/_templates/get-component-html-result.md @@ -0,0 +1,23 @@ +# {{componentName}} Component + +## HTML Structure + +```html +{{exampleHtml}} +``` + +## CSS Class +- **Base class**: `{{className}}` +- **Type**: {{type}} + +## Modifiers +{{modifiers}} + +## Elements +{{elements}} + +## Documentation +{{docsUrl}} + +## Usage Notes +{{description}} diff --git a/src/tools/_templates/get-layout-utility-result.md b/src/tools/_templates/get-layout-utility-result.md new file mode 100644 index 0000000..ecc9f13 --- /dev/null +++ b/src/tools/_templates/get-layout-utility-result.md @@ -0,0 +1,22 @@ +# {{utilityName}} Layout Utility + +## HTML Structure + +```html +{{exampleHtml}} +``` + +## CSS Class +`{{className}}` + +## Purpose +{{description}} + +## When to Use +{{whenToUse}} + +## Common Patterns +{{patterns}} + +## Documentation +{{docsUrl}} diff --git a/src/tools/_templates/suggest-token-migration-header.md b/src/tools/_templates/suggest-token-migration-header.md new file mode 100644 index 0000000..20db436 --- /dev/null +++ b/src/tools/_templates/suggest-token-migration-header.md @@ -0,0 +1,5 @@ +# Token Migration Suggestions + +**Input Value**: `{{inputValue}}` + +## Suggested Tokens diff --git a/src/tools/_templates/suggest-token-migration-none.md b/src/tools/_templates/suggest-token-migration-none.md new file mode 100644 index 0000000..c0a5d3c --- /dev/null +++ b/src/tools/_templates/suggest-token-migration-none.md @@ -0,0 +1,6 @@ +# Token Migration Suggestions + +**Input Value**: `{{inputValue}}` + +No suitable tokens found for this value. +Consider adding a new token to your design system. diff --git a/src/tools/_templates/validate-color-pairing-error.md b/src/tools/_templates/validate-color-pairing-error.md new file mode 100644 index 0000000..4bb7e90 --- /dev/null +++ b/src/tools/_templates/validate-color-pairing-error.md @@ -0,0 +1,11 @@ +# Color Pairing Validation Error + +## Tokens +- **Background**: `{{backgroundToken}}` +- **Text**: `{{textToken}}` + +## Error +{{reason}} + +## Suggestion +{{suggestion}} diff --git a/src/tools/_templates/validate-color-pairing-result.md b/src/tools/_templates/validate-color-pairing-result.md new file mode 100644 index 0000000..163cdd1 --- /dev/null +++ b/src/tools/_templates/validate-color-pairing-result.md @@ -0,0 +1,16 @@ +# Color Pairing Validation + +## Pair Analysis +- **Background**: `{{backgroundToken}}` ({{backgroundValue}}) +- **Text**: `{{textToken}}` ({{textValue}}) + +## Contrast Check +- **Ratio**: {{contrastRatio}}:1 +- **WCAG AA**: {{wcagAA}} +- **WCAG AAA**: {{wcagAAA}} + +## Pairing Rules +{{pairingRules}} + +## Result +{{result}} diff --git a/src/tools/_templates/validate-token-usage-header.md b/src/tools/_templates/validate-token-usage-header.md new file mode 100644 index 0000000..6ccc5b4 --- /dev/null +++ b/src/tools/_templates/validate-token-usage-header.md @@ -0,0 +1,7 @@ +# Token Validation Report + +**Status**: {{status}} +**Issues**: {{issueCount}} +**Values Checked**: {{totalChecked}} + +## Issues diff --git a/src/tools/_templates/validate-token-usage-valid.md b/src/tools/_templates/validate-token-usage-valid.md new file mode 100644 index 0000000..eaa5b43 --- /dev/null +++ b/src/tools/_templates/validate-token-usage-valid.md @@ -0,0 +1,7 @@ +# Token Validation Report + +**Status**: ✓ Valid +**Issues**: 0 +**Values Checked**: {{totalChecked}} + +✓ No issues found! All values use design tokens. diff --git a/src/tools/accessibility.ts b/src/tools/accessibility.ts deleted file mode 100644 index 5a44602..0000000 --- a/src/tools/accessibility.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Accessibility checker tool - * Validates WCAG contrast ratios for token combinations - */ - -import { DesignToken } from '../optics-data.js'; -import { checkContrast, ContrastResult } from '../utils/color.js'; - -export interface ContrastCheckResult { - foregroundToken: string; - backgroundToken: string; - foregroundValue: string; - backgroundValue: string; - contrast: ContrastResult | null; - passes: boolean; - recommendation?: string; -} - -/** - * Check contrast between two tokens - */ -export function checkTokenContrast( - foregroundToken: string, - backgroundToken: string, - tokens: DesignToken[] -): ContrastCheckResult { - const fgToken = tokens.find(t => t.name === foregroundToken); - const bgToken = tokens.find(t => t.name === backgroundToken); - - if (!fgToken || !bgToken) { - return { - foregroundToken, - backgroundToken, - foregroundValue: '', - backgroundValue: '', - contrast: null, - passes: false, - recommendation: 'Token not found' - }; - } - - const contrast = checkContrast(fgToken.value, bgToken.value); - - if (!contrast) { - return { - foregroundToken, - backgroundToken, - foregroundValue: fgToken.value, - backgroundValue: bgToken.value, - contrast: null, - passes: false, - recommendation: 'Unable to calculate contrast (non-color tokens?)' - }; - } - - const passes = contrast.wcagAA; - let recommendation = ''; - - if (!passes) { - recommendation = findBetterTokenCombination(fgToken, tokens, bgToken.value); - } - - return { - foregroundToken, - backgroundToken, - foregroundValue: fgToken.value, - backgroundValue: bgToken.value, - contrast, - passes, - recommendation - }; -} - -/** - * Find better token combination with sufficient contrast - */ -function findBetterTokenCombination( - currentToken: DesignToken, - allTokens: DesignToken[], - backgroundValue: string -): string { - const colorTokens = allTokens.filter(t => t.category === 'color'); - - for (const token of colorTokens) { - const contrast = checkContrast(token.value, backgroundValue); - if (contrast && contrast.wcagAA) { - return `Try using ${token.name} (${token.value}) for better contrast`; - } - } - - return 'No alternative tokens found with sufficient contrast'; -} - -/** - * Format contrast check result - */ -export function formatContrastResult(result: ContrastCheckResult): string { - const lines: string[] = [ - '# Contrast Check Result', - '', - `**Foreground**: ${result.foregroundToken} (\`${result.foregroundValue}\`)`, - `**Background**: ${result.backgroundToken} (\`${result.backgroundValue}\`)`, - '' - ]; - - if (result.contrast) { - lines.push(`**Contrast Ratio**: ${result.contrast.ratio}:1`); - lines.push(`**WCAG AA**: ${result.contrast.wcagAA ? '✓ Pass' : '✗ Fail'}`); - lines.push(`**WCAG AAA**: ${result.contrast.wcagAAA ? '✓ Pass' : '✗ Fail'}`); - lines.push(`**Score**: ${result.contrast.score}`); - lines.push(''); - - if (!result.passes && result.recommendation) { - lines.push('## Recommendation'); - lines.push(result.recommendation); - } - } else { - lines.push('✗ Unable to calculate contrast'); - if (result.recommendation) { - lines.push(`**Reason**: ${result.recommendation}`); - } - } - - return lines.join('\n'); -} - -/** - * Check all color token combinations for a given background - */ -export function checkAllCombinations( - backgroundToken: string, - tokens: DesignToken[] -): ContrastCheckResult[] { - const bgToken = tokens.find(t => t.name === backgroundToken); - if (!bgToken) return []; - - const colorTokens = tokens.filter(t => t.category === 'color' && t.name !== backgroundToken); - const results: ContrastCheckResult[] = []; - - for (const fgToken of colorTokens) { - results.push(checkTokenContrast(fgToken.name, backgroundToken, tokens)); - } - - return results.sort((a, b) => { - if (!a.contrast || !b.contrast) return 0; - return b.contrast.ratio - a.contrast.ratio; - }); -} diff --git a/src/tools/calculation/calculate-contrast-tool.ts b/src/tools/calculation/calculate-contrast-tool.ts new file mode 100644 index 0000000..70cb6a8 --- /dev/null +++ b/src/tools/calculation/calculate-contrast-tool.ts @@ -0,0 +1,149 @@ +/** + * Calculate Contrast Tool + * Validates WCAG contrast ratios for token combinations + * (Renamed from check_contrast for clarity) + */ + +import { z } from 'zod'; +import Tool, { type ToolInputSchema } from '../tool.js'; +import { designTokens, type DesignToken } from '../../optics-data.js'; +import { checkContrast, type ContrastResult } from '../../utils/color.js'; +import { readToolFile } from '../../_internal/resource-path.js'; + +export interface ContrastCheckResult { + foregroundToken: string; + backgroundToken: string; + foregroundValue: string; + backgroundValue: string; + contrast: ContrastResult | null; + passes: boolean; + recommendation?: string; +} + +class CalculateContrastTool extends Tool { + name = 'calculate_contrast'; + title = 'Calculate Contrast'; + description = 'Calculate WCAG contrast ratio between two color tokens'; + + inputSchema = { + foregroundToken: z.string().describe('Foreground color token name'), + backgroundToken: z.string().describe('Background color token name'), + }; + + async handler(args: ToolInputSchema): Promise { + const { foregroundToken, backgroundToken } = args; + const result = this.checkTokenContrast(foregroundToken, backgroundToken, designTokens); + const formatted = await this.formatContrastResult(result); + + return formatted; + } + + /** + * Check contrast between two tokens + */ + private checkTokenContrast( + foregroundToken: string, + backgroundToken: string, + tokens: DesignToken[] + ): ContrastCheckResult { + const fgToken = tokens.find(t => t.name === foregroundToken); + const bgToken = tokens.find(t => t.name === backgroundToken); + + if (!fgToken || !bgToken) { + return { + foregroundToken, + backgroundToken, + foregroundValue: '', + backgroundValue: '', + contrast: null, + passes: false, + recommendation: 'Token not found' + }; + } + + const contrast = checkContrast(fgToken.value, bgToken.value); + + if (!contrast) { + return { + foregroundToken, + backgroundToken, + foregroundValue: fgToken.value, + backgroundValue: bgToken.value, + contrast: null, + passes: false, + recommendation: 'Unable to calculate contrast (non-color tokens?)' + }; + } + + const passes = contrast.wcagAA; + let recommendation = ''; + + if (!passes) { + recommendation = this.findBetterTokenCombination(fgToken, tokens, bgToken.value); + } + + return { + foregroundToken, + backgroundToken, + foregroundValue: fgToken.value, + backgroundValue: bgToken.value, + contrast, + passes, + recommendation + }; + } + + /** + * Find better token combination with sufficient contrast + */ + private findBetterTokenCombination( + currentToken: DesignToken, + allTokens: DesignToken[], + backgroundValue: string + ): string { + const colorTokens = allTokens.filter(t => t.category === 'color'); + + for (const token of colorTokens) { + const contrast = checkContrast(token.value, backgroundValue); + if (contrast && contrast.wcagAA) { + return `Try using ${token.name} (${token.value}) for better contrast`; + } + } + + return 'No alternative tokens found with sufficient contrast'; + } + + /** + * Format contrast check result + */ + private async formatContrastResult(result: ContrastCheckResult): Promise { + if (result.contrast) { + const template = await readToolFile('check-contrast-result.md'); + let output = template + .replace('{{foregroundToken}}', result.foregroundToken) + .replace('{{foregroundValue}}', result.foregroundValue) + .replace('{{backgroundToken}}', result.backgroundToken) + .replace('{{backgroundValue}}', result.backgroundValue) + .replace('{{contrastRatio}}', result.contrast.ratio.toString()) + .replace('{{wcagAA}}', result.contrast.wcagAA ? '✓ Pass' : '✗ Fail') + .replace('{{wcagAAA}}', result.contrast.wcagAAA ? '✓ Pass' : '✗ Fail') + .replace('{{score}}', result.contrast.score); + + if (!result.passes && result.recommendation) { + output += '\n\n## Recommendation\n' + result.recommendation; + } + + return output; + } else { + const template = await readToolFile('check-contrast-error.md'); + return template + .replace('{{foregroundToken}}', result.foregroundToken) + .replace('{{foregroundValue}}', result.foregroundValue) + .replace('{{backgroundToken}}', result.backgroundToken) + .replace('{{backgroundValue}}', result.backgroundValue) + .replace('{{reason}}', result.recommendation || 'Unknown error'); + } + } +} + +export default CalculateContrastTool; diff --git a/src/tools/calculation/calculate-hsl-tokens-tool.ts b/src/tools/calculation/calculate-hsl-tokens-tool.ts new file mode 100644 index 0000000..3bf5c2a --- /dev/null +++ b/src/tools/calculation/calculate-hsl-tokens-tool.ts @@ -0,0 +1,266 @@ +/** + * Calculate HSL Tokens Tool + * Converts hex colors to Optics HSL format tokens + * (Renamed from generate_theme for clarity - focused on the calculation aspect) + */ + +import { z } from 'zod'; +import Tool, { type ToolInputSchema } from '../tool.js'; +import { designTokens, type DesignToken } from '../../optics-data.js'; +import { generateFigmaVariablesJSON } from '../../utils/figma-tokens.js'; +import { readToolFile } from '../../_internal/resource-path.js'; + +interface BrandColors { + primary?: string; + neutral?: string; + 'alerts-warning'?: string; + 'alerts-danger'?: string; + 'alerts-info'?: string; + 'alerts-notice'?: string; +} + +interface GeneratedTheme { + cssVariables: string; + figmaVariables: string; + tokens: DesignToken[]; + documentation: string; +} + +class CalculateHslTokensTool extends Tool { + name = 'calculate_hsl_tokens'; + title = 'Calculate HSL Tokens'; + description = 'Convert hex colors to Optics HSL format and generate theme tokens'; + + inputSchema = { + brandName: z + .string() + .describe('The name of the brand/theme (e.g., "Acme Corp")'), + primary: z + .string() + .describe('Primary brand color (hex, e.g., "#FF5733")'), + neutral: z + .string() + .optional() + .describe('Neutral color (hex, optional)') + }; + + async handler(args: ToolInputSchema): Promise { + const brandColors: BrandColors = { + primary: args.primary, + neutral: args.neutral + }; + const theme = await this.generateTheme(args.brandName, brandColors); + + let output = await readToolFile('generate-theme-output.md'); + + output = output + .replace('{{brandName}}', args.brandName) + .replace('{{cssVariables}}', theme.cssVariables) + .replace('{{figmaVariables}}', theme.figmaVariables) + .replace('{{totalTokens}}', String(theme.tokens.length)) + .replace('{{colorTokens}}', String(theme.tokens.filter(t => t.category === 'color').length)) + .replace('{{typographyTokens}}', String(theme.tokens.filter(t => t.category === 'typography').length)) + .replace('{{spacingTokens}}', String(theme.tokens.filter(t => t.category === 'spacing').length)) + .replace('{{documentation}}', theme.documentation); + + return output; + } + + /** + * Generate Optics HSL color tokens from brand colors + */ + private generateColorTokens(brandColors: BrandColors): DesignToken[] { + const tokens: DesignToken[] = []; + + const defaults: BrandColors = { + primary: '#2D6FDB', + neutral: '#757882', + 'alerts-warning': '#FFD93D', + 'alerts-danger': '#FF6B94', + 'alerts-info': '#2D6FDB', + 'alerts-notice': '#6ACF71' + }; + + const colors = { ...defaults, ...brandColors }; + + for (const [family, hex] of Object.entries(colors)) { + if (!hex) continue; + + const hsl = this.hexToHSL(hex); + + tokens.push({ + name: `op-color-${family}-h`, + cssVar: `--op-color-${family}-h`, + value: String(hsl.h), + category: 'color', + description: `${family} color hue (HSL) - drives all ${family} scale tokens` + }); + + tokens.push({ + name: `op-color-${family}-s`, + cssVar: `--op-color-${family}-s`, + value: `${hsl.s}%`, + category: 'color', + description: `${family} color saturation (HSL)` + }); + + tokens.push({ + name: `op-color-${family}-l`, + cssVar: `--op-color-${family}-l`, + value: `${hsl.l}%`, + category: 'color', + description: `${family} color lightness (HSL)` + }); + } + + return tokens; + } + + /** + * Convert hex to HSL + */ + private hexToHSL(hex: string): { h: number; s: number; l: number } { + hex = hex.replace('#', ''); + + const r = parseInt(hex.substring(0, 2), 16) / 255; + const g = parseInt(hex.substring(2, 4), 16) / 255; + const b = parseInt(hex.substring(4, 6), 16) / 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h = 0; + let s = 0; + const l = (max + min) / 2; + + if (max !== min) { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break; + case g: h = ((b - r) / d + 2) / 6; break; + case b: h = ((r - g) / d + 4) / 6; break; + } + } + + return { + h: Math.round(h * 360), + s: Math.round(s * 100), + l: Math.round(l * 100) + }; + } + + /** + * Generate CSS variables from tokens + */ + private generateCSSVariables(tokens: DesignToken[], themeName: string = 'default'): string { + const lines: string[] = [ + `/* ${themeName} Theme - Generated by Optics MCP */`, + `/* HSL tokens for easy theming */`, + `:root {` + ]; + + const grouped: Record = {}; + tokens.forEach(token => { + if (!grouped[token.category]) grouped[token.category] = []; + grouped[token.category].push(token); + }); + + if (grouped['color']) { + lines.push(` /* Colors (HSL) */`); + for (const token of grouped['color']) { + lines.push(` --${token.name}: ${token.value};`); + } + lines.push(''); + } + + for (const [category, categoryTokens] of Object.entries(grouped)) { + if (category === 'color') continue; + + lines.push(` /* ${category.charAt(0).toUpperCase() + category.slice(1)} */`); + for (const token of categoryTokens) { + lines.push(` --${token.name}: ${token.value};`); + } + lines.push(''); + } + + lines.push('}'); + + return lines.join('\n'); + } + + /** + * Generate theme documentation + */ + private async generateDocumentation(themeName: string, tokens: DesignToken[]): Promise { + let documentation = await readToolFile('generate-theme-instructions.md'); + + const stats = tokens.reduce((acc, token) => { + acc[token.category] = (acc[token.category] || 0) + 1; + return acc; + }, {} as Record); + + const tokenSummary = Object.entries(stats) + .map(([category, count]) => `- **${category}**: ${count} tokens`) + .join('\n'); + + const grouped: Record = {}; + tokens.forEach(token => { + if (!grouped[token.category]) grouped[token.category] = []; + grouped[token.category].push(token); + }); + + const tokenCategories = Object.entries(grouped) + .map(([category, categoryTokens]) => { + const lines = [ + `### ${category.charAt(0).toUpperCase() + category.slice(1)}`, + '', + '| Token Name | Value | Description |', + '|------------|-------|-------------|' + ]; + + for (const token of categoryTokens) { + lines.push(`| \`${token.name}\` | \`${token.value}\` | ${token.description || ''} |`); + } + + return lines.join('\n'); + }) + .join('\n\n'); + + documentation = documentation + .replace('{{themeName}}', themeName) + .replace('{{tokenSummary}}', tokenSummary) + .replace('{{tokenCategories}}', tokenCategories); + + return documentation; + } + + /** + * Main theme generation function + */ + private async generateTheme(brandName: string, brandColors: BrandColors): Promise { + let tokens: DesignToken[] = [...designTokens]; + + if (Object.keys(brandColors).length > 0) { + const customColorTokens = this.generateColorTokens(brandColors); + + tokens = tokens.map(token => { + const customToken = customColorTokens.find(ct => ct.name === token.name); + return customToken || token; + }); + } + + const cssVariables = this.generateCSSVariables(tokens, brandName); + const figmaVariables = generateFigmaVariablesJSON(tokens, { collectionName: `${brandName} Design System` }); + const documentation = await this.generateDocumentation(brandName, tokens); + + return { + cssVariables, + figmaVariables, + tokens, + documentation + }; + } +} + +export default CalculateHslTokensTool; diff --git a/src/tools/check-contrast-tool.ts b/src/tools/check-contrast-tool.ts new file mode 100644 index 0000000..40f9db2 --- /dev/null +++ b/src/tools/check-contrast-tool.ts @@ -0,0 +1,148 @@ +/** + * Check Contrast Tool + * Validates WCAG contrast ratios for token combinations + */ + +import { z } from 'zod'; +import Tool, { type ToolInputSchema } from './tool.js'; +import { designTokens, type DesignToken } from '../optics-data.js'; +import { checkContrast, type ContrastResult } from '../utils/color.js'; +import { readToolFile } from '../_internal/resource-path.js'; + +export interface ContrastCheckResult { + foregroundToken: string; + backgroundToken: string; + foregroundValue: string; + backgroundValue: string; + contrast: ContrastResult | null; + passes: boolean; + recommendation?: string; +} + +class CheckContrastTool extends Tool { + name = 'check_contrast'; + title = 'Check Contrast'; + description = 'Check WCAG contrast ratio between two color tokens'; + + inputSchema = { + foregroundToken: z.string().describe('Foreground color token name'), + backgroundToken: z.string().describe('Background color token name'), + }; + + async handler(args: ToolInputSchema): Promise { + const { foregroundToken, backgroundToken } = args; + const result = this.checkTokenContrast(foregroundToken, backgroundToken, designTokens); + const formatted = await this.formatContrastResult(result); + + return formatted; + } + + /** + * Check contrast between two tokens + */ + private checkTokenContrast( + foregroundToken: string, + backgroundToken: string, + tokens: DesignToken[] + ): ContrastCheckResult { + const fgToken = tokens.find(t => t.name === foregroundToken); + const bgToken = tokens.find(t => t.name === backgroundToken); + + if (!fgToken || !bgToken) { + return { + foregroundToken, + backgroundToken, + foregroundValue: '', + backgroundValue: '', + contrast: null, + passes: false, + recommendation: 'Token not found' + }; + } + + const contrast = checkContrast(fgToken.value, bgToken.value); + + if (!contrast) { + return { + foregroundToken, + backgroundToken, + foregroundValue: fgToken.value, + backgroundValue: bgToken.value, + contrast: null, + passes: false, + recommendation: 'Unable to calculate contrast (non-color tokens?)' + }; + } + + const passes = contrast.wcagAA; + let recommendation = ''; + + if (!passes) { + recommendation = this.findBetterTokenCombination(fgToken, tokens, bgToken.value); + } + + return { + foregroundToken, + backgroundToken, + foregroundValue: fgToken.value, + backgroundValue: bgToken.value, + contrast, + passes, + recommendation + }; + } + + /** + * Find better token combination with sufficient contrast + */ + private findBetterTokenCombination( + currentToken: DesignToken, + allTokens: DesignToken[], + backgroundValue: string + ): string { + const colorTokens = allTokens.filter(t => t.category === 'color'); + + for (const token of colorTokens) { + const contrast = checkContrast(token.value, backgroundValue); + if (contrast && contrast.wcagAA) { + return `Try using ${token.name} (${token.value}) for better contrast`; + } + } + + return 'No alternative tokens found with sufficient contrast'; + } + + /** + * Format contrast check result + */ + private async formatContrastResult(result: ContrastCheckResult): Promise { + if (result.contrast) { + const template = await readToolFile('check-contrast-result.md'); + let output = template + .replace('{{foregroundToken}}', result.foregroundToken) + .replace('{{foregroundValue}}', result.foregroundValue) + .replace('{{backgroundToken}}', result.backgroundToken) + .replace('{{backgroundValue}}', result.backgroundValue) + .replace('{{contrastRatio}}', result.contrast.ratio.toString()) + .replace('{{wcagAA}}', result.contrast.wcagAA ? '✓ Pass' : '✗ Fail') + .replace('{{wcagAAA}}', result.contrast.wcagAAA ? '✓ Pass' : '✗ Fail') + .replace('{{score}}', result.contrast.score); + + if (!result.passes && result.recommendation) { + output += '\n\n## Recommendation\n' + result.recommendation; + } + + return output; + } else { + const template = await readToolFile('check-contrast-error.md'); + return template + .replace('{{foregroundToken}}', result.foregroundToken) + .replace('{{foregroundValue}}', result.foregroundValue) + .replace('{{backgroundToken}}', result.backgroundToken) + .replace('{{backgroundValue}}', result.backgroundValue) + .replace('{{reason}}', result.recommendation || 'Unknown error'); + } + } +} + +export default CheckContrastTool; diff --git a/src/tools/data-retrieval/get-color-scale-tool.ts b/src/tools/data-retrieval/get-color-scale-tool.ts new file mode 100644 index 0000000..b125a36 --- /dev/null +++ b/src/tools/data-retrieval/get-color-scale-tool.ts @@ -0,0 +1,241 @@ +/** + * Get Color Scale Tool + * Returns all tokens in a color family with pairing guidance + */ + +import { z } from 'zod'; +import Tool, { type ToolInputSchema } from '../tool.js'; +import { designTokens, type DesignToken } from '../../optics-data.js'; +import { readToolFile } from '../../_internal/resource-path.js'; + +/** + * Color pairing rules for each color family + */ +const COLOR_PAIRING_GUIDANCE: Record = { + primary: { + backgrounds: ['color-primary-original', 'color-primary-plus-*', 'color-primary-minus-*'], + textColors: ['color-primary-on-*', 'color-white', 'color-black'], + notes: 'Primary colors should use matching "on" tokens for text. For example, if using --op-color-primary-plus-two as background, use --op-color-primary-on-plus-two for text.' + }, + neutral: { + backgrounds: ['color-neutral-plus-*', 'color-neutral-minus-*', 'color-background'], + textColors: ['color-neutral-on-*', 'color-on-background'], + notes: 'Neutral colors form the foundation of your UI. Always pair neutral backgrounds with their matching "on" tokens.' + }, + 'alerts-danger': { + backgrounds: ['color-alerts-danger-original'], + textColors: ['color-white', 'color-alerts-danger-on-*'], + notes: 'Danger/error colors should be used sparingly. Ensure high contrast for accessibility.' + }, + 'alerts-warning': { + backgrounds: ['color-alerts-warning-original'], + textColors: ['color-black', 'color-neutral-900'], + notes: 'Warning colors often need dark text due to their lighter nature.' + }, + 'alerts-info': { + backgrounds: ['color-alerts-info-original'], + textColors: ['color-white', 'color-alerts-info-on-*'], + notes: 'Info colors are used for informational messages and highlights.' + }, + 'alerts-notice': { + backgrounds: ['color-alerts-notice-original'], + textColors: ['color-black', 'color-neutral-900'], + notes: 'Notice/success colors indicate positive outcomes or confirmations.' + } +}; + +class GetColorScaleTool extends Tool { + name = 'get_color_scale'; + title = 'Get Color Scale'; + description = 'Get all tokens in a color family with pairing guidance'; + + inputSchema = { + colorFamily: z.string().describe('Color family name: "primary", "neutral", "alerts-danger", "alerts-warning", "alerts-info", "alerts-notice"'), + }; + + async handler(args: ToolInputSchema): Promise { + const { colorFamily } = args; + const result = await this.getColorScale(colorFamily.toLowerCase()); + + return result; + } + + /** + * Get all tokens for a color family + */ + private async getColorScale(colorFamily: string): Promise { + // Find all tokens that belong to this color family + const familyTokens = designTokens.filter( + t => t.category === 'color' && t.name.startsWith(`color-${colorFamily}`) + ); + + if (familyTokens.length === 0) { + const availableFamilies = this.getAvailableColorFamilies(); + return `# Color Family Not Found\n\nColor family "${colorFamily}" not found.\n\nAvailable color families:\n${availableFamilies.map(f => `- ${f}`).join('\n')}\n\nUse one of these family names to get the full color scale.`; + } + + const template = await readToolFile('get-color-scale-result.md'); + + // Extract HSL components + const hueToken = familyTokens.find(t => t.name.endsWith('-h')); + const satToken = familyTokens.find(t => t.name.endsWith('-s')); + const lightToken = familyTokens.find(t => t.name.endsWith('-l')); + + // Build scale tokens list + const scaleTokensText = this.formatScaleTokens(familyTokens); + + // Get pairing guidance + const guidance = COLOR_PAIRING_GUIDANCE[colorFamily]; + const pairingText = guidance + ? this.formatPairingGuidance(guidance) + : 'No specific pairing guidance available. Follow the general rule: always pair background colors with their matching "on" text colors.'; + + // Build usage examples + const usageExamples = this.getUsageExamples(colorFamily, familyTokens); + + return template + .replace('{{colorFamily}}', this.capitalize(colorFamily)) + .replace('{{hue}}', hueToken?.value || 'N/A') + .replace('{{saturation}}', satToken?.value || 'N/A') + .replace('{{lightness}}', lightToken?.value || 'N/A') + .replace('{{scaleTokens}}', scaleTokensText) + .replace('{{pairingGuidance}}', pairingText) + .replace('{{usageExamples}}', usageExamples); + } + + /** + * Get available color families from tokens + */ + private getAvailableColorFamilies(): string[] { + const families = new Set(); + + for (const token of designTokens) { + if (token.category === 'color' && token.name.startsWith('color-')) { + // Extract family name (e.g., "primary" from "color-primary-h") + const parts = token.name.replace('color-', '').split('-'); + if (parts.length >= 1) { + // Handle compound names like "alerts-danger" + if (parts[0] === 'alerts' && parts.length >= 2) { + families.add(`alerts-${parts[1]}`); + } else if (!['h', 's', 'l', 'original', 'on', 'plus', 'minus'].includes(parts[0])) { + families.add(parts[0]); + } + } + } + } + + return Array.from(families).sort(); + } + + /** + * Format scale tokens as a list + */ + private formatScaleTokens(tokens: DesignToken[]): string { + // Group tokens by type + const hslTokens = tokens.filter(t => t.name.match(/-[hsl]$/)); + const scaleTokens = tokens.filter(t => t.name.match(/-(plus|minus)-\d+$/)); + const onTokens = tokens.filter(t => t.name.includes('-on-')); + const otherTokens = tokens.filter(t => + !hslTokens.includes(t) && + !scaleTokens.includes(t) && + !onTokens.includes(t) + ); + + const lines: string[] = []; + + if (hslTokens.length > 0) { + lines.push('### HSL Components'); + for (const token of hslTokens) { + lines.push(`- \`${token.cssVar}\`: ${token.value}`); + } + lines.push(''); + } + + if (scaleTokens.length > 0) { + lines.push('### Scale Variations'); + for (const token of scaleTokens.sort((a, b) => a.name.localeCompare(b.name))) { + lines.push(`- \`${token.cssVar}\`: ${token.value}`); + } + lines.push(''); + } + + if (onTokens.length > 0) { + lines.push('### Text Colors (for pairing)'); + for (const token of onTokens.sort((a, b) => a.name.localeCompare(b.name))) { + lines.push(`- \`${token.cssVar}\`: ${token.value}`); + } + lines.push(''); + } + + if (otherTokens.length > 0) { + lines.push('### Other Tokens'); + for (const token of otherTokens) { + lines.push(`- \`${token.cssVar}\`: ${token.value}`); + } + } + + return lines.join('\n'); + } + + /** + * Format pairing guidance + */ + private formatPairingGuidance(guidance: { backgrounds: string[]; textColors: string[]; notes: string }): string { + const lines: string[] = [ + '### Background Colors', + ...guidance.backgrounds.map(b => `- \`--op-${b}\``), + '', + '### Recommended Text Colors', + ...guidance.textColors.map(t => `- \`--op-${t}\``), + '', + '### Important Notes', + guidance.notes + ]; + + return lines.join('\n'); + } + + /** + * Get usage examples for the color family + */ + private getUsageExamples(colorFamily: string, tokens: DesignToken[]): string { + const examples: string[] = []; + + // Find a background and matching text token + const bgToken = tokens.find(t => t.name.includes('original') || t.name.includes('plus')); + const onToken = tokens.find(t => t.name.includes('-on-')); + + if (bgToken && onToken) { + examples.push('```css'); + examples.push('.my-element {'); + examples.push(` background-color: var(${bgToken.cssVar});`); + examples.push(` color: var(${onToken.cssVar});`); + examples.push('}'); + examples.push('```'); + } else if (bgToken) { + examples.push('```css'); + examples.push('.my-element {'); + examples.push(` background-color: var(${bgToken.cssVar});`); + examples.push(` /* Always pair with matching "on" token for text */`); + examples.push('}'); + examples.push('```'); + } + + return examples.length > 0 + ? examples.join('\n') + : 'No specific examples available.'; + } + + /** + * Capitalize first letter + */ + private capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); + } +} + +export default GetColorScaleTool; diff --git a/src/tools/data-retrieval/get-component-html-tool.ts b/src/tools/data-retrieval/get-component-html-tool.ts new file mode 100644 index 0000000..0293a1e --- /dev/null +++ b/src/tools/data-retrieval/get-component-html-tool.ts @@ -0,0 +1,150 @@ +/** + * Get Component HTML Tool + * Returns exact HTML/classes for an Optics component + */ + +import { z } from 'zod'; +import Tool, { type ToolInputSchema } from '../tool.js'; +import { components, type Component } from '../../optics-data.js'; +import { readToolFile } from '../../_internal/resource-path.js'; + +export interface ComponentHTMLResult { + found: boolean; + component?: Component; + variant?: string; + variantHtml?: string; + error?: string; +} + +class GetComponentHtmlTool extends Tool { + name = 'get_component_html'; + title = 'Get Component HTML'; + description = 'Get the exact HTML structure and CSS classes for an Optics component'; + + inputSchema = { + componentName: z.string().describe('Name of the component (e.g., "Button", "Card", "Alert")'), + variant: z.string().optional().describe('Optional variant/modifier (e.g., "primary", "danger", "filled")'), + }; + + async handler(args: ToolInputSchema): Promise { + const { componentName, variant } = args; + const result = this.getComponentHtml(componentName, variant); + const formatted = await this.formatResult(result); + + return formatted; + } + + /** + * Get component HTML and details + */ + private getComponentHtml(componentName: string, variant?: string): ComponentHTMLResult { + const component = components.find( + c => c.name.toLowerCase() === componentName.toLowerCase() + ); + + if (!component) { + // Try to find partial matches + const partialMatches = components.filter( + c => c.name.toLowerCase().includes(componentName.toLowerCase()) || + componentName.toLowerCase().includes(c.name.toLowerCase()) + ); + + if (partialMatches.length > 0) { + return { + found: false, + error: `Component "${componentName}" not found. Did you mean: ${partialMatches.map(c => c.name).join(', ')}?` + }; + } + + return { + found: false, + error: `Component "${componentName}" not found. Use list_components to see available components.` + }; + } + + // If variant specified, generate variant-specific HTML + let variantHtml: string | undefined; + if (variant) { + const matchingModifier = component.modifiers.find( + m => m.toLowerCase().includes(variant.toLowerCase()) + ); + + if (matchingModifier) { + // Generate HTML with the variant class applied + variantHtml = this.applyVariantToHtml(component.exampleHtml, component.className, matchingModifier); + } + } + + return { + found: true, + component, + variant, + variantHtml + }; + } + + /** + * Apply a variant modifier to the example HTML + */ + private applyVariantToHtml(html: string, baseClass: string, modifier: string): string { + // Replace the base class with base class + modifier + const classPattern = new RegExp(`class="([^"]*\\b${baseClass}\\b[^"]*)"`, 'g'); + return html.replace(classPattern, (match, classes) => { + // Check if modifier already exists + if (classes.includes(modifier)) { + return match; + } + return `class="${classes} ${modifier}"`; + }); + } + + /** + * Format the result + */ + private async formatResult(result: ComponentHTMLResult): Promise { + if (!result.found || !result.component) { + return `# Component Not Found\n\n${result.error}`; + } + + const component = result.component; + const template = await readToolFile('get-component-html-result.md'); + + // Format modifiers list + const modifiersText = component.modifiers.length > 0 + ? component.modifiers.map(m => `- \`${m}\``).join('\n') + : 'No modifiers available'; + + // Format elements list + const elementsText = component.elements.length > 0 + ? component.elements.map(e => `- \`${e}\``).join('\n') + : 'No sub-elements defined'; + + // Use variant HTML if available, otherwise use base example + const htmlToShow = result.variantHtml || component.exampleHtml; + + let output = template + .replace('{{componentName}}', component.name) + .replace('{{exampleHtml}}', htmlToShow) + .replace('{{className}}', component.className) + .replace('{{type}}', component.type) + .replace('{{modifiers}}', modifiersText) + .replace('{{elements}}', elementsText) + .replace('{{docsUrl}}', component.docsUrl || 'No documentation URL available') + .replace('{{description}}', component.description); + + // Add variant info if specified + if (result.variant && result.variantHtml) { + output += `\n\n## Applied Variant\n- **Variant**: \`${result.variant}\`\n- The HTML above includes the variant modifier class.`; + } else if (result.variant && !result.variantHtml) { + const availableVariants = component.modifiers + .filter(m => m.includes('--')) + .map(m => m.split('--')[1]) + .join(', '); + output += `\n\n## Variant Note\nVariant "${result.variant}" not found. Available variants: ${availableVariants || 'none'}`; + } + + return output; + } +} + +export default GetComponentHtmlTool; diff --git a/src/tools/data-retrieval/get-layout-utility-tool.ts b/src/tools/data-retrieval/get-layout-utility-tool.ts new file mode 100644 index 0000000..aaa71b8 --- /dev/null +++ b/src/tools/data-retrieval/get-layout-utility-tool.ts @@ -0,0 +1,202 @@ +/** + * Get Layout Utility Tool + * Returns HTML patterns and guidance for Optics layout utilities + */ + +import { z } from 'zod'; +import Tool, { type ToolInputSchema } from '../tool.js'; +import { components, type Component } from '../../optics-data.js'; +import { readToolFile } from '../../_internal/resource-path.js'; + +/** + * Extended layout utility information + */ +const LAYOUT_UTILITY_DETAILS: Record = { + stack: { + whenToUse: 'Use Stack for vertical layouts with consistent spacing between items. Perfect for forms, card content, navigation lists, and any vertical flow of content.', + patterns: [ + '**Form layout**: Wrap form fields in a stack for consistent vertical spacing', + '**Card content**: Use inside cards to space out title, description, and actions', + '**Navigation**: Vertical nav items with consistent gaps', + '**Article content**: Space out paragraphs and sections' + ], + responsiveModifiers: ['op-stack--small', 'op-stack--large'] + }, + cluster: { + whenToUse: 'Use Cluster for horizontal layouts that wrap naturally. Ideal for tags, buttons, badges, and any group of items that should flow and wrap.', + patterns: [ + '**Tag groups**: Display multiple tags that wrap to new lines', + '**Button groups**: Group related actions horizontally', + '**Badge collections**: Show multiple badges inline', + '**Filter chips**: Wrap filter options naturally' + ], + responsiveModifiers: ['op-cluster--small', 'op-cluster--large'] + }, + split: { + whenToUse: 'Use Split for two-column layouts where content is pushed to opposite ends. Perfect for headers, footers, and any "left vs right" arrangement.', + patterns: [ + '**Header layout**: Logo on left, navigation on right', + '**Card footer**: Metadata on left, actions on right', + '**List items**: Label on left, value on right', + '**Toolbar**: Title on left, buttons on right' + ], + responsiveModifiers: [] + }, + sidebar: { + whenToUse: 'Use Sidebar for layouts with a fixed-width sidebar and flexible main content area.', + patterns: [ + '**Dashboard layout**: Navigation sidebar with main content', + '**Settings page**: Menu sidebar with settings panels', + '**Documentation**: Table of contents with content area' + ], + responsiveModifiers: ['op-sidebar--right'] + }, + grid: { + whenToUse: 'Use Grid for multi-column layouts with equal-width columns. Great for card grids, galleries, and dashboard widgets.', + patterns: [ + '**Card grid**: Display cards in responsive columns', + '**Image gallery**: Equal-sized image thumbnails', + '**Dashboard widgets**: Arrange dashboard panels', + '**Product listing**: E-commerce product grid' + ], + responsiveModifiers: ['op-grid--2', 'op-grid--3', 'op-grid--4'] + } +}; + +class GetLayoutUtilityTool extends Tool { + name = 'get_layout_utility'; + title = 'Get Layout Utility'; + description = 'Get HTML patterns and usage guidance for Optics layout utilities (Stack, Cluster, Split, Sidebar, Grid)'; + + inputSchema = { + utilityType: z.string().describe('Type of layout utility: "stack", "cluster", "split", "sidebar", or "grid"'), + }; + + async handler(args: ToolInputSchema): Promise { + const { utilityType } = args; + const result = await this.getLayoutUtility(utilityType.toLowerCase()); + + return result; + } + + /** + * Get layout utility details + */ + private async getLayoutUtility(utilityType: string): Promise { + // Find the layout component in optics-data + const layoutComponent = components.find( + c => c.type === 'layout' && c.name.toLowerCase() === utilityType.toLowerCase() + ); + + // Get extended details + const details = LAYOUT_UTILITY_DETAILS[utilityType]; + + if (!layoutComponent && !details) { + const availableLayouts = Object.keys(LAYOUT_UTILITY_DETAILS).join(', '); + return `# Layout Utility Not Found\n\nUtility "${utilityType}" not found.\n\nAvailable layout utilities: ${availableLayouts}\n\nUse one of these utility types to get detailed information.`; + } + + const template = await readToolFile('get-layout-utility-result.md'); + + // Build patterns text + const patternsText = details?.patterns + ? details.patterns.map(p => `- ${p}`).join('\n') + : 'No specific patterns documented.'; + + // Build example HTML with more detail + let exampleHtml = layoutComponent?.exampleHtml || `
...
`; + + // Enhance example HTML based on utility type + exampleHtml = this.getEnhancedExample(utilityType, exampleHtml); + + return template + .replace('{{utilityName}}', this.capitalize(utilityType)) + .replace('{{exampleHtml}}', exampleHtml) + .replace('{{className}}', layoutComponent?.className || `op-${utilityType}`) + .replace('{{description}}', layoutComponent?.description || `Layout utility: op-${utilityType}`) + .replace('{{whenToUse}}', details?.whenToUse || 'Use for layout purposes.') + .replace('{{patterns}}', patternsText) + .replace('{{docsUrl}}', layoutComponent?.docsUrl || `https://docs.optics.rolemodel.design/?path=/docs/layout-${utilityType}--docs`); + } + + /** + * Get enhanced example HTML for each utility type + */ + private getEnhancedExample(utilityType: string, baseExample: string): string { + const examples: Record = { + stack: ` +
+
First item
+
Second item
+
Third item
+
+ + +
+

Title

+

Description text

+ +
`, + + cluster: ` +
+ Tag 1 + Tag 2 + Tag 3 +
+ + +
+ + +
`, + + split: ` +
+
Left content (logo, title)
+
Right content (actions, nav)
+
+ + +
+ Last updated: Today + +
`, + + sidebar: ` +
+ +
+ +
...
+
+
`, + + grid: ` +
+
Card 1
+
Card 2
+
Card 3
+
Card 4
+
` + }; + + return examples[utilityType] || baseExample; + } + + /** + * Capitalize first letter + */ + private capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); + } +} + +export default GetLayoutUtilityTool; diff --git a/src/tools/generate-component-scaffold-tool.ts b/src/tools/generate-component-scaffold-tool.ts new file mode 100644 index 0000000..2aae030 --- /dev/null +++ b/src/tools/generate-component-scaffold-tool.ts @@ -0,0 +1,216 @@ +/** + * Generate Component Scaffold Tool + * Generates component templates with proper token usage + */ + +import { z } from 'zod'; +import Tool, { type ToolInputSchema } from './tool.js'; +import { designTokens, type DesignToken } from '../optics-data.js'; +import { readToolFile } from '../_internal/resource-path.js'; + +export interface ComponentScaffold { + name: string; + typescript: string; + css: string; + usage: string; +} + +class GenerateComponentScaffoldTool extends Tool { + name = 'generate_component_scaffold'; + title = 'Generate Component Scaffold'; + description = 'Generate a React component scaffold with proper token usage'; + + inputSchema = { + componentName: z.string().describe('Name of the component (e.g., "Alert", "Card")'), + description: z.string().describe('Brief description of the component'), + tokens: z.array(z.string()).describe('List of token names the component should use'), + }; + + async handler(args: ToolInputSchema): Promise { + const { componentName, description, tokens } = args; + const scaffold = await this.generateComponentScaffold( + componentName, + description, + tokens, + designTokens + ); + const formatted = this.formatScaffoldOutput(scaffold); + + return formatted; + } + + /** + * Generate component scaffold + */ + private async generateComponentScaffold( + componentName: string, + description: string, + requiredTokens: string[], + allTokens: DesignToken[] + ): Promise { + const validTokens = requiredTokens.filter(tokenName => + allTokens.some(t => t.name === tokenName) + ); + + const typescript = this.generateTypeScriptComponent(componentName, description, validTokens); + const css = this.generateCSSModule(componentName, validTokens, allTokens); + const usage = await this.generateUsageExample(componentName); + + return { + name: componentName, + typescript, + css, + usage + }; + } + + /** + * Generate TypeScript component + */ + private generateTypeScriptComponent( + name: string, + description: string, + tokens: string[] + ): string { + const lines: string[] = [ + `/**`, + ` * ${name} Component`, + ` * ${description}`, + ` * `, + ` * Design tokens used:`, + ...tokens.map(t => ` * - ${t}`), + ` */`, + ``, + `import React from 'react';`, + `import styles from './${name}.module.css';`, + ``, + `export interface ${name}Props {`, + ` children: React.ReactNode;`, + ` className?: string;`, + `}`, + ``, + `export const ${name}: React.FC<${name}Props> = ({ children, className }) => {`, + ` return (`, + `
`, + ` {children}`, + `
`, + ` );`, + `};` + ]; + + return lines.join('\n'); + } + + /** + * Generate CSS module + */ + private generateCSSModule( + name: string, + tokenNames: string[], + allTokens: DesignToken[] + ): string { + const lines: string[] = [ + `/**`, + ` * ${name} Component Styles`, + ` * Uses Optics design tokens for consistent styling`, + ` */`, + ``, + `.${name.toLowerCase()} {` + ]; + + // Group tokens by category + const colorTokens = tokenNames.filter(t => allTokens.find(token => token.name === t && token.category === 'color')); + const spacingTokens = tokenNames.filter(t => allTokens.find(token => token.name === t && token.category === 'spacing')); + const typographyTokens = tokenNames.filter(t => allTokens.find(token => token.name === t && token.category === 'typography')); + const borderTokens = tokenNames.filter(t => allTokens.find(token => token.name === t && token.category === 'border')); + const shadowTokens = tokenNames.filter(t => allTokens.find(token => token.name === t && token.category === 'shadow')); + + // Add color properties + if (colorTokens.length > 0) { + lines.push(` /* Colors */`); + if (colorTokens.some(t => t.includes('background'))) { + const bgToken = colorTokens.find(t => t.includes('background')); + lines.push(` background-color: var(--${bgToken});`); + } + if (colorTokens.some(t => t.includes('text') || t.includes('color-primary'))) { + const textToken = colorTokens.find(t => t.includes('text')) || colorTokens[0]; + lines.push(` color: var(--${textToken});`); + } + } + + // Add spacing + if (spacingTokens.length > 0) { + lines.push(` /* Spacing */`); + const paddingToken = spacingTokens[0]; + lines.push(` padding: var(--${paddingToken});`); + } + + // Add typography + if (typographyTokens.length > 0) { + lines.push(` /* Typography */`); + typographyTokens.forEach(token => { + if (token.includes('font-size')) { + lines.push(` font-size: var(--${token});`); + } else if (token.includes('font-weight')) { + lines.push(` font-weight: var(--${token});`); + } else if (token.includes('line-height')) { + lines.push(` line-height: var(--${token});`); + } else if (token.includes('font-family')) { + lines.push(` font-family: var(--${token});`); + } + }); + } + + // Add borders + if (borderTokens.length > 0) { + lines.push(` /* Borders */`); + lines.push(` border-radius: var(--${borderTokens[0]});`); + } + + // Add shadows + if (shadowTokens.length > 0) { + lines.push(` /* Elevation */`); + lines.push(` box-shadow: var(--${shadowTokens[0]});`); + } + + lines.push(`}`); + lines.push(``); + + return lines.join('\n'); + } + + /** + * Generate usage example + */ + private async generateUsageExample(name: string): Promise { + const template = await readToolFile('generate-component-scaffold-usage.md'); + + return template.replace(/{{componentName}}/g, name); + } + + /** + * Format scaffold output + */ + private formatScaffoldOutput(scaffold: ComponentScaffold): string { + const lines: string[] = [ + `# ${scaffold.name} Component Scaffold`, + ``, + `## TypeScript Component`, + `\`\`\`typescript`, + scaffold.typescript, + `\`\`\``, + ``, + `## CSS Module`, + `\`\`\`css`, + scaffold.css, + `\`\`\``, + ``, + `## Usage`, + scaffold.usage + ]; + + return lines.join('\n'); + } +} + +export default GenerateComponentScaffoldTool; diff --git a/src/tools/sticker-sheet.ts b/src/tools/generate-sticker-sheet-tool.ts similarity index 67% rename from src/tools/sticker-sheet.ts rename to src/tools/generate-sticker-sheet-tool.ts index 7f1e3c5..723b67c 100644 --- a/src/tools/sticker-sheet.ts +++ b/src/tools/generate-sticker-sheet-tool.ts @@ -1,9 +1,12 @@ /** - * Sticker sheet generator + * Generate Sticker Sheet Tool * Generates visual style guide with color swatches and component examples */ -import { DesignToken, Component } from '../optics-data.js'; +import { z } from 'zod'; +import Tool, { type ToolInputSchema } from './tool.js'; +import { designTokens, components, type DesignToken, type Component } from '../optics-data.js'; +import { readToolFile } from '../_internal/resource-path.js'; export type FrameworkType = 'react' | 'vue' | 'svelte' | 'html'; @@ -11,7 +14,6 @@ export interface StickerSheetOptions { framework?: FrameworkType; includeColors?: boolean; includeTypography?: boolean; - includeSpacing?: boolean; includeComponents?: boolean; } @@ -22,32 +24,58 @@ export interface StickerSheet { instructions: string; } -/** - * Generate color swatch component - */ -function generateColorSwatches(tokens: DesignToken[], framework: FrameworkType): string { - const colors = tokens.filter(t => t.category === 'color'); - - const swatchesData = colors.map(token => ({ - name: token.name, - value: token.value, - hsl: token.name.startsWith('color-') ? `var(--op-${token.name.replace('color-', '')}-h) var(--op-${token.name.replace('color-', '')}-s) var(--op-${token.name.replace('color-', '')}-l)` : token.value - })); - - switch (framework) { - case 'react': - return ` +class GenerateStickerSheetTool extends Tool { + name = 'generate_sticker_sheet'; + title = 'Generate Sticker Sheet'; + description = 'Generate a visual style guide with color swatches and component examples'; + + inputSchema = { + framework: z.enum(['react', 'vue', 'svelte', 'html']).optional().describe('Target framework (default: react)'), + includeColors: z.boolean().optional().describe('Include color swatches (default: true)'), + includeTypography: z.boolean().optional().describe('Include typography specimens (default: true)'), + includeComponents: z.boolean().optional().describe('Include component examples (default: true)'), + }; + + async handler(args: ToolInputSchema): Promise { + const { framework, includeColors, includeTypography, includeComponents } = args; + const options = { + framework: framework ?? 'react', + includeColors: includeColors ?? true, + includeTypography: includeTypography ?? true, + includeComponents: includeComponents ?? true, + }; + const sheet = await this.generateStickerSheet(designTokens, components, options); + const formatted = this.formatStickerSheet(sheet); + + return formatted; + } + + /** + * Generate color swatch component + */ + private generateColorSwatches(tokens: DesignToken[], framework: FrameworkType): string { + const colors = tokens.filter(t => t.category === 'color'); + + const swatchesData = colors.map(token => ({ + name: token.name, + value: token.value, + hsl: token.name.startsWith('color-') ? `var(--op-${token.name.replace('color-', '')}-h) var(--op-${token.name.replace('color-', '')}-s) var(--op-${token.name.replace('color-', '')}-l)` : token.value + })); + + switch (framework) { + case 'react': + return ` export function ColorSwatches() { const colors = ${JSON.stringify(swatchesData, null, 2)}; - + return (

Color Palette

{colors.map(color => (
-
@@ -61,15 +89,15 @@ export function ColorSwatches() { ); }`; - case 'vue': - return ` + case 'vue': + return `