diff --git a/tools/dss_mcp/context/__init__.py b/apps/__init__.py similarity index 100% rename from tools/dss_mcp/context/__init__.py rename to apps/__init__.py diff --git a/tools/dss_mcp/integrations/__init__.py b/apps/api/__init__.py similarity index 100% rename from tools/dss_mcp/integrations/__init__.py rename to apps/api/__init__.py diff --git a/tools/api/ai_providers.py b/apps/api/ai_providers.py similarity index 100% rename from tools/api/ai_providers.py rename to apps/api/ai_providers.py diff --git a/tools/api/browser_logger.py b/apps/api/browser_logger.py similarity index 100% rename from tools/api/browser_logger.py rename to apps/api/browser_logger.py diff --git a/tools/api/server.py b/apps/api/server.py similarity index 97% rename from tools/api/server.py rename to apps/api/server.py index 21c34c3..928d019 100644 --- a/tools/api/server.py +++ b/apps/api/server.py @@ -21,15 +21,15 @@ import os from pathlib import Path from dotenv import load_dotenv -# Get project root - tools/api/server.py -> tools/api -> tools -> project_root +# Get project root - apps/api/server.py -> apps/api -> apps -> project_root _server_file = Path(__file__).resolve() _project_root = _server_file.parent.parent.parent # /home/.../dss # Try loading from multiple possible .env locations env_paths = [ - _project_root / "dss-mvp1" / ".env", # dss-mvp1/.env (primary) - _project_root / ".env", # root .env - _server_file.parent / ".env", # tools/api/.env + _project_root / ".env", # root .env (primary) + _project_root / "storybook" / ".env", # storybook/.env + _server_file.parent / ".env", # apps/api/.env ] for env_path in env_paths: if env_path.exists(): @@ -50,33 +50,70 @@ from pydantic import BaseModel from typing import Optional import sys -# Add tools directory to path (legacy imports) -sys.path.insert(0, str(Path(__file__).parent.parent)) -# Add dss-mvp1 directory to path (consolidated dss package) -sys.path.insert(0, str(Path(__file__).parent.parent.parent / "dss-mvp1")) +# Add project root to path for dss package +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) -# Import browser logger router -from browser_logger import router as browser_log_router +# Import browser logger router (local import from same directory) +from apps.api.browser_logger import router as browser_log_router -# Legacy imports (will gradually migrate these) -from config import config -from storage.json_store import ( +# DSS package imports - unified package +from dss import settings +from dss.storage.json_store import ( Projects, Components, SyncHistory, ActivityLog, Teams, Cache, get_stats, FigmaFiles, CodeMetrics, TestResults, TokenDrift, Tokens, Styles, Integrations, IntegrationHealth ) -from figma.figma_tools import FigmaToolSuite +from dss.figma.figma_tools import FigmaToolSuite +from dss.services.project_manager import ProjectManager +from dss.services.config_service import ConfigService, DSSConfig +from dss.services.sandboxed_fs import SandboxedFS -# New consolidated dss imports - now available! -# from dss import DesignToken, TokenSource, ProjectScanner, etc. +# Additional DSS imports available: +# from dss import DesignToken, TokenSource, ProjectScanner # from dss.ingest import CSSTokenSource, SCSSTokenSource, TailwindTokenSource # from dss.analyze import ReactAnalyzer, StyleAnalyzer, QuickWinFinder # from dss.storybook import StorybookScanner, StoryGenerator -# MVP1 Configuration Architecture - Services -from services.project_manager import ProjectManager -from services.config_service import ConfigService, DSSConfig -from services.sandboxed_fs import SandboxedFS + +# === Legacy Config Compatibility === +# Wrapper to maintain compatibility with old config.x.y references +class _FigmaConfigCompat: + @property + def is_configured(self): + return settings.figma_configured + @property + def token(self): + return settings.FIGMA_TOKEN + @property + def cache_ttl(self): + return settings.FIGMA_CACHE_TTL + +class _ServerConfigCompat: + @property + def env(self): + return settings.SERVER_ENV + @property + def port(self): + return settings.SERVER_PORT + @property + def host(self): + return settings.SERVER_HOST + @property + def is_production(self): + return settings.is_production + +class _ConfigCompat: + figma = _FigmaConfigCompat() + server = _ServerConfigCompat() + + def summary(self): + return { + "figma": {"configured": settings.figma_configured, "cache_ttl": settings.FIGMA_CACHE_TTL}, + "server": {"port": settings.SERVER_PORT, "env": settings.SERVER_ENV, "log_level": settings.LOG_LEVEL}, + "database": {"path": str(settings.DATABASE_PATH)}, + } + +config = _ConfigCompat() # === Runtime Configuration === diff --git a/tools/dss_mcp/tools/__init__.py b/apps/cli/__init__.py similarity index 100% rename from tools/dss_mcp/tools/__init__.py rename to apps/cli/__init__.py diff --git a/apps/cli/package-lock.json b/apps/cli/package-lock.json new file mode 100644 index 0000000..20dcbd3 --- /dev/null +++ b/apps/cli/package-lock.json @@ -0,0 +1,624 @@ +{ + "name": "@overbits/dss", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@overbits/dss", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "commander": "^12.0.0", + "conf": "^12.0.0", + "node-fetch": "^3.3.2", + "ora": "^8.0.1" + }, + "bin": { + "dss": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.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": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/atomically": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.0.tgz", + "integrity": "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==", + "license": "MIT", + "dependencies": { + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/conf": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-12.0.0.tgz", + "integrity": "sha512-fIWyWUXrJ45cHCIQX+Ck1hrZDIf/9DR0P0Zewn3uNht28hbt5OfGUq8rRWsxi96pZWPyBEd0eY9ama01JTaknA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "atomically": "^2.0.2", + "debounce-fn": "^5.1.2", + "dot-prop": "^8.0.2", + "env-paths": "^3.0.0", + "json-schema-typed": "^8.0.1", + "semver": "^7.5.4", + "uint8array-extras": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debounce-fn": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-5.1.2.tgz", + "integrity": "sha512-Sr4SdOZ4vw6eQDvPYNxHogvrxmCIld/VenC5JbNrFwMiwd7lY/Z18ZFfo+EWNG4DD9nFlAujWAo/wGuOPHmy5A==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-8.0.2.tgz", + "integrity": "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^3.8.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/stubborn-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", + "license": "MIT", + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "license": "MIT" + }, + "node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uint8array-extras": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-0.3.0.tgz", + "integrity": "sha512-erJsJwQ0tKdwuqI0359U8ijkFmfiTcq25JvvzRVc1VP+2son1NJRXhxcAKJmAW3ajM8JSGAfsAXye8g4s+znxA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/when-exit": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", + "license": "MIT" + } + } +} diff --git a/apps/cli/package.json b/apps/cli/package.json new file mode 100644 index 0000000..8eab7b1 --- /dev/null +++ b/apps/cli/package.json @@ -0,0 +1,51 @@ +{ + "name": "@overbits/dss", + "version": "0.1.0", + "description": "Design System Server - UI Developer Companion", + "type": "module", + "main": "dist/index.js", + "bin": { + "dss": "dist/cli.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "start": "node dist/cli.js", + "postinstall": "node scripts/postinstall.js", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "design-system", + "figma", + "tokens", + "components", + "ui", + "developer-tools" + ], + "author": "Overbits", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/overbits/dss" + }, + "engines": { + "node": ">=18.0.0" + }, + "dependencies": { + "commander": "^12.0.0", + "chalk": "^5.3.0", + "ora": "^8.0.1", + "conf": "^12.0.0", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "typescript": "^5.3.0" + }, + "files": [ + "dist", + "python/api", + "scripts", + "README.md" + ] +} diff --git a/tools/api/requirements.txt b/apps/cli/python/api/requirements.txt similarity index 71% rename from tools/api/requirements.txt rename to apps/cli/python/api/requirements.txt index bad3980..d7b30fe 100644 --- a/tools/api/requirements.txt +++ b/apps/cli/python/api/requirements.txt @@ -3,5 +3,3 @@ uvicorn[standard]>=0.23.0 httpx>=0.24.0 python-dotenv>=1.0.0 pydantic>=2.0.0 -mcp>=1.0.0 -google-generativeai>=0.3.0 diff --git a/apps/cli/python/api/server.py b/apps/cli/python/api/server.py new file mode 100644 index 0000000..c13bffb --- /dev/null +++ b/apps/cli/python/api/server.py @@ -0,0 +1,724 @@ +""" +Design System Server (DSS) - FastAPI Server + +Portable API server providing: +- Project management (CRUD) +- Figma integration endpoints +- Discovery & health endpoints +- Activity tracking +- Runtime configuration management +- Service discovery (Storybook, etc.) + +Modes: +- Server: Deployed remotely, serves design systems to teams +- Local: Dev companion, UI advisor, local services + +Uses SQLite for persistence, integrates with Figma tools. +""" + +import asyncio +import subprocess +import json +import os +from pathlib import Path +from typing import Optional, List, Dict, Any +from datetime import datetime + +from fastapi import FastAPI, HTTPException, Query, BackgroundTasks +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + +import sys +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from config import config +from storage.json_store import ( + Projects, Components, SyncHistory, ActivityLog, Teams, Cache, get_stats +) +from figma.figma_tools import FigmaToolSuite + + +# === Runtime Configuration === + +class RuntimeConfig: + """ + Runtime configuration that can be modified from the dashboard. + Persists to .dss/runtime-config.json for portability. + """ + def __init__(self): + self.config_path = Path(__file__).parent.parent.parent / ".dss" / "runtime-config.json" + self.config_path.parent.mkdir(parents=True, exist_ok=True) + self._data = self._load() + + def _load(self) -> dict: + if self.config_path.exists(): + try: + return json.loads(self.config_path.read_text()) + except: + pass + return { + "mode": "local", # "local" or "server" + "figma": {"token": "", "configured": False}, + "services": { + "storybook": {"enabled": False, "port": 6006, "url": ""}, + "chromatic": {"enabled": False, "project_token": ""}, + "github": {"enabled": False, "repo": ""}, + }, + "features": { + "visual_qa": True, + "token_sync": True, + "code_gen": True, + "ai_advisor": False, + } + } + + def _save(self): + self.config_path.write_text(json.dumps(self._data, indent=2)) + + def get(self, key: str = None): + if key is None: + # Return safe copy without secrets + safe = self._data.copy() + if safe.get("figma", {}).get("token"): + safe["figma"]["token"] = "***configured***" + return safe + return self._data.get(key) + + def set(self, key: str, value: Any): + self._data[key] = value + self._save() + return self._data[key] + + def update(self, updates: dict): + for key, value in updates.items(): + if isinstance(value, dict) and isinstance(self._data.get(key), dict): + self._data[key].update(value) + else: + self._data[key] = value + self._save() + return self.get() + + def set_figma_token(self, token: str): + self._data["figma"]["token"] = token + self._data["figma"]["configured"] = bool(token) + self._save() + # Also update the global config + os.environ["FIGMA_TOKEN"] = token + return {"configured": bool(token)} + + +runtime_config = RuntimeConfig() + + +# === Service Discovery === + +class ServiceDiscovery: + """Discovers and manages companion services.""" + + KNOWN_SERVICES = { + "storybook": {"ports": [6006, 6007], "health": "/"}, + "chromatic": {"ports": [], "health": None}, + "vite": {"ports": [5173, 5174, 3000], "health": "/"}, + "webpack": {"ports": [8080, 8081], "health": "/"}, + "nextjs": {"ports": [3000, 3001], "health": "/"}, + } + + @classmethod + async def discover(cls) -> dict: + """Discover running services by checking known ports.""" + import socket + + discovered = {} + for service, info in cls.KNOWN_SERVICES.items(): + for port in info["ports"]: + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(0.5) + result = sock.connect_ex(('127.0.0.1', port)) + sock.close() + if result == 0: + discovered[service] = { + "running": True, + "port": port, + "url": f"http://localhost:{port}" + } + break + except: + pass + if service not in discovered: + discovered[service] = {"running": False, "port": None, "url": None} + + return discovered + + @classmethod + async def check_storybook(cls) -> dict: + """Check Storybook status specifically.""" + import httpx + + configured = runtime_config.get("services").get("storybook", {}) + port = configured.get("port", 6006) + url = configured.get("url") or f"http://localhost:{port}" + + try: + async with httpx.AsyncClient(timeout=2.0) as client: + resp = await client.get(url) + return { + "running": resp.status_code == 200, + "url": url, + "port": port + } + except: + return {"running": False, "url": url, "port": port} + + +# === App Setup === + +app = FastAPI( + title="Design System Server (DSS)", + description="API for design system management and Figma integration", + version="1.0.0" +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Mount Admin UI static files +UI_DIR = Path(__file__).parent.parent.parent / "admin-ui" +if UI_DIR.exists(): + app.mount("/admin-ui", StaticFiles(directory=str(UI_DIR), html=True), name="admin-ui") + +# Initialize Figma tools +figma_suite = FigmaToolSuite(output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output")) + + +# === Request/Response Models === + +class ProjectCreate(BaseModel): + name: str + description: str = "" + figma_file_key: str = "" + +class ProjectUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + figma_file_key: Optional[str] = None + status: Optional[str] = None + +class FigmaExtractRequest(BaseModel): + file_key: str + format: str = "css" + +class FigmaSyncRequest(BaseModel): + file_key: str + target_path: str + format: str = "css" + +class TeamCreate(BaseModel): + name: str + description: str = "" + + +# === Root & Health === + +@app.get("/") +async def root(): + """Redirect to Admin UI dashboard.""" + from fastapi.responses import RedirectResponse + return RedirectResponse(url="/admin-ui/index.html") + + +@app.get("/health") +async def health(): + """Health check endpoint.""" + return { + "status": "ok", + "name": "dss-api", + "version": "1.0.0", + "timestamp": datetime.utcnow().isoformat() + "Z", + "figma_mode": figma_suite.mode, + "config": config.summary() + } + +@app.get("/api/stats") +async def get_statistics(): + """Get database and system statistics.""" + db_stats = get_stats() + return { + "database": db_stats, + "figma": { + "mode": figma_suite.mode, + "configured": config.figma.is_configured + } + } + + +# === Projects === + +@app.get("/api/projects") +async def list_projects(status: Optional[str] = None): + """List all projects.""" + projects = Projects.list(status=status) + return projects + +@app.get("/api/projects/{project_id}") +async def get_project(project_id: str): + """Get a specific project.""" + project = Projects.get(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + return project + +@app.post("/api/projects") +async def create_project(project: ProjectCreate): + """Create a new project.""" + project_id = f"proj-{int(datetime.utcnow().timestamp() * 1000)}" + created = Projects.create( + id=project_id, + name=project.name, + description=project.description, + figma_file_key=project.figma_file_key + ) + ActivityLog.log( + action="project_created", + entity_type="project", + entity_id=project_id, + project_id=project_id, + details={"name": project.name} + ) + return created + +@app.put("/api/projects/{project_id}") +async def update_project(project_id: str, update: ProjectUpdate): + """Update a project.""" + existing = Projects.get(project_id) + if not existing: + raise HTTPException(status_code=404, detail="Project not found") + + update_data = {k: v for k, v in update.dict().items() if v is not None} + if not update_data: + return existing + + updated = Projects.update(project_id, **update_data) + ActivityLog.log( + action="project_updated", + entity_type="project", + entity_id=project_id, + project_id=project_id, + details=update_data + ) + return updated + +@app.delete("/api/projects/{project_id}") +async def delete_project(project_id: str): + """Delete a project.""" + if not Projects.delete(project_id): + raise HTTPException(status_code=404, detail="Project not found") + ActivityLog.log( + action="project_deleted", + entity_type="project", + entity_id=project_id + ) + return {"success": True} + + +# === Components === + +@app.get("/api/projects/{project_id}/components") +async def list_components(project_id: str): + """List components for a project.""" + if not Projects.get(project_id): + raise HTTPException(status_code=404, detail="Project not found") + return Components.list(project_id) + + +# === Figma Integration === + +@app.post("/api/figma/extract-variables") +async def extract_variables(request: FigmaExtractRequest, background_tasks: BackgroundTasks): + """Extract design tokens from Figma file.""" + try: + result = await figma_suite.extract_variables(request.file_key, request.format) + ActivityLog.log( + action="figma_extract_variables", + entity_type="figma", + details={"file_key": request.file_key, "format": request.format, "count": result.get("tokens_count")} + ) + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/api/figma/extract-components") +async def extract_components(request: FigmaExtractRequest): + """Extract components from Figma file.""" + try: + result = await figma_suite.extract_components(request.file_key) + ActivityLog.log( + action="figma_extract_components", + entity_type="figma", + details={"file_key": request.file_key, "count": result.get("components_count")} + ) + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/api/figma/extract-styles") +async def extract_styles(request: FigmaExtractRequest): + """Extract styles from Figma file.""" + try: + result = await figma_suite.extract_styles(request.file_key) + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/api/figma/sync-tokens") +async def sync_tokens(request: FigmaSyncRequest): + """Sync tokens from Figma to target path.""" + try: + result = await figma_suite.sync_tokens(request.file_key, request.target_path, request.format) + ActivityLog.log( + action="figma_sync_tokens", + entity_type="figma", + details={"file_key": request.file_key, "target": request.target_path, "synced": result.get("tokens_synced")} + ) + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/api/figma/validate") +async def validate_components(request: FigmaExtractRequest): + """Validate components against design system rules.""" + try: + result = await figma_suite.validate_components(request.file_key) + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/api/figma/generate-code") +async def generate_code(file_key: str, component_name: str, framework: str = "webcomponent"): + """Generate component code from Figma.""" + try: + result = await figma_suite.generate_code(file_key, component_name, framework) + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# === Discovery === + +@app.get("/api/discovery") +async def run_discovery(path: str = "."): + """Run project discovery.""" + script_path = Path(__file__).parent.parent / "discovery" / "discover.sh" + + try: + result = subprocess.run( + [str(script_path), path], + capture_output=True, + text=True, + timeout=30 + ) + if result.returncode == 0: + return json.loads(result.stdout) + else: + return {"error": result.stderr} + except subprocess.TimeoutExpired: + raise HTTPException(status_code=504, detail="Discovery timed out") + except json.JSONDecodeError: + return {"raw_output": result.stdout} + +@app.get("/api/discovery/ports") +async def discover_ports(): + """Discover listening ports and services.""" + script_path = Path(__file__).parent.parent / "discovery" / "discover-ports.sh" + + try: + result = subprocess.run( + [str(script_path)], + capture_output=True, + text=True, + timeout=10 + ) + return json.loads(result.stdout) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/api/discovery/env") +async def discover_env(path: str = "."): + """Analyze environment configuration.""" + script_path = Path(__file__).parent.parent / "discovery" / "discover-env.sh" + + try: + result = subprocess.run( + [str(script_path), path], + capture_output=True, + text=True, + timeout=10 + ) + return json.loads(result.stdout) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# === Activity & Sync History === + +@app.get("/api/activity") +async def get_activity(limit: int = Query(default=50, le=100)): + """Get recent activity log.""" + return ActivityLog.recent(limit=limit) + +@app.get("/api/sync-history") +async def get_sync_history(project_id: Optional[str] = None, limit: int = Query(default=20, le=100)): + """Get sync history.""" + return SyncHistory.recent(project_id=project_id, limit=limit) + + +# === Teams === + +@app.get("/api/teams") +async def list_teams(): + """List all teams.""" + return Teams.list() + +@app.post("/api/teams") +async def create_team(team: TeamCreate): + """Create a new team.""" + team_id = f"team-{int(datetime.utcnow().timestamp() * 1000)}" + created = Teams.create(team_id, team.name, team.description) + return created + +@app.get("/api/teams/{team_id}") +async def get_team(team_id: str): + """Get a specific team.""" + team = Teams.get(team_id) + if not team: + raise HTTPException(status_code=404, detail="Team not found") + return team + + +# === Cache Management === + +@app.post("/api/cache/clear") +async def clear_cache(): + """Clear expired cache entries.""" + count = Cache.clear_expired() + return {"cleared": count} + +@app.delete("/api/cache") +async def purge_cache(): + """Purge all cache entries.""" + Cache.clear_all() + return {"success": True} + + +# === Configuration Management === + +class ConfigUpdate(BaseModel): + mode: Optional[str] = None + figma_token: Optional[str] = None + services: Optional[Dict[str, Any]] = None + features: Optional[Dict[str, bool]] = None + + +@app.get("/api/config") +async def get_config(): + """Get current runtime configuration (secrets masked).""" + return { + "config": runtime_config.get(), + "env": config.summary(), + "mode": runtime_config.get("mode") + } + + +@app.put("/api/config") +async def update_config(update: ConfigUpdate): + """Update runtime configuration.""" + updates = {} + + if update.mode: + updates["mode"] = update.mode + + if update.figma_token is not None: + runtime_config.set_figma_token(update.figma_token) + # Reinitialize Figma tools with new token + global figma_suite + figma_suite = FigmaToolSuite(output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output")) + ActivityLog.log( + action="figma_token_updated", + entity_type="config", + details={"configured": bool(update.figma_token)} + ) + + if update.services: + updates["services"] = update.services + + if update.features: + updates["features"] = update.features + + if updates: + runtime_config.update(updates) + ActivityLog.log( + action="config_updated", + entity_type="config", + details={"keys": list(updates.keys())} + ) + + return runtime_config.get() + + +@app.get("/api/config/figma") +async def get_figma_config(): + """Get Figma configuration status.""" + figma_cfg = runtime_config.get("figma") + return { + "configured": figma_cfg.get("configured", False), + "mode": figma_suite.mode, + "features": { + "extract_variables": True, + "extract_components": True, + "extract_styles": True, + "sync_tokens": True, + "validate": True, + "generate_code": True, + } + } + + +@app.post("/api/config/figma/test") +async def test_figma_connection(): + """Test Figma API connection.""" + try: + # Try to make a simple API call + if not runtime_config.get("figma").get("configured"): + return {"success": False, "error": "Figma token not configured"} + + # Test with a minimal API call + import httpx + token = runtime_config._data["figma"]["token"] + async with httpx.AsyncClient() as client: + resp = await client.get( + "https://api.figma.com/v1/me", + headers={"X-Figma-Token": token} + ) + if resp.status_code == 200: + user = resp.json() + return { + "success": True, + "user": user.get("email", "connected"), + "handle": user.get("handle") + } + else: + return {"success": False, "error": f"API returned {resp.status_code}"} + except Exception as e: + return {"success": False, "error": str(e)} + + +# === Service Discovery === + +@app.get("/api/services") +async def list_services(): + """List configured and discovered services.""" + configured = runtime_config.get("services") + discovered = await ServiceDiscovery.discover() + + return { + "configured": configured, + "discovered": discovered, + "storybook": await ServiceDiscovery.check_storybook() + } + + +@app.put("/api/services/{service_name}") +async def configure_service(service_name: str, config_data: Dict[str, Any]): + """Configure a service.""" + services = runtime_config.get("services") or {} + services[service_name] = {**services.get(service_name, {}), **config_data} + runtime_config.set("services", services) + + ActivityLog.log( + action="service_configured", + entity_type="service", + entity_id=service_name, + details={"keys": list(config_data.keys())} + ) + + return services[service_name] + + +@app.get("/api/services/storybook") +async def get_storybook_status(): + """Get Storybook service status.""" + return await ServiceDiscovery.check_storybook() + + +# === DSS Mode === + +@app.get("/api/mode") +async def get_mode(): + """Get current DSS mode.""" + mode = runtime_config.get("mode") + return { + "mode": mode, + "description": "Local dev companion" if mode == "local" else "Remote design system server", + "features": runtime_config.get("features") + } + + +@app.put("/api/mode") +async def set_mode(mode: str): + """Set DSS mode (local or server).""" + if mode not in ["local", "server"]: + raise HTTPException(status_code=400, detail="Mode must be 'local' or 'server'") + + runtime_config.set("mode", mode) + ActivityLog.log( + action="mode_changed", + entity_type="config", + details={"mode": mode} + ) + + return {"mode": mode, "success": True} + + +# === Run Server === + +# === Static Files (Admin UI) === +# Mount at the end so API routes take precedence +# This enables portable mode: ./dss start serves everything on one port + +UI_DIR = Path(__file__).parent.parent.parent / "admin-ui" +if UI_DIR.exists(): + app.mount("/", StaticFiles(directory=str(UI_DIR), html=True), name="ui") + + +if __name__ == "__main__": + import uvicorn + + port = int(os.getenv("PORT", "3456")) + host = os.getenv("HOST", "0.0.0.0") + + url = f"http://{host}:{port}" + print(f""" +╔═══════════════════════════════════════════════════════════════╗ +║ Design System Server (DSS) - Portable Server ║ +╠═══════════════════════════════════════════════════════════════╣ +║ Dashboard: {url + '/':^47}║ +║ API: {url + '/api':^47}║ +║ Docs: {url + '/docs':^47}║ +║ Environment: {config.server.env:^47}║ +║ Figma Mode: {figma_suite.mode:^47}║ +╚═══════════════════════════════════════════════════════════════╝ +""") + + uvicorn.run( + "server:app", + host=host, + port=port, + reload=config.server.env == "development" + ) diff --git a/apps/cli/scripts/postinstall.js b/apps/cli/scripts/postinstall.js new file mode 100644 index 0000000..8664e7b --- /dev/null +++ b/apps/cli/scripts/postinstall.js @@ -0,0 +1,74 @@ +#!/usr/bin/env node +/** + * DSS Postinstall Script + * + * Sets up Python virtual environment and installs dependencies + * after npm install. + */ + +import { spawn, execSync } from 'child_process'; +import { existsSync, mkdirSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const pythonDir = join(__dirname, '..', 'python'); +const venvDir = join(pythonDir, 'venv'); +const apiDir = join(pythonDir, 'api'); + +console.log('[DSS] Setting up Python environment...'); + +// Check if Python 3 is available +function getPythonCmd() { + const commands = ['python3', 'python']; + for (const cmd of commands) { + try { + execSync(`${cmd} --version`, { stdio: 'ignore' }); + return cmd; + } catch { + continue; + } + } + return null; +} + +const python = getPythonCmd(); + +if (!python) { + console.error('[DSS] Error: Python 3 is required but not found.'); + console.error('[DSS] Please install Python 3.8+ and try again.'); + process.exit(1); +} + +console.log(`[DSS] Using ${python}`); + +// Create virtual environment if needed +if (!existsSync(venvDir)) { + console.log('[DSS] Creating virtual environment...'); + try { + execSync(`${python} -m venv ${venvDir}`, { stdio: 'inherit' }); + } catch (error) { + console.error('[DSS] Failed to create virtual environment'); + console.error('[DSS] Try: pip install virtualenv'); + process.exit(1); + } +} + +// Install requirements +const pip = join(venvDir, 'bin', 'pip'); +const requirements = join(apiDir, 'requirements.txt'); + +if (existsSync(requirements)) { + console.log('[DSS] Installing Python dependencies...'); + try { + execSync(`${pip} install -q -r ${requirements}`, { stdio: 'inherit' }); + console.log('[DSS] Python environment ready!'); + } catch (error) { + console.error('[DSS] Failed to install Python dependencies'); + process.exit(1); + } +} else { + console.log('[DSS] No requirements.txt found, skipping dependency install'); +} + +console.log('[DSS] Setup complete!'); diff --git a/apps/cli/scripts/publish.sh b/apps/cli/scripts/publish.sh new file mode 100755 index 0000000..5393fbd --- /dev/null +++ b/apps/cli/scripts/publish.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# DSS npm publish script + +set -e + +echo "🚀 Publishing @overbits/dss to npm..." + +# Ensure we're in the cli directory +cd "$(dirname "$0")/.." + +# Build +echo "📦 Building..." +npm run build + +# Check if logged in to npm +if ! npm whoami &> /dev/null; then + echo "❌ Not logged in to npm. Run: npm login" + exit 1 +fi + +# Verify package +echo "📋 Package contents:" +npm pack --dry-run + +# Confirm +read -p "Publish to npm? (y/N) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Cancelled." + exit 0 +fi + +# Publish +npm publish --access public + +echo "✅ Published @overbits/dss!" +echo "" +echo "Users can now install with:" +echo " npm install -g @overbits/dss" +echo " npx @overbits/dss" diff --git a/apps/cli/src/cli.ts b/apps/cli/src/cli.ts new file mode 100644 index 0000000..484f072 --- /dev/null +++ b/apps/cli/src/cli.ts @@ -0,0 +1,103 @@ +#!/usr/bin/env node +/** + * 🧬 DSS CLI - Design System Server Organism Controller + * + * A portable companion for UI developers - think of it as the organism's + * command-line nervous system. Through these commands, you can: + * + * - 🧬 Awaken a new organism (init) + * - 💚 Check the organism's vital signs (status) + * - 🩸 Direct the sensory organs to perceive Figma (extract) + * - 🔄 Circulate extracted nutrients (sync) + * - ⚙️ Adjust the organism's behavior (config) + * - 🧠 Birth a conscious instance (start) + * + * Framework: DSS Organism Framework + */ + +import { Command } from 'commander'; +import chalk from 'chalk'; +import { initCommand } from './commands/init.js'; +import { startCommand } from './commands/start.js'; +import { syncCommand } from './commands/sync.js'; +import { extractCommand } from './commands/extract.js'; +import { configCommand } from './commands/config.js'; +import { statusCommand } from './commands/status.js'; +import { stopCommand } from './commands/stop.js'; + +const program = new Command(); + +program + .name('dss') + .description('🧬 Design System Server - Organism Controller for UI Developers') + .version('0.1.0'); + +// Init command - setup DSS in a project +program + .command('init') + .description('🧬 ORGANISM GENESIS - Create a new design system organism in your project') + .option('-f, --figma-key ', 'Link to Figma genetic blueprint') + .option('-t, --figma-token ', 'Figma sensory organ connection token') + .action(initCommand); + +// Start command - start the DSS server +program + .command('start') + .description('💚 ORGANISM AWAKENING - Bring the design system organism to life') + .option('-p, --port ', 'Neural pathway port', '3456') + .option('-d, --dev', 'Live consciousness mode with hot-reload') + .option('--no-open', 'Do not open sensory interface') + .action(startCommand); + +// Sync command - sync tokens from Figma +program + .command('sync') + .description('🩸 NUTRIENT CIRCULATION - Distribute extracted tokens through the codebase') + .option('-f, --format ', 'Nutrient format: css, scss, json, ts', 'css') + .option('-o, --output ', 'Circulation destination') + .option('--file-key ', 'Figma file key (overrides config)') + .action(syncCommand); + +// Extract command - extract components or tokens +program + .command('extract ') + .description('👁️ SENSORY PERCEPTION - Direct organism eyes to perceive Figma designs') + .option('-f, --format ', 'Perception output format', 'json') + .option('-o, --output ', 'Memory storage location') + .option('--file-key ', 'Figma file key') + .action(extractCommand); + +// Config command - manage configuration +program + .command('config') + .description('⚙️ ENDOCRINE ADJUSTMENT - Configure organism behavior and preferences') + .option('--set ', 'Set organism hormone value') + .option('--get ', 'Read organism hormone value') + .option('--list', 'View all hormones') + .action(configCommand); + +// Stop command - stop the server +program + .command('stop') + .description('😴 ORGANISM REST - Put the design system organism into sleep mode') + .action(stopCommand); + +// Status command - check DSS status +program + .command('status') + .description('🏥 VITAL SIGNS CHECK - Monitor organism health and configuration') + .action(statusCommand); + +// Parse arguments +program.parse(); + +// Show help if no command provided +if (!process.argv.slice(2).length) { + console.log(chalk.blue(` + ╔═══════════════════════════════════════════════════════════════╗ + ║ ${chalk.bold('DSS')} - Design System Server ║ + ║ UI Developer Companion ║ + ╚═══════════════════════════════════════════════════════════════╝ + `)); + program.outputHelp(); +} diff --git a/apps/cli/src/commands/config.ts b/apps/cli/src/commands/config.ts new file mode 100644 index 0000000..b4f2fcf --- /dev/null +++ b/apps/cli/src/commands/config.ts @@ -0,0 +1,138 @@ +/** + * DSS Config Command + * + * Manage DSS configuration. + */ + +import chalk from 'chalk'; +import { + getConfig, + loadProjectConfig, + saveProjectConfig, + setGlobalConfig, + getGlobalConfig, + listGlobalConfig, + hasProjectConfig, +} from '../lib/config.js'; + +interface ConfigOptions { + set?: string; + get?: string; + list?: boolean; +} + +export async function configCommand(options: ConfigOptions): Promise { + if (options.set) { + await setConfig(options.set); + } else if (options.get) { + await getConfigValue(options.get); + } else if (options.list) { + await listConfig(); + } else { + await listConfig(); + } +} + +async function setConfig(keyValue: string): Promise { + const [key, ...valueParts] = keyValue.split('='); + const value = valueParts.join('='); + + if (!key || value === undefined) { + console.log(chalk.red(' Invalid format. Use: --set key=value')); + process.exit(1); + } + + // Determine if this is a global or project config + const globalKeys = ['figmaToken', 'defaultPort', 'defaultFormat']; + const projectKeys = ['figmaFileKey', 'port', 'outputDir', 'tokenFormat', 'componentFramework']; + + if (globalKeys.includes(key)) { + // Global config + const parsedValue = parseValue(value); + setGlobalConfig(key, parsedValue as string | number); + console.log(chalk.green(` Set global config: ${key}`)); + console.log(chalk.dim(` Value: ${key === 'figmaToken' ? '***hidden***' : value}`)); + } else if (projectKeys.includes(key)) { + // Project config + if (!hasProjectConfig()) { + console.log(chalk.yellow(' No project config found. Run: dss init')); + process.exit(1); + } + + const config = loadProjectConfig(); + (config as Record)[key] = parseValue(value); + saveProjectConfig(config); + + console.log(chalk.green(` Set project config: ${key}`)); + console.log(chalk.dim(` Value: ${value}`)); + } else { + console.log(chalk.yellow(` Unknown config key: ${key}`)); + console.log(''); + console.log(chalk.dim(' Global keys: figmaToken, defaultPort, defaultFormat')); + console.log(chalk.dim(' Project keys: figmaFileKey, port, outputDir, tokenFormat, componentFramework')); + } +} + +async function getConfigValue(key: string): Promise { + const config = getConfig(); + const value = (config as Record)[key]; + + if (value === undefined) { + // Try global config + const globalValue = getGlobalConfig(key); + if (globalValue !== undefined) { + console.log(chalk.dim(` ${key} (global):`), key === 'figmaToken' ? '***hidden***' : String(globalValue)); + } else { + console.log(chalk.yellow(` Config key not found: ${key}`)); + } + } else { + console.log(chalk.dim(` ${key}:`), key === 'figmaToken' ? '***hidden***' : String(value)); + } +} + +async function listConfig(): Promise { + console.log(''); + console.log(chalk.blue(' DSS Configuration')); + console.log(chalk.dim(' ─────────────────')); + console.log(''); + + // Project config + if (hasProjectConfig()) { + console.log(chalk.green(' Project Config:')); + const projectConfig = loadProjectConfig(); + Object.entries(projectConfig).forEach(([key, value]) => { + console.log(chalk.dim(` ${key}:`), String(value)); + }); + } else { + console.log(chalk.yellow(' No project config (run: dss init)')); + } + + console.log(''); + + // Global config + console.log(chalk.green(' Global Config:')); + const globalConf = listGlobalConfig(); + Object.entries(globalConf).forEach(([key, value]) => { + const displayValue = key === 'figmaToken' ? '***hidden***' : String(value); + console.log(chalk.dim(` ${key}:`), displayValue); + }); + + console.log(''); + + // Merged config + console.log(chalk.green(' Effective Config:')); + const merged = getConfig(); + Object.entries(merged).forEach(([key, value]) => { + const displayValue = key === 'figmaToken' ? (value ? '***configured***' : 'not set') : String(value); + console.log(chalk.dim(` ${key}:`), displayValue); + }); + + console.log(''); +} + +function parseValue(value: string): string | number | boolean { + if (value === 'true') return true; + if (value === 'false') return false; + if (!isNaN(Number(value))) return Number(value); + return value; +} diff --git a/apps/cli/src/commands/extract.ts b/apps/cli/src/commands/extract.ts new file mode 100644 index 0000000..8fb0022 --- /dev/null +++ b/apps/cli/src/commands/extract.ts @@ -0,0 +1,146 @@ +/** + * DSS Extract Command + * + * Extract tokens or components from Figma. + */ + +import chalk from 'chalk'; +import ora from 'ora'; +import { writeFileSync, existsSync, mkdirSync } from 'fs'; +import { join } from 'path'; +import { getConfig, getProjectRoot } from '../lib/config.js'; +import { getApiClient } from '../lib/api.js'; +import { isServerRunning } from '../lib/server.js'; + +interface ExtractOptions { + format: string; + output?: string; + fileKey?: string; +} + +export async function extractCommand( + type: string, + options: ExtractOptions +): Promise { + const validTypes = ['tokens', 'components', 'styles', 'all']; + if (!validTypes.includes(type)) { + console.log(chalk.red(` Invalid type: ${type}`)); + console.log(chalk.dim(` Valid types: ${validTypes.join(', ')}`)); + process.exit(1); + } + + const config = getConfig(); + const cwd = getProjectRoot(); + + // Check if server is running + if (!isServerRunning(cwd)) { + console.log(chalk.yellow(' DSS server is not running')); + console.log(chalk.dim(' Start it with: dss start')); + process.exit(1); + } + + const fileKey = options.fileKey || config.figmaFileKey; + if (!fileKey) { + console.log(chalk.red(' No Figma file key configured')); + console.log(chalk.dim(' Set it with: dss config --set figmaFileKey=YOUR_KEY')); + process.exit(1); + } + + const outputDir = options.output || join(cwd, '.dss', 'output'); + if (!existsSync(outputDir)) { + mkdirSync(outputDir, { recursive: true }); + } + + const api = getApiClient({ port: config.port }); + + if (type === 'tokens' || type === 'all') { + await extractTokens(api, fileKey, options.format, outputDir); + } + + if (type === 'components' || type === 'all') { + await extractComponents(api, fileKey, outputDir); + } + + if (type === 'styles' || type === 'all') { + await extractStyles(api, fileKey, outputDir); + } +} + +async function extractTokens( + api: ReturnType, + fileKey: string, + format: string, + outputDir: string +): Promise { + const spinner = ora('Extracting tokens...').start(); + + try { + const result = await api.extractTokens(fileKey, format || 'json'); + + const filename = format === 'css' ? 'tokens.css' : + format === 'scss' ? '_tokens.scss' : + format === 'ts' ? 'tokens.ts' : 'tokens.json'; + + const outputPath = join(outputDir, filename); + writeFileSync(outputPath, result.formatted_output); + + spinner.succeed(`Extracted ${result.tokens_count} tokens`); + console.log(chalk.dim(` Output: ${outputPath}`)); + } catch (error) { + spinner.fail('Failed to extract tokens'); + console.error(chalk.red(` ${(error as Error).message}`)); + } +} + +async function extractComponents( + api: ReturnType, + fileKey: string, + outputDir: string +): Promise { + const spinner = ora('Extracting components...').start(); + + try { + const result = await api.extractComponents(fileKey); + + const outputPath = join(outputDir, 'components.json'); + writeFileSync(outputPath, JSON.stringify(result.components, null, 2)); + + spinner.succeed(`Extracted ${result.components_count} components`); + console.log(chalk.dim(` Output: ${outputPath}`)); + + // Show component summary + console.log(''); + result.components.forEach(comp => { + console.log(chalk.dim(` - ${comp.name}`)); + if (comp.variants?.length) { + console.log(chalk.dim(` Variants: ${comp.variants.join(', ')}`)); + } + }); + } catch (error) { + spinner.fail('Failed to extract components'); + console.error(chalk.red(` ${(error as Error).message}`)); + } +} + +async function extractStyles( + api: ReturnType, + fileKey: string, + outputDir: string +): Promise { + const spinner = ora('Extracting styles...').start(); + + try { + // Note: This would need a corresponding API endpoint + // For now, we'll extract tokens which include style information + const result = await api.extractTokens(fileKey, 'json'); + + const outputPath = join(outputDir, 'styles.json'); + writeFileSync(outputPath, JSON.stringify(result.tokens, null, 2)); + + spinner.succeed(`Extracted styles`); + console.log(chalk.dim(` Output: ${outputPath}`)); + } catch (error) { + spinner.fail('Failed to extract styles'); + console.error(chalk.red(` ${(error as Error).message}`)); + } +} diff --git a/apps/cli/src/commands/init.ts b/apps/cli/src/commands/init.ts new file mode 100644 index 0000000..97aed09 --- /dev/null +++ b/apps/cli/src/commands/init.ts @@ -0,0 +1,107 @@ +/** + * DSS Init Command + * + * Initialize DSS in the current project. + */ + +import chalk from 'chalk'; +import ora from 'ora'; +import { existsSync, mkdirSync } from 'fs'; +import { join } from 'path'; +import { + getProjectRoot, + hasProjectConfig, + saveProjectConfig, + setGlobalConfig, + type DSSConfig, +} from '../lib/config.js'; + +interface InitOptions { + figmaKey?: string; + figmaToken?: string; +} + +export async function initCommand(options: InitOptions): Promise { + const spinner = ora('Initializing DSS...').start(); + + try { + const projectRoot = getProjectRoot(); + const dssDir = join(projectRoot, '.dss'); + + // Check if already initialized + if (hasProjectConfig()) { + spinner.warn('DSS is already initialized in this project'); + console.log(chalk.dim(` Config: ${join(dssDir, 'config.json')}`)); + return; + } + + // Create .dss directory + if (!existsSync(dssDir)) { + mkdirSync(dssDir, { recursive: true }); + } + + // Create output directory + const outputDir = join(dssDir, 'output'); + if (!existsSync(outputDir)) { + mkdirSync(outputDir, { recursive: true }); + } + + // Build config + const config: DSSConfig = { + port: 3456, + outputDir: '.dss/output', + tokenFormat: 'css', + componentFramework: 'react', + }; + + if (options.figmaKey) { + config.figmaFileKey = options.figmaKey; + } + + // Save Figma token globally (not in project config for security) + if (options.figmaToken) { + setGlobalConfig('figmaToken', options.figmaToken); + spinner.info('Figma token saved to global config'); + } + + // Save project config + saveProjectConfig(config); + + spinner.succeed('DSS initialized successfully!'); + + console.log(''); + console.log(chalk.green(' Created:')); + console.log(chalk.dim(` .dss/config.json`)); + console.log(chalk.dim(` .dss/output/`)); + console.log(''); + + // Next steps + console.log(chalk.blue(' Next steps:')); + if (!options.figmaToken) { + console.log(chalk.dim(' 1. Set your Figma token:')); + console.log(chalk.white(' dss config --set figmaToken=figd_xxxxx')); + } + if (!options.figmaKey) { + console.log(chalk.dim(` ${options.figmaToken ? '1' : '2'}. Set your Figma file key:`)); + console.log(chalk.white(' dss config --set figmaFileKey=abc123')); + } + console.log(chalk.dim(` ${options.figmaToken && options.figmaKey ? '1' : '3'}. Start the server:`)); + console.log(chalk.white(' dss start')); + console.log(''); + + // Add to .gitignore if exists + const gitignorePath = join(projectRoot, '.gitignore'); + if (existsSync(gitignorePath)) { + const { readFileSync, appendFileSync } = await import('fs'); + const gitignore = readFileSync(gitignorePath, 'utf-8'); + if (!gitignore.includes('.dss/')) { + appendFileSync(gitignorePath, '\n# DSS\n.dss/\n'); + console.log(chalk.dim(' Added .dss/ to .gitignore')); + } + } + } catch (error) { + spinner.fail('Failed to initialize DSS'); + console.error(chalk.red(` ${(error as Error).message}`)); + process.exit(1); + } +} diff --git a/apps/cli/src/commands/start.ts b/apps/cli/src/commands/start.ts new file mode 100644 index 0000000..7b122e7 --- /dev/null +++ b/apps/cli/src/commands/start.ts @@ -0,0 +1,109 @@ +/** + * DSS Start Command + * + * Start the DSS server. + */ + +import chalk from 'chalk'; +import ora from 'ora'; +import { exec } from 'child_process'; +import { getConfig, getProjectRoot, hasProjectConfig } from '../lib/config.js'; +import { + startServer, + isServerRunning, + getServerPid, + waitForServer, + stopServer, +} from '../lib/server.js'; + +interface StartOptions { + port: string; + dev: boolean; + open: boolean; +} + +export async function startCommand(options: StartOptions): Promise { + const port = parseInt(options.port, 10); + const cwd = getProjectRoot(); + const config = getConfig(); + + // Check if already running + if (isServerRunning(cwd)) { + const pid = getServerPid(cwd); + console.log(chalk.yellow(` DSS is already running (PID: ${pid})`)); + console.log(chalk.dim(` Dashboard: http://localhost:${port}`)); + console.log(''); + console.log(chalk.dim(' To restart: dss stop && dss start')); + return; + } + + const spinner = ora('Starting DSS server...').start(); + + try { + // Start server + const serverProcess = await startServer({ + port, + dev: options.dev, + cwd, + }); + + if (options.dev) { + spinner.succeed('DSS running in development mode'); + console.log(chalk.dim(' Press Ctrl+C to stop')); + console.log(''); + + // In dev mode, we're attached to the process + serverProcess.on('exit', (code) => { + console.log(chalk.dim(`\n Server exited with code ${code}`)); + process.exit(code || 0); + }); + + // Handle Ctrl+C + process.on('SIGINT', () => { + console.log(chalk.dim('\n Stopping server...')); + serverProcess.kill('SIGTERM'); + }); + } else { + // Wait for server to be ready + spinner.text = 'Waiting for server to be ready...'; + const ready = await waitForServer(port, 15000); + + if (!ready) { + spinner.fail('Server failed to start'); + console.error(chalk.red(' Check logs: .dss/dss.log')); + await stopServer(cwd); + process.exit(1); + } + + spinner.succeed(`DSS started (PID: ${serverProcess.pid})`); + console.log(''); + console.log(chalk.green(' Dashboard:'), chalk.blue(`http://localhost:${port}`)); + console.log(chalk.green(' API: '), chalk.blue(`http://localhost:${port}/api`)); + console.log(chalk.green(' Docs: '), chalk.blue(`http://localhost:${port}/docs`)); + console.log(''); + console.log(chalk.dim(' Logs: .dss/dss.log')); + console.log(chalk.dim(' Stop: dss stop')); + console.log(''); + + // Show Figma status + if (config.figmaFileKey) { + console.log(chalk.dim(` Figma file: ${config.figmaFileKey}`)); + } else { + console.log(chalk.yellow(' No Figma file configured')); + console.log(chalk.dim(' Run: dss config --set figmaFileKey=YOUR_KEY')); + } + + // Open browser if requested + if (options.open) { + const url = `http://localhost:${port}`; + const openCmd = process.platform === 'darwin' ? 'open' : + process.platform === 'win32' ? 'start' : 'xdg-open'; + exec(`${openCmd} ${url}`); + } + } + } catch (error) { + spinner.fail('Failed to start DSS'); + console.error(chalk.red(` ${(error as Error).message}`)); + process.exit(1); + } +} diff --git a/apps/cli/src/commands/status.ts b/apps/cli/src/commands/status.ts new file mode 100644 index 0000000..ccce038 --- /dev/null +++ b/apps/cli/src/commands/status.ts @@ -0,0 +1,96 @@ +/** + * 🏥 DSS Status Command - Organism Vital Signs + * + * Check DSS design system organism's vital signs, consciousness state, + * and sensory organ configuration. + */ + +import chalk from 'chalk'; +import { getConfig, getProjectRoot, hasProjectConfig } from '../lib/config.js'; +import { isServerRunning, getServerPid } from '../lib/server.js'; +import { getApiClient } from '../lib/api.js'; + +export async function statusCommand(): Promise { + const cwd = getProjectRoot(); + const config = getConfig(); + + console.log(''); + console.log(chalk.cyan(' 🏥 ORGANISM VITAL SIGNS')); + console.log(chalk.dim(' ────────────────────────')); + console.log(''); + + // Organism status + if (hasProjectConfig()) { + console.log(chalk.green(' 🧬 Organism:'), chalk.dim('Born and conscious')); + console.log(chalk.dim(` Home: ${cwd}`)); + } else { + console.log(chalk.yellow(' 🧬 Organism:'), chalk.dim('Not yet born')); + console.log(chalk.dim(' Genesis: dss init')); + } + + console.log(''); + + // Consciousness status (server) + const running = isServerRunning(cwd); + const pid = getServerPid(cwd); + const port = config.port || 3456; + + if (running) { + console.log(chalk.green(' 💚 Consciousness:'), chalk.dim(`Awake (PID: ${pid})`)); + console.log(chalk.dim(` Neural port: http://localhost:${port}`)); + + // Try to get health info + try { + const api = getApiClient({ port }); + const health = await api.health(); + console.log(chalk.dim(` Awareness: ${health.figma_mode}`)); + } catch { + console.log(chalk.yellow(' ⚠️ Unable to read consciousness')); + } + } else { + console.log(chalk.yellow(' 💚 Consciousness:'), chalk.dim('Sleeping')); + console.log(chalk.dim(' Awaken: dss start')); + } + + console.log(''); + + // Sensory organs (Figma) + if (config.figmaToken) { + console.log(chalk.green(' 👁️ Sensory Eyes:'), chalk.dim('Configured')); + + // Test connection if server is running + if (running) { + try { + const api = getApiClient({ port }); + const test = await api.testFigmaConnection(); + if (test.success) { + console.log(chalk.green(' Perception:'), chalk.dim(`Clear (${test.user})`)); + } else { + console.log(chalk.red(' Perception:'), chalk.dim(test.error || 'Blinded')); + } + } catch { + console.log(chalk.yellow(' Perception:'), chalk.dim('Cannot test')); + } + } + } else { + console.log(chalk.yellow(' 👁️ Sensory Eyes:'), chalk.dim('Not configured')); + console.log(chalk.dim(' Configure: dss config --set figmaToken=figd_xxxxx')); + } + + if (config.figmaFileKey) { + console.log(chalk.green(' 📋 Genetic Blueprint:'), chalk.dim(config.figmaFileKey)); + } else { + console.log(chalk.yellow(' Figma File:'), chalk.dim('Not configured')); + console.log(chalk.dim(' Set: dss config --set figmaFileKey=abc123')); + } + + console.log(''); + + // Output config + console.log(chalk.dim(' Output:')); + console.log(chalk.dim(` Format: ${config.tokenFormat || 'css'}`)); + console.log(chalk.dim(` Framework: ${config.componentFramework || 'react'}`)); + console.log(chalk.dim(` Directory: ${config.outputDir || '.dss/output'}`)); + + console.log(''); +} diff --git a/apps/cli/src/commands/stop.ts b/apps/cli/src/commands/stop.ts new file mode 100644 index 0000000..b171dbf --- /dev/null +++ b/apps/cli/src/commands/stop.ts @@ -0,0 +1,25 @@ +/** + * DSS Stop Command + * + * Stop the DSS server. + */ + +import chalk from 'chalk'; +import { getProjectRoot } from '../lib/config.js'; +import { isServerRunning, stopServer, getServerPid } from '../lib/server.js'; + +export async function stopCommand(): Promise { + const cwd = getProjectRoot(); + + if (!isServerRunning(cwd)) { + console.log(chalk.yellow(' DSS is not running')); + return; + } + + const pid = getServerPid(cwd); + console.log(chalk.dim(` Stopping DSS (PID: ${pid})...`)); + + await stopServer(cwd); + + console.log(chalk.green(' DSS stopped')); +} diff --git a/apps/cli/src/commands/sync.ts b/apps/cli/src/commands/sync.ts new file mode 100644 index 0000000..4e65a73 --- /dev/null +++ b/apps/cli/src/commands/sync.ts @@ -0,0 +1,102 @@ +/** + * DSS Sync Command + * + * Sync design tokens from Figma to local files. + */ + +import chalk from 'chalk'; +import ora from 'ora'; +import { writeFileSync, existsSync, mkdirSync } from 'fs'; +import { dirname, join } from 'path'; +import { getConfig, getProjectRoot } from '../lib/config.js'; +import { getApiClient } from '../lib/api.js'; +import { isServerRunning } from '../lib/server.js'; + +interface SyncOptions { + format: string; + output?: string; + fileKey?: string; +} + +export async function syncCommand(options: SyncOptions): Promise { + const config = getConfig(); + const cwd = getProjectRoot(); + + // Check if server is running + if (!isServerRunning(cwd)) { + console.log(chalk.yellow(' DSS server is not running')); + console.log(chalk.dim(' Start it with: dss start')); + console.log(''); + console.log(chalk.dim(' Or sync directly via API if running remotely')); + process.exit(1); + } + + const fileKey = options.fileKey || config.figmaFileKey; + if (!fileKey) { + console.log(chalk.red(' No Figma file key configured')); + console.log(chalk.dim(' Set it with: dss config --set figmaFileKey=YOUR_KEY')); + process.exit(1); + } + + const format = options.format || config.tokenFormat || 'css'; + const outputPath = options.output || getDefaultOutputPath(format, cwd); + + const spinner = ora(`Syncing tokens from Figma (${format})...`).start(); + + try { + const api = getApiClient({ port: config.port }); + + // Extract tokens + spinner.text = 'Extracting tokens from Figma...'; + const result = await api.extractTokens(fileKey, format); + + if (!result.success) { + throw new Error('Token extraction failed'); + } + + // Write output file + spinner.text = `Writing ${result.tokens_count} tokens to ${outputPath}...`; + + const dir = dirname(outputPath); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + writeFileSync(outputPath, result.formatted_output); + + spinner.succeed(`Synced ${result.tokens_count} tokens`); + console.log(''); + console.log(chalk.green(' Output:'), chalk.dim(outputPath)); + console.log(''); + + // Show token summary + const categories = result.tokens.reduce((acc, t) => { + acc[t.category] = (acc[t.category] || 0) + 1; + return acc; + }, {} as Record); + + console.log(chalk.dim(' Token breakdown:')); + Object.entries(categories).forEach(([cat, count]) => { + console.log(chalk.dim(` ${cat}: ${count}`)); + }); + console.log(''); + + } catch (error) { + spinner.fail('Sync failed'); + console.error(chalk.red(` ${(error as Error).message}`)); + process.exit(1); + } +} + +function getDefaultOutputPath(format: string, cwd: string): string { + const extensions: Record = { + css: 'tokens.css', + scss: '_tokens.scss', + json: 'tokens.json', + ts: 'tokens.ts', + js: 'tokens.js', + }; + + const filename = extensions[format] || 'tokens.css'; + return join(cwd, '.dss', 'output', filename); +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts new file mode 100644 index 0000000..70c29ea --- /dev/null +++ b/apps/cli/src/index.ts @@ -0,0 +1,15 @@ +/** + * DSS - Design System Server + * + * Programmatic API for DSS. + */ + +export { DSSApiClient, getApiClient, type ApiOptions } from './lib/api.js'; +export { getConfig, getProjectRoot, hasProjectConfig, type DSSConfig } from './lib/config.js'; +export { + startServer, + stopServer, + isServerRunning, + getServerPid, + waitForServer, +} from './lib/server.js'; diff --git a/apps/cli/src/lib/api.ts b/apps/cli/src/lib/api.ts new file mode 100644 index 0000000..7e863bb --- /dev/null +++ b/apps/cli/src/lib/api.ts @@ -0,0 +1,144 @@ +/** + * DSS API Client + * + * Communicates with the DSS server. + */ + +import { getConfig } from './config.js'; + +export interface ApiOptions { + port?: number; + baseUrl?: string; +} + +export class DSSApiClient { + private baseUrl: string; + + constructor(options: ApiOptions = {}) { + const config = getConfig(); + const port = options.port || config.port || 3456; + this.baseUrl = options.baseUrl || `http://localhost:${port}/api`; + } + + private async request( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${this.baseUrl}${endpoint}`; + const response = await fetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ message: response.statusText })) as { message?: string }; + throw new Error(errorData.message || `Request failed: ${response.status}`); + } + + const text = await response.text(); + return text ? JSON.parse(text) as T : {} as T; + } + + async health(): Promise<{ status: string; figma_mode: string }> { + // Health endpoint is at root, not under /api + const url = this.baseUrl.replace('/api', '') + '/health'; + const response = await fetch(url); + return response.json() as Promise<{ status: string; figma_mode: string }>; + } + + async getConfig(): Promise> { + return this.request('/config'); + } + + async setFigmaToken(token: string): Promise { + await this.request('/config', { + method: 'PUT', + body: JSON.stringify({ figma_token: token }), + }); + } + + async testFigmaConnection(): Promise<{ success: boolean; user?: string; error?: string }> { + return this.request('/config/figma/test', { method: 'POST' }); + } + + async extractTokens(fileKey: string, format: string = 'css'): Promise<{ + success: boolean; + tokens_count: number; + tokens: Array<{ name: string; value: string; type: string; category: string }>; + formatted_output: string; + output_path: string; + }> { + return this.request('/figma/extract-variables', { + method: 'POST', + body: JSON.stringify({ file_key: fileKey, format }), + }); + } + + async extractComponents(fileKey: string): Promise<{ + success: boolean; + components_count: number; + components: Array<{ name: string; key: string; description: string; variants: string[] }>; + output_path: string; + }> { + return this.request('/figma/extract-components', { + method: 'POST', + body: JSON.stringify({ file_key: fileKey }), + }); + } + + async syncTokens(fileKey: string, targetPath: string, format: string = 'css'): Promise<{ + success: boolean; + tokens_synced: number; + output_file: string; + }> { + return this.request('/figma/sync-tokens', { + method: 'POST', + body: JSON.stringify({ file_key: fileKey, target_path: targetPath, format }), + }); + } + + async generateCode(fileKey: string, componentName: string, framework: string = 'react'): Promise<{ + success: boolean; + component: string; + framework: string; + code: string; + }> { + return this.request('/figma/generate-code', { + method: 'POST', + body: JSON.stringify({ file_key: fileKey, component_name: componentName, framework }), + }); + } + + async getProjects(): Promise> { + return this.request('/projects'); + } + + async createProject(data: { + name: string; + description?: string; + figma_file_key?: string; + }): Promise<{ id: string; name: string }> { + return this.request('/projects', { + method: 'POST', + body: JSON.stringify(data), + }); + } +} + +// Singleton instance +let apiClient: DSSApiClient | null = null; + +export function getApiClient(options?: ApiOptions): DSSApiClient { + if (!apiClient || options) { + apiClient = new DSSApiClient(options); + } + return apiClient; +} diff --git a/apps/cli/src/lib/config.ts b/apps/cli/src/lib/config.ts new file mode 100644 index 0000000..8c57522 --- /dev/null +++ b/apps/cli/src/lib/config.ts @@ -0,0 +1,95 @@ +/** + * DSS Configuration Manager + * + * Manages local and project configuration. + */ + +import Conf from 'conf'; +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +// Global user config (stored in home directory) +const globalConfig = new Conf({ + projectName: 'dss', + schema: { + figmaToken: { type: 'string' }, + defaultPort: { type: 'number', default: 3456 }, + defaultFormat: { type: 'string', default: 'css' }, + }, +}); + +// Project-level config file +const PROJECT_CONFIG_FILE = '.dss/config.json'; + +export interface DSSConfig { + figmaFileKey?: string; + figmaToken?: string; + port?: number; + outputDir?: string; + tokenFormat?: 'css' | 'scss' | 'json' | 'ts'; + componentFramework?: 'react' | 'vue' | 'svelte' | 'webcomponent'; +} + +export function getProjectRoot(): string { + return process.cwd(); +} + +export function getProjectConfigPath(): string { + return join(getProjectRoot(), PROJECT_CONFIG_FILE); +} + +export function hasProjectConfig(): boolean { + return existsSync(getProjectConfigPath()); +} + +export function loadProjectConfig(): DSSConfig { + const configPath = getProjectConfigPath(); + if (!existsSync(configPath)) { + return {}; + } + + try { + const content = readFileSync(configPath, 'utf-8'); + return JSON.parse(content); + } catch (error) { + console.warn('Failed to parse project config:', error); + return {}; + } +} + +export function saveProjectConfig(config: DSSConfig): void { + const configPath = getProjectConfigPath(); + const dir = join(getProjectRoot(), '.dss'); + + // Ensure .dss directory exists + if (!existsSync(dir)) { + const { mkdirSync } = require('fs'); + mkdirSync(dir, { recursive: true }); + } + + writeFileSync(configPath, JSON.stringify(config, null, 2)); +} + +export function getConfig(): DSSConfig { + const project = loadProjectConfig(); + return { + figmaToken: project.figmaToken || globalConfig.get('figmaToken') as string | undefined, + figmaFileKey: project.figmaFileKey, + port: project.port || globalConfig.get('defaultPort') as number, + outputDir: project.outputDir || '.dss/output', + tokenFormat: project.tokenFormat || (globalConfig.get('defaultFormat') as DSSConfig['tokenFormat']), + componentFramework: project.componentFramework || 'react', + }; +} + +export function setGlobalConfig(key: string, value: string | number): void { + globalConfig.set(key, value); +} + +export function getGlobalConfig(key: string): unknown { + return globalConfig.get(key); +} + +export function listGlobalConfig(): Record { + return globalConfig.store; +} diff --git a/apps/cli/src/lib/server.ts b/apps/cli/src/lib/server.ts new file mode 100644 index 0000000..3a173e9 --- /dev/null +++ b/apps/cli/src/lib/server.ts @@ -0,0 +1,197 @@ +/** + * DSS Server Manager + * + * Manages the Python server subprocess. + */ + +import { spawn, ChildProcess } from 'child_process'; +import { existsSync, writeFileSync, readFileSync, unlinkSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +interface ServerOptions { + port: number; + dev: boolean; + cwd: string; +} + +let serverProcess: ChildProcess | null = null; + +export function getPythonPath(): string { + // Check for bundled Python venv in npm package (dist/lib -> ../../python/venv) + const bundledPython = join(__dirname, '../../python/venv/bin/python3'); + if (existsSync(bundledPython)) { + return bundledPython; + } + + // Development path (src/lib -> ../../python/venv) + const devPython = join(__dirname, '../../../python/venv/bin/python3'); + if (existsSync(devPython)) { + return devPython; + } + + return 'python3'; +} + +export function getServerPath(): string { + // Check for bundled server or use from package + const bundledServer = join(__dirname, '../../python/api/server.py'); + if (existsSync(bundledServer)) { + return dirname(bundledServer); + } + + // Fall back to relative path from CLI (cli/dist/lib -> ../../../../tools/api) + const devServer = join(__dirname, '../../../../tools/api'); + if (existsSync(join(devServer, 'server.py'))) { + return devServer; + } + + // Also check development src path (cli/src/lib -> ../../../../tools/api) + const srcServer = join(__dirname, '../../../tools/api'); + if (existsSync(join(srcServer, 'server.py'))) { + return srcServer; + } + + throw new Error('Could not find DSS server. Run from the design-system-swarm directory or install the full package.'); +} + +export function getPidFile(cwd: string): string { + return join(cwd, '.dss', 'dss.pid'); +} + +export function getLogFile(cwd: string): string { + return join(cwd, '.dss', 'dss.log'); +} + +export function isServerRunning(cwd: string): boolean { + const pidFile = getPidFile(cwd); + if (!existsSync(pidFile)) { + return false; + } + + try { + const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10); + // Check if process exists + process.kill(pid, 0); + return true; + } catch { + // Process doesn't exist, clean up stale PID file + try { + unlinkSync(pidFile); + } catch { + // Ignore + } + return false; + } +} + +export function getServerPid(cwd: string): number | null { + const pidFile = getPidFile(cwd); + if (!existsSync(pidFile)) { + return null; + } + + try { + return parseInt(readFileSync(pidFile, 'utf-8').trim(), 10); + } catch { + return null; + } +} + +export async function startServer(options: ServerOptions): Promise { + const { port, dev, cwd } = options; + + if (isServerRunning(cwd)) { + throw new Error(`Server is already running (PID: ${getServerPid(cwd)})`); + } + + const pythonPath = getPythonPath(); + const serverDir = getServerPath(); + + // Build command args + const args = [ + '-m', 'uvicorn', + 'server:app', + '--host', '0.0.0.0', + '--port', String(port), + ]; + + if (dev) { + args.push('--reload'); + } + + // Set environment + const env = { + ...process.env, + PYTHONPATH: join(serverDir, '..'), + PORT: String(port), + }; + + // Spawn server process + serverProcess = spawn(pythonPath, args, { + cwd: serverDir, + env, + stdio: dev ? 'inherit' : ['ignore', 'pipe', 'pipe'], + detached: !dev, + }); + + if (!dev && serverProcess.pid) { + // Write PID file + const dssDir = join(cwd, '.dss'); + if (!existsSync(dssDir)) { + const { mkdirSync } = await import('fs'); + mkdirSync(dssDir, { recursive: true }); + } + + writeFileSync(getPidFile(cwd), String(serverProcess.pid)); + + // Write logs + const logStream = await import('fs').then(fs => + fs.createWriteStream(getLogFile(cwd), { flags: 'a' }) + ); + + serverProcess.stdout?.pipe(logStream); + serverProcess.stderr?.pipe(logStream); + + // Detach from parent + serverProcess.unref(); + } + + return serverProcess; +} + +export async function stopServer(cwd: string): Promise { + const pid = getServerPid(cwd); + if (!pid) { + return false; + } + + try { + process.kill(pid, 'SIGTERM'); + unlinkSync(getPidFile(cwd)); + return true; + } catch { + return false; + } +} + +export async function waitForServer(port: number, timeout = 10000): Promise { + const start = Date.now(); + const url = `http://localhost:${port}/health`; + + while (Date.now() - start < timeout) { + try { + const response = await fetch(url); + if (response.ok) { + return true; + } + } catch { + // Server not ready yet + } + await new Promise(resolve => setTimeout(resolve, 200)); + } + + return false; +} diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json new file mode 100644 index 0000000..479c240 --- /dev/null +++ b/apps/cli/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/dss-mvp1/dss-cli.py b/dss-cli.py similarity index 100% rename from dss-mvp1/dss-cli.py rename to dss-cli.py diff --git a/dss-mvp1/.dss/dss.backup-20251208-082339 b/dss-mvp1/.dss/dss.backup-20251208-082339 deleted file mode 100644 index 40c375e..0000000 Binary files a/dss-mvp1/.dss/dss.backup-20251208-082339 and /dev/null differ diff --git a/dss-mvp1/.dss/dss.backup-20251208-082356 b/dss-mvp1/.dss/dss.backup-20251208-082356 deleted file mode 100644 index 40c375e..0000000 Binary files a/dss-mvp1/.dss/dss.backup-20251208-082356 and /dev/null differ diff --git a/dss-mvp1/.env.test b/dss-mvp1/.env.test deleted file mode 100644 index 4a4d866..0000000 --- a/dss-mvp1/.env.test +++ /dev/null @@ -1,57 +0,0 @@ -# DSS MVP1 Test Environment Variables -# This file contains mock/test values for running tests -# DO NOT use these values in production! - -# ============================================================================= -# Mock API Keys for Testing -# ============================================================================= -# These are MOCK keys from tests/fixtures/api_keys.json -# They will NOT work with real APIs - -# Mock Anthropic API key (for testing only) -ANTHROPIC_API_KEY=sk-ant-api03-test-mock-key-for-testing-only-do-not-use-in-production-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - -# Mock Figma token (for testing only) -FIGMA_TOKEN=figd_test_mock_token_for_testing_only_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - -# ============================================================================= -# Test Configuration -# ============================================================================= -# Use test database -DATABASE_PATH=.dss/test.db - -# Disable caching in tests -FIGMA_CACHE_TTL=0 -DSS_CACHE_DIR=.dss/test_cache - -# Test mode -NODE_ENV=test -LOG_LEVEL=debug - -# ============================================================================= -# Server Configuration for Tests -# ============================================================================= -PORT=3456 -DSS_MCP_PORT=3457 -DSS_MCP_HOST=127.0.0.1 - -# ============================================================================= -# For Real API Testing (Optional) -# ============================================================================= -# If you want to test with REAL APIs, uncomment and add your real keys: -# REAL_ANTHROPIC_API_KEY=sk-ant-api03-your-real-key-here -# REAL_FIGMA_TOKEN=your-real-figma-token-here -# REAL_FIGMA_FILE_KEY=your-real-file-key-here - -# ============================================================================= -# Usage Instructions -# ============================================================================= -# To use this file: -# 1. Copy to .env: cp .env.test .env -# 2. Run tests: pytest tests/ -# 3. Mock APIs will be used automatically -# -# To test with real APIs: -# 1. Add your real keys above (REAL_* variables) -# 2. Update test code to use real keys when REAL_* vars are set -# 3. Run tests: pytest tests/ --real-api diff --git a/dss-mvp1/.storybook/dss-theme.ts b/dss-mvp1/.storybook/dss-theme.ts deleted file mode 100644 index 427029c..0000000 --- a/dss-mvp1/.storybook/dss-theme.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { create } from '@storybook/theming/create'; - -export const dssTheme = create({ - base: 'light', - brandTitle: 'Design System', - brandUrl: '', - brandImage: '', - brandTarget: '_self', - colorPrimary: '#3B82F6', - colorSecondary: '#10B981', - appBg: '#FFFFFF', - appContentBg: '#FFFFFF', - appBorderColor: '#E5E7EB', - textColor: '#1F2937', - textInverseColor: '#FFFFFF', - textMutedColor: '#6B7280', - barTextColor: '#6B7280', - barSelectedColor: '#3B82F6', - barBg: '#FFFFFF', - inputBg: '#FFFFFF', - inputBorder: '#D1D5DB', - inputTextColor: '#1F2937', - inputBorderRadius: 4, - fontBase: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', - fontCode: '"Fira Code", "Monaco", monospace', -}); diff --git a/dss-mvp1/.storybook/main.js b/dss-mvp1/.storybook/main.js deleted file mode 100644 index 680d293..0000000 --- a/dss-mvp1/.storybook/main.js +++ /dev/null @@ -1,20 +0,0 @@ -/** @type { import("@storybook/html").StorybookConfig } */ -const config = { - stories: [ - "../stories/Welcome.stories.js", - "../stories/generated/**/*.mdx", - "../stories/generated/**/*.stories.@(js|jsx|mjs|ts|tsx)" - ], - addons: [ - "@storybook/addon-essentials", - "@storybook/addon-webpack5-compiler-babel", - "@chromatic-com/storybook" - ], - framework: { - name: "@storybook/html-webpack5", - options: {} - }, - docs: {} -}; - -export default config; diff --git a/dss-mvp1/.storybook/manager.ts b/dss-mvp1/.storybook/manager.ts deleted file mode 100644 index d7d38ef..0000000 --- a/dss-mvp1/.storybook/manager.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { addons } from '@storybook/manager-api'; -import { dssTheme } from './dss-theme'; - -addons.setConfig({ - theme: dssTheme, -}); diff --git a/dss-mvp1/.storybook/preview.js b/dss-mvp1/.storybook/preview.js deleted file mode 100644 index 301c826..0000000 --- a/dss-mvp1/.storybook/preview.js +++ /dev/null @@ -1,188 +0,0 @@ -/** - * Storybook Preview Configuration - * - * Integrates DSS design tokens into Storybook: - * - Applies tokens globally to all stories - * - Configures Storybook UI with DSS colors - * - Sets up theme switching with token variables - * - Enables component stories to use own design system - */ - -// Import tokens generated from Figma -// These would be auto-generated via build process from token exporters -const DSSTokens = { - colors: { - primary: '#0066FF', - secondary: '#FF6B00', - success: '#00B600', - warning: '#FFB800', - danger: '#FF0000', - text: '#1A1A1A', - textLight: '#666666', - surface: '#FFFFFF', - background: '#F5F5F5', - border: '#E0E0E0', - }, - spacing: { - xs: '4px', - sm: '8px', - md: '16px', - lg: '24px', - xl: '32px', - }, - typography: { - headingFamily: "'Inter', sans-serif", - bodyFamily: "'Inter', sans-serif", - monospaceFamily: "'Courier New', monospace", - }, - borderRadius: { - sm: '4px', - md: '8px', - lg: '12px', - full: '9999px', - }, - shadows: { - sm: '0 1px 2px rgba(0, 0, 0, 0.05)', - md: '0 4px 6px rgba(0, 0, 0, 0.1)', - lg: '0 10px 15px rgba(0, 0, 0, 0.1)', - }, -}; - -// Create Storybook theme using DSS tokens -const createStorybookTheme = () => ({ - base: 'light', - colorPrimary: DSSTokens.colors.primary, - colorSecondary: DSSTokens.colors.secondary, - appBg: DSSTokens.colors.background, - appContentBg: DSSTokens.colors.surface, - appBorderColor: DSSTokens.colors.border, - appBorderRadius: parseInt(DSSTokens.borderRadius.md), - textColor: DSSTokens.colors.text, - textInverseColor: '#FFFFFF', - barTextColor: DSSTokens.colors.text, - barBg: DSSTokens.colors.surface, - barSelectedColor: DSSTokens.colors.primary, - barHoverColor: DSSTokens.colors.primary, - barBorderColor: DSSTokens.colors.border, - inputBg: '#FFFFFF', - inputBorder: DSSTokens.colors.border, - inputTextColor: DSSTokens.colors.text, - inputBorderRadius: parseInt(DSSTokens.borderRadius.md), - brandUrl: 'https://dss.overbits.luz.uy', - brandImage: '/dss-logo.svg', - brandTitle: 'DSS Design System', -}); - -// Register all CSS variables globally -const registerCSSVariables = () => { - const style = document.documentElement.style; - - // Register color tokens - Object.entries(DSSTokens.colors).forEach(([name, value]) => { - style.setProperty(`--dss-color-${name}`, value); - }); - - // Register spacing tokens - Object.entries(DSSTokens.spacing).forEach(([name, value]) => { - style.setProperty(`--dss-spacing-${name}`, value); - }); - - // Register typography tokens - Object.entries(DSSTokens.typography).forEach(([name, value]) => { - style.setProperty(`--dss-typography-${name}`, value); - }); - - // Register border radius tokens - Object.entries(DSSTokens.borderRadius).forEach(([name, value]) => { - style.setProperty(`--dss-radius-${name}`, value); - }); - - // Register shadow tokens - Object.entries(DSSTokens.shadows).forEach(([name, value]) => { - style.setProperty(`--dss-shadow-${name}`, value); - }); -}; - -// Export preview configuration -export const preview = { - parameters: { - // Apply DSS theme to Storybook UI - docs: { - theme: createStorybookTheme(), - }, - // Configure viewport options - viewport: { - viewports: { - mobile: { - name: 'Mobile', - styles: { width: '375px', height: '812px' }, - type: 'mobile', - }, - tablet: { - name: 'Tablet', - styles: { width: '768px', height: '1024px' }, - type: 'tablet', - }, - desktop: { - name: 'Desktop', - styles: { width: '1280px', height: '720px' }, - type: 'desktop', - }, - }, - }, - // Setup backgrounds - backgrounds: { - default: 'light', - values: [ - { name: 'light', value: DSSTokens.colors.surface }, - { name: 'dark', value: '#1A1A1A' }, - { name: 'gray', value: DSSTokens.colors.background }, - ], - }, - }, - - // Global decorator to apply DSS tokens to all stories - decorators: [ - (Story, context) => { - // Register CSS variables - registerCSSVariables(); - - // Get the story content - const storyContent = Story(); - - // Create wrapper div with DSS token styles - const wrapper = document.createElement('div'); - wrapper.style.cssText = ` - --dss-primary: ${DSSTokens.colors.primary}; - --dss-secondary: ${DSSTokens.colors.secondary}; - --dss-text: ${DSSTokens.colors.text}; - --dss-surface: ${DSSTokens.colors.surface}; - --dss-background: ${DSSTokens.colors.background}; - --dss-border: ${DSSTokens.colors.border}; - --dss-spacing-base: ${DSSTokens.spacing.md}; - --dss-font-body: ${DSSTokens.typography.bodyFamily}; - --dss-font-heading: ${DSSTokens.typography.headingFamily}; - --dss-radius: ${DSSTokens.borderRadius.md}; - font-family: ${DSSTokens.typography.bodyFamily}; - color: ${DSSTokens.colors.text}; - background-color: ${DSSTokens.colors.surface}; - padding: ${DSSTokens.spacing.lg}; - `; - - // Append story content to wrapper - if (typeof storyContent === 'string') { - wrapper.innerHTML = storyContent; - } else if (storyContent instanceof Node) { - wrapper.appendChild(storyContent); - } - - return wrapper; - }, - ], -}; - -// Export Storybook theme (for use in stories) -export const dssTheme = createStorybookTheme(); - -// Export DSS tokens for use in stories -export const dssTokens = DSSTokens; diff --git a/dss-mvp1/.storybook/preview.ts b/dss-mvp1/.storybook/preview.ts deleted file mode 100644 index 6d9aced..0000000 --- a/dss-mvp1/.storybook/preview.ts +++ /dev/null @@ -1,435 +0,0 @@ -import type { Preview } from '@storybook/react'; - -// Inject design tokens as CSS variables -const tokenStyles = ` -:root { - --version: 1.0.0; - --source: figma; - --figma_file: evCZlaeZrP7X20NIViSJbl; - --synced_at: 2025-12-09T12:50:40.840869; - --categories-color-neutral-50: {'6:0': {'r': 0.9803921580314636, 'g': 0.9803921580314636, 'b': 0.9803921580314636, 'a': 1}}; - --categories-color-neutral-100: {'6:0': {'r': 0.9607843160629272, 'g': 0.9607843160629272, 'b': 0.9607843160629272, 'a': 1}}; - --categories-color-neutral-200: {'6:0': {'r': 0.8980392217636108, 'g': 0.8980392217636108, 'b': 0.8980392217636108, 'a': 1}}; - --categories-color-neutral-300: {'6:0': {'r': 0.8313725590705872, 'g': 0.8313725590705872, 'b': 0.8313725590705872, 'a': 1}}; - --categories-color-neutral-400: {'6:0': {'r': 0.6392157077789307, 'g': 0.6392157077789307, 'b': 0.6392157077789307, 'a': 1}}; - --categories-color-neutral-500: {'6:0': {'r': 0.45098039507865906, 'g': 0.45098039507865906, 'b': 0.45098039507865906, 'a': 1}}; - --categories-color-neutral-600: {'6:0': {'r': 0.32156863808631897, 'g': 0.32156863808631897, 'b': 0.32156863808631897, 'a': 1}}; - --categories-color-neutral-700: {'6:0': {'r': 0.250980406999588, 'g': 0.250980406999588, 'b': 0.250980406999588, 'a': 1}}; - --categories-color-neutral-800: {'6:0': {'r': 0.14901961386203766, 'g': 0.14901961386203766, 'b': 0.14901961386203766, 'a': 1}}; - --categories-color-neutral-900: {'6:0': {'r': 0.09019608050584793, 'g': 0.09019608050584793, 'b': 0.09019608050584793, 'a': 1}}; - --categories-color-neutral-950: {'6:0': {'r': 0.03921568766236305, 'g': 0.03921568766236305, 'b': 0.03921568766236305, 'a': 1}}; - --categories-color-red-50: {'6:0': {'r': 0.9960784316062927, 'g': 0.9490196108818054, 'b': 0.9490196108818054, 'a': 1}}; - --categories-color-red-100: {'6:0': {'r': 1, 'g': 0.886274516582489, 'b': 0.886274516582489, 'a': 1}}; - --categories-color-red-200: {'6:0': {'r': 0.9960784316062927, 'g': 0.7921568751335144, 'b': 0.7921568751335144, 'a': 1}}; - --categories-color-red-300: {'6:0': {'r': 0.9882352948188782, 'g': 0.6470588445663452, 'b': 0.6470588445663452, 'a': 1}}; - --categories-color-red-400: {'6:0': {'r': 0.9725490212440491, 'g': 0.4431372582912445, 'b': 0.4431372582912445, 'a': 1}}; - --categories-color-red-500: {'6:0': {'r': 0.9372549057006836, 'g': 0.2666666805744171, 'b': 0.2666666805744171, 'a': 1}}; - --categories-color-red-600: {'6:0': {'r': 0.8627451062202454, 'g': 0.14901961386203766, 'b': 0.14901961386203766, 'a': 1}}; - --categories-color-red-700: {'6:0': {'r': 0.7254902124404907, 'g': 0.10980392247438431, 'b': 0.10980392247438431, 'a': 1}}; - --categories-color-red-800: {'6:0': {'r': 0.6000000238418579, 'g': 0.10588235408067703, 'b': 0.10588235408067703, 'a': 1}}; - --categories-color-red-900: {'6:0': {'r': 0.49803921580314636, 'g': 0.11372549086809158, 'b': 0.11372549086809158, 'a': 1}}; - --categories-color-red-950: {'6:0': {'r': 0.2705882489681244, 'g': 0.03921568766236305, 'b': 0.03921568766236305, 'a': 1}}; - --categories-color-blue-50: {'6:0': {'r': 0.9372549057006836, 'g': 0.9647058844566345, 'b': 1, 'a': 1}}; - --categories-color-blue-100: {'6:0': {'r': 0.8588235378265381, 'g': 0.9176470637321472, 'b': 0.9960784316062927, 'a': 1}}; - --categories-color-blue-200: {'6:0': {'r': 0.7490196228027344, 'g': 0.8588235378265381, 'b': 0.9960784316062927, 'a': 1}}; - --categories-color-blue-300: {'6:0': {'r': 0.5764706134796143, 'g': 0.772549033164978, 'b': 0.9921568632125854, 'a': 1}}; - --categories-color-blue-400: {'6:0': {'r': 0.3764705955982208, 'g': 0.6470588445663452, 'b': 0.9803921580314636, 'a': 1}}; - --categories-color-blue-500: {'6:0': {'r': 0.23137255012989044, 'g': 0.5098039507865906, 'b': 0.9647058844566345, 'a': 1}}; - --categories-color-blue-600: {'6:0': {'r': 0.14509804546833038, 'g': 0.38823530077934265, 'b': 0.9215686321258545, 'a': 1}}; - --categories-color-blue-700: {'6:0': {'r': 0.11372549086809158, 'g': 0.30588236451148987, 'b': 0.8470588326454163, 'a': 1}}; - --categories-color-blue-800: {'6:0': {'r': 0.11764705926179886, 'g': 0.250980406999588, 'b': 0.686274528503418, 'a': 1}}; - --categories-color-blue-900: {'6:0': {'r': 0.11764705926179886, 'g': 0.22745098173618317, 'b': 0.5411764979362488, 'a': 1}}; - --categories-color-blue-950: {'6:0': {'r': 0.09019608050584793, 'g': 0.14509804546833038, 'b': 0.3294117748737335, 'a': 1}}; - --categories-color-white: {'6:0': {'r': 1, 'g': 1, 'b': 1, 'a': 1}}; - --categories-color-color: {'6:0': {'r': 1, 'g': 1, 'b': 1, 'a': 1}}; - --categories-color-black: {'6:0': {'r': 0, 'g': 0, 'b': 0, 'a': 1}}; - --categories-color-slate-50: {'6:0': {'r': 0.9725490212440491, 'g': 0.9803921580314636, 'b': 0.9882352948188782, 'a': 1}}; - --categories-color-slate-100: {'6:0': {'r': 0.9450980424880981, 'g': 0.9607843160629272, 'b': 0.9764705896377563, 'a': 1}}; - --categories-color-slate-200: {'6:0': {'r': 0.886274516582489, 'g': 0.9098039269447327, 'b': 0.9411764740943909, 'a': 1}}; - --categories-color-slate-300: {'6:0': {'r': 0.7960784435272217, 'g': 0.8352941274642944, 'b': 0.8823529481887817, 'a': 1}}; - --categories-color-slate-400: {'6:0': {'r': 0.5803921818733215, 'g': 0.6392157077789307, 'b': 0.7215686440467834, 'a': 1}}; - --categories-color-slate-500: {'6:0': {'r': 0.3921568691730499, 'g': 0.45490196347236633, 'b': 0.545098066329956, 'a': 1}}; - --categories-color-slate-600: {'6:0': {'r': 0.27843138575553894, 'g': 0.3333333432674408, 'b': 0.4117647111415863, 'a': 1}}; - --categories-color-slate-700: {'6:0': {'r': 0.20000000298023224, 'g': 0.2549019753932953, 'b': 0.3333333432674408, 'a': 1}}; - --categories-color-slate-800: {'6:0': {'r': 0.11764705926179886, 'g': 0.16078431904315948, 'b': 0.23137255012989044, 'a': 1}}; - --categories-color-slate-900: {'6:0': {'r': 0.05882352963089943, 'g': 0.09019608050584793, 'b': 0.16470588743686676, 'a': 1}}; - --categories-color-slate-950: {'6:0': {'r': 0.007843137718737125, 'g': 0.0235294122248888, 'b': 0.09019608050584793, 'a': 1}}; - --categories-color-gray-50: {'6:0': {'r': 0.9764705896377563, 'g': 0.9803921580314636, 'b': 0.9843137264251709, 'a': 1}}; - --categories-color-gray-100: {'6:0': {'r': 0.9529411792755127, 'g': 0.95686274766922, 'b': 0.9647058844566345, 'a': 1}}; - --categories-color-gray-200: {'6:0': {'r': 0.8980392217636108, 'g': 0.9058823585510254, 'b': 0.9215686321258545, 'a': 1}}; - --categories-color-gray-300: {'6:0': {'r': 0.8196078538894653, 'g': 0.8352941274642944, 'b': 0.8588235378265381, 'a': 1}}; - --categories-color-gray-400: {'6:0': {'r': 0.6117647290229797, 'g': 0.6392157077789307, 'b': 0.686274528503418, 'a': 1}}; - --categories-color-gray-500: {'6:0': {'r': 0.41960784792900085, 'g': 0.4470588266849518, 'b': 0.501960813999176, 'a': 1}}; - --categories-color-gray-600: {'6:0': {'r': 0.29411765933036804, 'g': 0.3333333432674408, 'b': 0.38823530077934265, 'a': 1}}; - --categories-color-gray-700: {'6:0': {'r': 0.21568627655506134, 'g': 0.2549019753932953, 'b': 0.3176470696926117, 'a': 1}}; - --categories-color-gray-800: {'6:0': {'r': 0.12156862765550613, 'g': 0.16078431904315948, 'b': 0.21568627655506134, 'a': 1}}; - --categories-color-gray-900: {'6:0': {'r': 0.06666667014360428, 'g': 0.0941176488995552, 'b': 0.15294118225574493, 'a': 1}}; - --categories-color-gray-950: {'6:0': {'r': 0.0117647061124444, 'g': 0.027450980618596077, 'b': 0.07058823853731155, 'a': 1}}; - --categories-color-zinc-50: {'6:0': {'r': 0.9803921580314636, 'g': 0.9803921580314636, 'b': 0.9803921580314636, 'a': 1}}; - --categories-color-zinc-100: {'6:0': {'r': 0.95686274766922, 'g': 0.95686274766922, 'b': 0.9607843160629272, 'a': 1}}; - --categories-color-zinc-200: {'6:0': {'r': 0.8941176533699036, 'g': 0.8941176533699036, 'b': 0.9058823585510254, 'a': 1}}; - --categories-color-zinc-300: {'6:0': {'r': 0.8313725590705872, 'g': 0.8313725590705872, 'b': 0.8470588326454163, 'a': 1}}; - --categories-color-zinc-400: {'6:0': {'r': 0.6313725709915161, 'g': 0.6313725709915161, 'b': 0.6666666865348816, 'a': 1}}; - --categories-color-zinc-500: {'6:0': {'r': 0.4431372582912445, 'g': 0.4431372582912445, 'b': 0.47843137383461, 'a': 1}}; - --categories-color-zinc-600: {'6:0': {'r': 0.32156863808631897, 'g': 0.32156863808631897, 'b': 0.35686275362968445, 'a': 1}}; - --categories-color-zinc-700: {'6:0': {'r': 0.24705882370471954, 'g': 0.24705882370471954, 'b': 0.27450981736183167, 'a': 1}}; - --categories-color-zinc-800: {'6:0': {'r': 0.15294118225574493, 'g': 0.15294118225574493, 'b': 0.16470588743686676, 'a': 1}}; - --categories-color-zinc-900: {'6:0': {'r': 0.0941176488995552, 'g': 0.0941176488995552, 'b': 0.10588235408067703, 'a': 1}}; - --categories-color-zinc-950: {'6:0': {'r': 0.03529411926865578, 'g': 0.03529411926865578, 'b': 0.04313725605607033, 'a': 1}}; - --categories-color-stone-50: {'6:0': {'r': 0.9803921580314636, 'g': 0.9803921580314636, 'b': 0.9764705896377563, 'a': 1}}; - --categories-color-stone-100: {'6:0': {'r': 0.9607843160629272, 'g': 0.9607843160629272, 'b': 0.95686274766922, 'a': 1}}; - --categories-color-stone-200: {'6:0': {'r': 0.9058823585510254, 'g': 0.8980392217636108, 'b': 0.8941176533699036, 'a': 1}}; - --categories-color-stone-300: {'6:0': {'r': 0.8392156958580017, 'g': 0.8274509906768799, 'b': 0.8196078538894653, 'a': 1}}; - --categories-color-stone-400: {'6:0': {'r': 0.658823549747467, 'g': 0.6352941393852234, 'b': 0.6196078658103943, 'a': 1}}; - --categories-color-stone-500: {'6:0': {'r': 0.47058823704719543, 'g': 0.4431372582912445, 'b': 0.42352941632270813, 'a': 1}}; - --categories-color-stone-600: {'6:0': {'r': 0.34117648005485535, 'g': 0.32549020648002625, 'b': 0.30588236451148987, 'a': 1}}; - --categories-color-stone-700: {'6:0': {'r': 0.2666666805744171, 'g': 0.250980406999588, 'b': 0.23529411852359772, 'a': 1}}; - --categories-color-stone-800: {'6:0': {'r': 0.16078431904315948, 'g': 0.14509804546833038, 'b': 0.1411764770746231, 'a': 1}}; - --categories-color-stone-900: {'6:0': {'r': 0.10980392247438431, 'g': 0.09803921729326248, 'b': 0.09019608050584793, 'a': 1}}; - --categories-color-stone-950: {'6:0': {'r': 0.0470588244497776, 'g': 0.03921568766236305, 'b': 0.03529411926865578, 'a': 1}}; - --categories-color-orange-50: {'6:0': {'r': 1, 'g': 0.9686274528503418, 'b': 0.929411768913269, 'a': 1}}; - --categories-color-orange-100: {'6:0': {'r': 1, 'g': 0.929411768913269, 'b': 0.8352941274642944, 'a': 1}}; - --categories-color-orange-200: {'6:0': {'r': 0.9960784316062927, 'g': 0.843137264251709, 'b': 0.6666666865348816, 'a': 1}}; - --categories-color-orange-300: {'6:0': {'r': 0.9921568632125854, 'g': 0.729411780834198, 'b': 0.45490196347236633, 'a': 1}}; - --categories-color-orange-400: {'6:0': {'r': 0.9843137264251709, 'g': 0.572549045085907, 'b': 0.23529411852359772, 'a': 1}}; - --categories-color-orange-500: {'6:0': {'r': 0.9764705896377563, 'g': 0.45098039507865906, 'b': 0.08627451211214066, 'a': 1}}; - --categories-color-orange-600: {'6:0': {'r': 0.9176470637321472, 'g': 0.3450980484485626, 'b': 0.0470588244497776, 'a': 1}}; - --categories-color-orange-700: {'6:0': {'r': 0.7607843279838562, 'g': 0.2549019753932953, 'b': 0.0470588244497776, 'a': 1}}; - --categories-color-orange-800: {'6:0': {'r': 0.6039215922355652, 'g': 0.20392157137393951, 'b': 0.07058823853731155, 'a': 1}}; - --categories-color-orange-900: {'6:0': {'r': 0.48627451062202454, 'g': 0.1764705926179886, 'b': 0.07058823853731155, 'a': 1}}; - --categories-color-orange-950: {'6:0': {'r': 0.26274511218070984, 'g': 0.0784313753247261, 'b': 0.027450980618596077, 'a': 1}}; - --categories-color-amber-50: {'6:0': {'r': 1, 'g': 0.9843137264251709, 'b': 0.9215686321258545, 'a': 1}}; - --categories-color-amber-100: {'6:0': {'r': 0.9960784316062927, 'g': 0.9529411792755127, 'b': 0.7803921699523926, 'a': 1}}; - --categories-color-amber-200: {'6:0': {'r': 0.9921568632125854, 'g': 0.9019607901573181, 'b': 0.5411764979362488, 'a': 1}}; - --categories-color-amber-300: {'6:0': {'r': 0.9882352948188782, 'g': 0.8274509906768799, 'b': 0.3019607961177826, 'a': 1}}; - --categories-color-amber-400: {'6:0': {'r': 0.9843137264251709, 'g': 0.7490196228027344, 'b': 0.1411764770746231, 'a': 1}}; - --categories-color-amber-500: {'6:0': {'r': 0.9607843160629272, 'g': 0.6196078658103943, 'b': 0.04313725605607033, 'a': 1}}; - --categories-color-amber-600: {'6:0': {'r': 0.8509804010391235, 'g': 0.46666666865348816, 'b': 0.0235294122248888, 'a': 1}}; - --categories-color-amber-700: {'6:0': {'r': 0.7058823704719543, 'g': 0.32549020648002625, 'b': 0.03529411926865578, 'a': 1}}; - --categories-color-amber-800: {'6:0': {'r': 0.572549045085907, 'g': 0.250980406999588, 'b': 0.054901961237192154, 'a': 1}}; - --categories-color-amber-900: {'6:0': {'r': 0.47058823704719543, 'g': 0.2078431397676468, 'b': 0.05882352963089943, 'a': 1}}; - --categories-color-amber-950: {'6:0': {'r': 0.2705882489681244, 'g': 0.10196078568696976, 'b': 0.0117647061124444, 'a': 1}}; - --categories-color-lime-50: {'6:0': {'r': 0.9686274528503418, 'g': 0.9960784316062927, 'b': 0.9058823585510254, 'a': 1}}; - --categories-color-lime-100: {'6:0': {'r': 0.9254902005195618, 'g': 0.9882352948188782, 'b': 0.7960784435272217, 'a': 1}}; - --categories-color-lime-200: {'6:0': {'r': 0.8509804010391235, 'g': 0.9764705896377563, 'b': 0.615686297416687, 'a': 1}}; - --categories-color-lime-300: {'6:0': {'r': 0.7450980544090271, 'g': 0.9490196108818054, 'b': 0.3921568691730499, 'a': 1}}; - --categories-color-lime-400: {'6:0': {'r': 0.6392157077789307, 'g': 0.9019607901573181, 'b': 0.2078431397676468, 'a': 1}}; - --categories-color-lime-500: {'6:0': {'r': 0.5176470875740051, 'g': 0.800000011920929, 'b': 0.08627451211214066, 'a': 1}}; - --categories-color-lime-600: {'6:0': {'r': 0.3960784375667572, 'g': 0.6392157077789307, 'b': 0.05098039284348488, 'a': 1}}; - --categories-color-lime-700: {'6:0': {'r': 0.3019607961177826, 'g': 0.48627451062202454, 'b': 0.05882352963089943, 'a': 1}}; - --categories-color-lime-800: {'6:0': {'r': 0.24705882370471954, 'g': 0.3843137323856354, 'b': 0.07058823853731155, 'a': 1}}; - --categories-color-lime-900: {'6:0': {'r': 0.21176470816135406, 'g': 0.32549020648002625, 'b': 0.0784313753247261, 'a': 1}}; - --categories-color-lime-950: {'6:0': {'r': 0.10196078568696976, 'g': 0.18039216101169586, 'b': 0.019607843831181526, 'a': 1}}; - --categories-color-emerald-50: {'6:0': {'r': 0.9254902005195618, 'g': 0.9921568632125854, 'b': 0.9607843160629272, 'a': 1}}; - --categories-color-emerald-100: {'6:0': {'r': 0.8196078538894653, 'g': 0.9803921580314636, 'b': 0.8980392217636108, 'a': 1}}; - --categories-color-emerald-200: {'6:0': {'r': 0.6549019813537598, 'g': 0.9529411792755127, 'b': 0.8156862854957581, 'a': 1}}; - --categories-color-emerald-300: {'6:0': {'r': 0.4313725531101227, 'g': 0.9058823585510254, 'b': 0.7176470756530762, 'a': 1}}; - --categories-color-emerald-400: {'6:0': {'r': 0.20392157137393951, 'g': 0.8274509906768799, 'b': 0.6000000238418579, 'a': 1}}; - --categories-color-emerald-500: {'6:0': {'r': 0.062745101749897, 'g': 0.7254902124404907, 'b': 0.5058823823928833, 'a': 1}}; - --categories-color-emerald-600: {'6:0': {'r': 0.019607843831181526, 'g': 0.5882353186607361, 'b': 0.4117647111415863, 'a': 1}}; - --categories-color-emerald-700: {'6:0': {'r': 0.01568627543747425, 'g': 0.47058823704719543, 'b': 0.34117648005485535, 'a': 1}}; - --categories-color-emerald-800: {'6:0': {'r': 0.0235294122248888, 'g': 0.37254902720451355, 'b': 0.27450981736183167, 'a': 1}}; - --categories-color-emerald-900: {'6:0': {'r': 0.0235294122248888, 'g': 0.30588236451148987, 'b': 0.23137255012989044, 'a': 1}}; - --categories-color-emerald-950: {'6:0': {'r': 0.007843137718737125, 'g': 0.1725490242242813, 'b': 0.13333334028720856, 'a': 1}}; - --categories-color-teal-50: {'6:0': {'r': 0.9411764740943909, 'g': 0.9921568632125854, 'b': 0.9803921580314636, 'a': 1}}; - --categories-color-teal-100: {'6:0': {'r': 0.800000011920929, 'g': 0.9843137264251709, 'b': 0.9450980424880981, 'a': 1}}; - --categories-color-teal-200: {'6:0': {'r': 0.6000000238418579, 'g': 0.9647058844566345, 'b': 0.8941176533699036, 'a': 1}}; - --categories-color-teal-300: {'6:0': {'r': 0.3686274588108063, 'g': 0.9176470637321472, 'b': 0.8313725590705872, 'a': 1}}; - --categories-color-teal-400: {'6:0': {'r': 0.1764705926179886, 'g': 0.8313725590705872, 'b': 0.7490196228027344, 'a': 1}}; - --categories-color-teal-500: {'6:0': {'r': 0.0784313753247261, 'g': 0.7215686440467834, 'b': 0.6509804129600525, 'a': 1}}; - --categories-color-teal-600: {'6:0': {'r': 0.05098039284348488, 'g': 0.5803921818733215, 'b': 0.5333333611488342, 'a': 1}}; - --categories-color-teal-700: {'6:0': {'r': 0.05882352963089943, 'g': 0.4627451002597809, 'b': 0.4313725531101227, 'a': 1}}; - --categories-color-teal-800: {'6:0': {'r': 0.06666667014360428, 'g': 0.3686274588108063, 'b': 0.3490196168422699, 'a': 1}}; - --categories-color-teal-900: {'6:0': {'r': 0.07450980693101883, 'g': 0.30588236451148987, 'b': 0.29019609093666077, 'a': 1}}; - --categories-color-teal-950: {'6:0': {'r': 0.01568627543747425, 'g': 0.18431372940540314, 'b': 0.18039216101169586, 'a': 1}}; - --categories-color-cyan-50: {'6:0': {'r': 0.9254902005195618, 'g': 0.9960784316062927, 'b': 1, 'a': 1}}; - --categories-color-cyan-100: {'6:0': {'r': 0.8117647171020508, 'g': 0.9803921580314636, 'b': 0.9960784316062927, 'a': 1}}; - --categories-color-cyan-200: {'6:0': {'r': 0.6470588445663452, 'g': 0.9529411792755127, 'b': 0.9882352948188782, 'a': 1}}; - --categories-color-cyan-300: {'6:0': {'r': 0.40392157435417175, 'g': 0.9098039269447327, 'b': 0.9764705896377563, 'a': 1}}; - --categories-color-cyan-400: {'6:0': {'r': 0.13333334028720856, 'g': 0.8274509906768799, 'b': 0.9333333373069763, 'a': 1}}; - --categories-color-cyan-500: {'6:0': {'r': 0.0235294122248888, 'g': 0.7137255072593689, 'b': 0.8313725590705872, 'a': 1}}; - --categories-color-cyan-600: {'6:0': {'r': 0.0313725508749485, 'g': 0.5686274766921997, 'b': 0.6980392336845398, 'a': 1}}; - --categories-color-cyan-700: {'6:0': {'r': 0.054901961237192154, 'g': 0.45490196347236633, 'b': 0.5647059082984924, 'a': 1}}; - --categories-color-cyan-800: {'6:0': {'r': 0.08235294371843338, 'g': 0.3686274588108063, 'b': 0.4588235318660736, 'a': 1}}; - --categories-color-cyan-900: {'6:0': {'r': 0.08627451211214066, 'g': 0.30588236451148987, 'b': 0.38823530077934265, 'a': 1}}; - --categories-color-cyan-950: {'6:0': {'r': 0.0313725508749485, 'g': 0.20000000298023224, 'b': 0.2666666805744171, 'a': 1}}; - --categories-color-sky-50: {'6:0': {'r': 0.9411764740943909, 'g': 0.9764705896377563, 'b': 1, 'a': 1}}; - --categories-color-sky-100: {'6:0': {'r': 0.8784313797950745, 'g': 0.9490196108818054, 'b': 0.9960784316062927, 'a': 1}}; - --categories-color-sky-200: {'6:0': {'r': 0.729411780834198, 'g': 0.9019607901573181, 'b': 0.9921568632125854, 'a': 1}}; - --categories-color-sky-300: {'6:0': {'r': 0.4901960790157318, 'g': 0.8274509906768799, 'b': 0.9882352948188782, 'a': 1}}; - --categories-color-sky-400: {'6:0': {'r': 0.21960784494876862, 'g': 0.7411764860153198, 'b': 0.9725490212440491, 'a': 1}}; - --categories-color-sky-500: {'6:0': {'r': 0.054901961237192154, 'g': 0.6470588445663452, 'b': 0.9137254953384399, 'a': 1}}; - --categories-color-sky-600: {'6:0': {'r': 0.007843137718737125, 'g': 0.5176470875740051, 'b': 0.7803921699523926, 'a': 1}}; - --categories-color-sky-700: {'6:0': {'r': 0.0117647061124444, 'g': 0.4117647111415863, 'b': 0.6313725709915161, 'a': 1}}; - --categories-color-sky-800: {'6:0': {'r': 0.027450980618596077, 'g': 0.3490196168422699, 'b': 0.5215686559677124, 'a': 1}}; - --categories-color-sky-900: {'6:0': {'r': 0.0470588244497776, 'g': 0.29019609093666077, 'b': 0.4313725531101227, 'a': 1}}; - --categories-color-sky-950: {'6:0': {'r': 0.0313725508749485, 'g': 0.18431372940540314, 'b': 0.2862745225429535, 'a': 1}}; - --categories-color-indigo-50: {'6:0': {'r': 0.9333333373069763, 'g': 0.9490196108818054, 'b': 1, 'a': 1}}; - --categories-color-indigo-100: {'6:0': {'r': 0.8784313797950745, 'g': 0.9058823585510254, 'b': 1, 'a': 1}}; - --categories-color-indigo-200: {'6:0': {'r': 0.7803921699523926, 'g': 0.8235294222831726, 'b': 0.9960784316062927, 'a': 1}}; - --categories-color-indigo-300: {'6:0': {'r': 0.6470588445663452, 'g': 0.7058823704719543, 'b': 0.9882352948188782, 'a': 1}}; - --categories-color-indigo-400: {'6:0': {'r': 0.5058823823928833, 'g': 0.5490196347236633, 'b': 0.9725490212440491, 'a': 1}}; - --categories-color-indigo-500: {'6:0': {'r': 0.38823530077934265, 'g': 0.4000000059604645, 'b': 0.9450980424880981, 'a': 1}}; - --categories-color-indigo-600: {'6:0': {'r': 0.30980393290519714, 'g': 0.27450981736183167, 'b': 0.8980392217636108, 'a': 1}}; - --categories-color-indigo-700: {'6:0': {'r': 0.26274511218070984, 'g': 0.21960784494876862, 'b': 0.7921568751335144, 'a': 1}}; - --categories-color-indigo-800: {'6:0': {'r': 0.21568627655506134, 'g': 0.1882352977991104, 'b': 0.6392157077789307, 'a': 1}}; - --categories-color-indigo-900: {'6:0': {'r': 0.1921568661928177, 'g': 0.18039216101169586, 'b': 0.5058823823928833, 'a': 1}}; - --categories-color-indigo-950: {'6:0': {'r': 0.11764705926179886, 'g': 0.10588235408067703, 'b': 0.29411765933036804, 'a': 1}}; - --categories-color-violet-50: {'6:0': {'r': 0.9607843160629272, 'g': 0.9529411792755127, 'b': 1, 'a': 1}}; - --categories-color-violet-100: {'6:0': {'r': 0.929411768913269, 'g': 0.9137254953384399, 'b': 0.9960784316062927, 'a': 1}}; - --categories-color-violet-200: {'6:0': {'r': 0.8666666746139526, 'g': 0.8392156958580017, 'b': 0.9960784316062927, 'a': 1}}; - --categories-color-violet-300: {'6:0': {'r': 0.7686274647712708, 'g': 0.7098039388656616, 'b': 0.9921568632125854, 'a': 1}}; - --categories-color-violet-400: {'6:0': {'r': 0.6549019813537598, 'g': 0.545098066329956, 'b': 0.9803921580314636, 'a': 1}}; - --categories-color-violet-500: {'6:0': {'r': 0.545098066329956, 'g': 0.3607843220233917, 'b': 0.9647058844566345, 'a': 1}}; - --categories-color-violet-600: {'6:0': {'r': 0.48627451062202454, 'g': 0.22745098173618317, 'b': 0.929411768913269, 'a': 1}}; - --categories-color-violet-700: {'6:0': {'r': 0.4274509847164154, 'g': 0.1568627506494522, 'b': 0.8509804010391235, 'a': 1}}; - --categories-color-violet-800: {'6:0': {'r': 0.35686275362968445, 'g': 0.12941177189350128, 'b': 0.7137255072593689, 'a': 1}}; - --categories-color-violet-900: {'6:0': {'r': 0.2980392277240753, 'g': 0.11372549086809158, 'b': 0.5843137502670288, 'a': 1}}; - --categories-color-violet-950: {'6:0': {'r': 0.18039216101169586, 'g': 0.062745101749897, 'b': 0.3960784375667572, 'a': 1}}; - --categories-color-purple-50: {'6:0': {'r': 0.9803921580314636, 'g': 0.9607843160629272, 'b': 1, 'a': 1}}; - --categories-color-purple-100: {'6:0': {'r': 0.9529411792755127, 'g': 0.9098039269447327, 'b': 1, 'a': 1}}; - --categories-color-purple-200: {'6:0': {'r': 0.9137254953384399, 'g': 0.8352941274642944, 'b': 1, 'a': 1}}; - --categories-color-purple-300: {'6:0': {'r': 0.8470588326454163, 'g': 0.7058823704719543, 'b': 0.9960784316062927, 'a': 1}}; - --categories-color-purple-400: {'6:0': {'r': 0.7529411911964417, 'g': 0.5176470875740051, 'b': 0.9882352948188782, 'a': 1}}; - --categories-color-purple-500: {'6:0': {'r': 0.658823549747467, 'g': 0.3333333432674408, 'b': 0.9686274528503418, 'a': 1}}; - --categories-color-purple-600: {'6:0': {'r': 0.5764706134796143, 'g': 0.20000000298023224, 'b': 0.9176470637321472, 'a': 1}}; - --categories-color-purple-700: {'6:0': {'r': 0.4941176474094391, 'g': 0.13333334028720856, 'b': 0.8078431487083435, 'a': 1}}; - --categories-color-purple-800: {'6:0': {'r': 0.41960784792900085, 'g': 0.12941177189350128, 'b': 0.658823549747467, 'a': 1}}; - --categories-color-purple-900: {'6:0': {'r': 0.3450980484485626, 'g': 0.10980392247438431, 'b': 0.529411792755127, 'a': 1}}; - --categories-color-purple-950: {'6:0': {'r': 0.23137255012989044, 'g': 0.027450980618596077, 'b': 0.3921568691730499, 'a': 1}}; - --categories-color-fuchsia-50: {'6:0': {'r': 0.9921568632125854, 'g': 0.95686274766922, 'b': 1, 'a': 1}}; - --categories-color-fuchsia-100: {'6:0': {'r': 0.9803921580314636, 'g': 0.9098039269447327, 'b': 1, 'a': 1}}; - --categories-color-fuchsia-200: {'6:0': {'r': 0.9607843160629272, 'g': 0.8156862854957581, 'b': 0.9960784316062927, 'a': 1}}; - --categories-color-fuchsia-300: {'6:0': {'r': 0.9411764740943909, 'g': 0.6705882549285889, 'b': 0.9882352948188782, 'a': 1}}; - --categories-color-fuchsia-400: {'6:0': {'r': 0.9098039269447327, 'g': 0.4745098054409027, 'b': 0.9764705896377563, 'a': 1}}; - --categories-color-fuchsia-500: {'6:0': {'r': 0.8509804010391235, 'g': 0.27450981736183167, 'b': 0.9372549057006836, 'a': 1}}; - --categories-color-fuchsia-600: {'6:0': {'r': 0.7529411911964417, 'g': 0.14901961386203766, 'b': 0.8274509906768799, 'a': 1}}; - --categories-color-fuchsia-700: {'6:0': {'r': 0.6352941393852234, 'g': 0.10980392247438431, 'b': 0.686274528503418, 'a': 1}}; - --categories-color-fuchsia-800: {'6:0': {'r': 0.5254902243614197, 'g': 0.09803921729326248, 'b': 0.5607843399047852, 'a': 1}}; - --categories-color-fuchsia-900: {'6:0': {'r': 0.43921568989753723, 'g': 0.10196078568696976, 'b': 0.4588235318660736, 'a': 1}}; - --categories-color-fuchsia-950: {'6:0': {'r': 0.29019609093666077, 'g': 0.01568627543747425, 'b': 0.30588236451148987, 'a': 1}}; - --categories-color-pink-50: {'6:0': {'r': 0.9921568632125854, 'g': 0.9490196108818054, 'b': 0.9725490212440491, 'a': 1}}; - --categories-color-pink-100: {'6:0': {'r': 0.9882352948188782, 'g': 0.9058823585510254, 'b': 0.9529411792755127, 'a': 1}}; - --categories-color-pink-200: {'6:0': {'r': 0.9843137264251709, 'g': 0.8117647171020508, 'b': 0.9098039269447327, 'a': 1}}; - --categories-color-pink-300: {'6:0': {'r': 0.9764705896377563, 'g': 0.658823549747467, 'b': 0.8313725590705872, 'a': 1}}; - --categories-color-pink-400: {'6:0': {'r': 0.95686274766922, 'g': 0.4470588266849518, 'b': 0.7137255072593689, 'a': 1}}; - --categories-color-pink-500: {'6:0': {'r': 0.9254902005195618, 'g': 0.2823529541492462, 'b': 0.6000000238418579, 'a': 1}}; - --categories-color-pink-600: {'6:0': {'r': 0.8588235378265381, 'g': 0.15294118225574493, 'b': 0.46666666865348816, 'a': 1}}; - --categories-color-pink-700: {'6:0': {'r': 0.7450980544090271, 'g': 0.0941176488995552, 'b': 0.364705890417099, 'a': 1}}; - --categories-color-pink-800: {'6:0': {'r': 0.615686297416687, 'g': 0.09019608050584793, 'b': 0.3019607961177826, 'a': 1}}; - --categories-color-pink-900: {'6:0': {'r': 0.5137255191802979, 'g': 0.0941176488995552, 'b': 0.26274511218070984, 'a': 1}}; - --categories-color-pink-950: {'6:0': {'r': 0.3137255012989044, 'g': 0.027450980618596077, 'b': 0.1411764770746231, 'a': 1}}; - --categories-color-rose-50: {'6:0': {'r': 1, 'g': 0.9450980424880981, 'b': 0.9490196108818054, 'a': 1}}; - --categories-color-rose-100: {'6:0': {'r': 1, 'g': 0.8941176533699036, 'b': 0.9019607901573181, 'a': 1}}; - --categories-color-rose-200: {'6:0': {'r': 0.9960784316062927, 'g': 0.8039215803146362, 'b': 0.8274509906768799, 'a': 1}}; - --categories-color-rose-300: {'6:0': {'r': 0.9921568632125854, 'g': 0.6431372761726379, 'b': 0.686274528503418, 'a': 1}}; - --categories-color-rose-400: {'6:0': {'r': 0.9843137264251709, 'g': 0.4431372582912445, 'b': 0.5215686559677124, 'a': 1}}; - --categories-color-rose-500: {'6:0': {'r': 0.95686274766922, 'g': 0.24705882370471954, 'b': 0.3686274588108063, 'a': 1}}; - --categories-color-rose-600: {'6:0': {'r': 0.8823529481887817, 'g': 0.11372549086809158, 'b': 0.2823529541492462, 'a': 1}}; - --categories-color-rose-700: {'6:0': {'r': 0.7450980544090271, 'g': 0.07058823853731155, 'b': 0.23529411852359772, 'a': 1}}; - --categories-color-rose-800: {'6:0': {'r': 0.6235294342041016, 'g': 0.07058823853731155, 'b': 0.2235294133424759, 'a': 1}}; - --categories-color-rose-900: {'6:0': {'r': 0.5333333611488342, 'g': 0.07450980693101883, 'b': 0.21568627655506134, 'a': 1}}; - --categories-color-rose-950: {'6:0': {'r': 0.2980392277240753, 'g': 0.019607843831181526, 'b': 0.09803921729326248, 'a': 1}}; - --categories-color-green-50: {'6:0': {'r': 0.9411764740943909, 'g': 0.9921568632125854, 'b': 0.95686274766922, 'a': 1}}; - --categories-color-green-100: {'6:0': {'r': 0.8627451062202454, 'g': 0.9882352948188782, 'b': 0.9058823585510254, 'a': 1}}; - --categories-color-green-200: {'6:0': {'r': 0.7333333492279053, 'g': 0.9686274528503418, 'b': 0.8156862854957581, 'a': 1}}; - --categories-color-green-300: {'6:0': {'r': 0.5254902243614197, 'g': 0.9372549057006836, 'b': 0.6745098233222961, 'a': 1}}; - --categories-color-green-400: {'6:0': {'r': 0.29019609093666077, 'g': 0.8705882430076599, 'b': 0.501960813999176, 'a': 1}}; - --categories-color-green-500: {'6:0': {'r': 0.13333334028720856, 'g': 0.772549033164978, 'b': 0.3686274588108063, 'a': 1}}; - --categories-color-green-600: {'6:0': {'r': 0.08627451211214066, 'g': 0.6392157077789307, 'b': 0.29019609093666077, 'a': 1}}; - --categories-color-green-700: {'6:0': {'r': 0.08235294371843338, 'g': 0.501960813999176, 'b': 0.239215686917305, 'a': 1}}; - --categories-color-green-800: {'6:0': {'r': 0.08627451211214066, 'g': 0.3960784375667572, 'b': 0.20392157137393951, 'a': 1}}; - --categories-color-green-900: {'6:0': {'r': 0.0784313753247261, 'g': 0.32549020648002625, 'b': 0.1764705926179886, 'a': 1}}; - --categories-color-green-950: {'6:0': {'r': 0.019607843831181526, 'g': 0.18039216101169586, 'b': 0.08627451211214066, 'a': 1}}; - --categories-color-yellow-50: {'6:0': {'r': 0.9960784316062927, 'g': 0.9882352948188782, 'b': 0.9098039269447327, 'a': 1}}; - --categories-color-yellow-100: {'6:0': {'r': 0.9960784316062927, 'g': 0.9764705896377563, 'b': 0.7647058963775635, 'a': 1}}; - --categories-color-yellow-200: {'6:0': {'r': 0.9960784316062927, 'g': 0.9411764740943909, 'b': 0.5411764979362488, 'a': 1}}; - --categories-color-yellow-300: {'6:0': {'r': 0.9921568632125854, 'g': 0.8784313797950745, 'b': 0.27843138575553894, 'a': 1}}; - --categories-color-yellow-400: {'6:0': {'r': 0.9803921580314636, 'g': 0.800000011920929, 'b': 0.08235294371843338, 'a': 1}}; - --categories-color-yellow-500: {'6:0': {'r': 0.9176470637321472, 'g': 0.7019608020782471, 'b': 0.0313725508749485, 'a': 1}}; - --categories-color-yellow-600: {'6:0': {'r': 0.7921568751335144, 'g': 0.5411764979362488, 'b': 0.01568627543747425, 'a': 1}}; - --categories-color-yellow-700: {'6:0': {'r': 0.6313725709915161, 'g': 0.3843137323856354, 'b': 0.027450980618596077, 'a': 1}}; - --categories-color-yellow-800: {'6:0': {'r': 0.5215686559677124, 'g': 0.3019607961177826, 'b': 0.054901961237192154, 'a': 1}}; - --categories-color-yellow-900: {'6:0': {'r': 0.4431372582912445, 'g': 0.24705882370471954, 'b': 0.07058823853731155, 'a': 1}}; - --categories-color-yellow-950: {'6:0': {'r': 0.25882354378700256, 'g': 0.125490203499794, 'b': 0.0235294122248888, 'a': 1}}; - --categories-color-general-background: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5102'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5103'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5102'}}; - --categories-color-general-foreground: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:111'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:101'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30321'}}; - --categories-color-general-primary: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:110'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:102'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30320'}}; - --categories-color-general-primary-foreground: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:101'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:111'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30311'}}; - --categories-color-general-secondary: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:102'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:109'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30312'}}; - --categories-color-general-secondary-foreground: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:110'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:102'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30320'}}; - --categories-color-general-accent: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:102'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:110'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30312'}}; - --categories-color-general-accent-foreground: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:110'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:102'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30320'}}; - --categories-color-general-muted: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:102'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:110'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30312'}}; - --categories-color-general-muted-foreground: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:106'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:105'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30316'}}; - --categories-color-general-destructive: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:16:1997'}, '618:1': {'r': 0.6196078658103943, 'g': 0.250980406999588, 'b': 0.25882354378700256, 'a': 1}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:16:1997'}}; - --categories-color-general-border: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:103'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:108'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30313'}}; - --categories-color-general-input: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5102'}, '618:1': {'r': 1, 'g': 1, 'b': 1, 'a': 0.05000000074505806}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5102'}}; - --categories-color-card-card: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5102'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:110'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5102'}}; - --categories-color-card-card-foreground: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:111'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5102'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30321'}}; - --categories-color-popover-popover: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5103'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5102'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5103'}}; - --categories-color-popover-popover-foreground: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5102'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5103'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5102'}}; - --categories-color-unofficial-foreground-alt: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:108'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:104'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30318'}}; - --categories-color-unofficial-body-background: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5102'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:111'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5102'}}; - --categories-color-unofficial-destructive-border: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:16:1996'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:16:1996'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:16:1996'}}; - --categories-color-unofficial-destructive-subtle: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:16:1991'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:16:2001'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:16:1991'}}; - --categories-color-unofficial-contrast-(deprecated): {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5103'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5102'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5103'}}; - --categories-color-unofficial-backdrop: {'618:0': {'r': 0, 'g': 0, 'b': 0, 'a': 0.6000000238418579}, '618:1': {'r': 0, 'g': 0, 'b': 0, 'a': 0.6000000238418579}, '847:4': {'r': 0.20000000298023224, 'g': 0.2549019753932953, 'b': 0.3333333432674408, 'a': 0.6000000238418579}}; - --categories-color-unofficial-mid-(deprecated): {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:106'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:106'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30316'}}; - --categories-color-unofficial-mid-alt: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:107'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:105'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30317'}}; - --categories-color-unofficial-destructive-foreground: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:16:1997'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:16:1995'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:16:1997'}}; - --categories-color-unofficial-ghost-foreground: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:108'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:103'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30318'}}; - --categories-color-unofficial-ghost: {'618:0': {'r': 1, 'g': 1, 'b': 1, 'a': 9.999999747378752e-05}, '618:1': {'r': 1, 'g': 1, 'b': 1, 'a': 9.999999747378752e-05}, '847:4': {'r': 1, 'g': 1, 'b': 1, 'a': 9.999999747378752e-05}}; - --categories-color-unofficial-ghost-hover: {'618:0': {'r': 0, 'g': 0, 'b': 0, 'a': 0.05000000074505806}, '618:1': {'r': 1, 'g': 1, 'b': 1, 'a': 0.10000000149011612}, '847:4': {'r': 0, 'g': 0, 'b': 0, 'a': 0.05000000074505806}}; - --categories-color-unofficial-primary-hover: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:108'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:104'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30318'}}; - --categories-color-unofficial-secondary-hover: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:101'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:110'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30311'}}; - --categories-color-unofficial-outline: {'618:0': {'r': 1, 'g': 1, 'b': 1, 'a': 0.10000000149011612}, '618:1': {'r': 1, 'g': 1, 'b': 1, 'a': 0.05000000074505806}, '847:4': {'r': 1, 'g': 1, 'b': 1, 'a': 0.10000000149011612}}; - --categories-color-unofficial-outline-hover: {'618:0': {'r': 0, 'g': 0, 'b': 0, 'a': 0.0333000011742115}, '618:1': {'r': 1, 'g': 1, 'b': 1, 'a': 0.10000000149011612}, '847:4': {'r': 0.20000000298023224, 'g': 0.2549019753932953, 'b': 0.3333333432674408, 'a': 0.0333000011742115}}; - --categories-color-unofficial-outline-active: {'618:0': {'r': 0, 'g': 0, 'b': 0, 'a': 0.05000000074505806}, '618:1': {'r': 1, 'g': 1, 'b': 1, 'a': 0.15000000596046448}, '847:4': {'r': 0.20000000298023224, 'g': 0.2549019753932953, 'b': 0.3333333432674408, 'a': 0.05000000074505806}}; - --categories-color-unofficial-accent-0: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:101'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:111'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30311'}}; - --categories-color-unofficial-accent-2: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:103'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:109'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30313'}}; - --categories-color-unofficial-accent-3: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:104'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:108'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30314'}}; - --categories-color-unofficial-border-0: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:101'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:111'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30311'}}; - --categories-color-unofficial-border-1: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:102'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:110'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30312'}}; - --categories-color-unofficial-border-3: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:104'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:108'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30314'}}; - --categories-color-unofficial-border-4: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:105'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:107'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30315'}}; - --categories-color-unofficial-border-5: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:106'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:106'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30316'}}; - --categories-color-focus-ring: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:104'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:108'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30314'}}; - --categories-color-sidebar-sidebar: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:101'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:111'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30312'}}; - --categories-color-sidebar-sidebar-foreground: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:108'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:104'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30318'}}; - --categories-color-sidebar-sidebar-accent: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:102'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:110'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30313'}}; - --categories-color-sidebar-sidebar-accent-foreground: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:110'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:102'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30318'}}; - --categories-color-sidebar-sidebar-primary: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:110'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:101'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30320'}}; - --categories-color-sidebar-sidebar-primary-foreground: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:101'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:110'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30311'}}; - --categories-color-sidebar-sidebar-border: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:103'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:109'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30313'}}; - --categories-color-sidebar-sidebar-ring: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:104'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:108'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30314'}}; - --categories-color-sidebar-unofficial-sidebar-muted: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:106'}, '618:1': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:6:106'}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30316'}}; - --categories-color-focus-ring-error: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:16:1994'}, '618:1': {'r': 0.4274509847164154, 'g': 0.18039216101169586, 'b': 0.18431372940540314, 'a': 1}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:16:1994'}}; - --categories-color-chart-legacy-chart-1: {'618:0': {'r': 0.9607843160629272, 'g': 0.29019609093666077, 'b': 0, 'a': 1}, '618:1': {'r': 0.0784313753247261, 'g': 0.27843138575553894, 'b': 0.9019607901573181, 'a': 1}, '847:4': {'r': 0.12156862765550613, 'g': 0.46666666865348816, 'b': 0.7058823704719543, 'a': 1}}; - --categories-color-chart-legacy-chart-2: {'618:0': {'r': 0, 'g': 0.5882353186607361, 'b': 0.5372549295425415, 'a': 1}, '618:1': {'r': 0, 'g': 0.7372549176216125, 'b': 0.4901960790157318, 'a': 1}, '847:4': {'r': 1, 'g': 0.49803921580314636, 'b': 0.054901961237192154, 'a': 1}}; - --categories-color-chart-legacy-chart-3: {'618:0': {'r': 0.062745101749897, 'g': 0.30588236451148987, 'b': 0.3921568691730499, 'a': 1}, '618:1': {'r': 0.9921568632125854, 'g': 0.6039215922355652, 'b': 0, 'a': 1}, '847:4': {'r': 0.1725490242242813, 'g': 0.6274510025978088, 'b': 0.1725490242242813, 'a': 1}}; - --categories-color-chart-legacy-chart-4: {'618:0': {'r': 1, 'g': 0.7254902124404907, 'b': 0, 'a': 1}, '618:1': {'r': 0.6784313917160034, 'g': 0.27450981736183167, 'b': 1, 'a': 1}, '847:4': {'r': 0.8392156958580017, 'g': 0.15294118225574493, 'b': 0.1568627506494522, 'a': 1}}; - --categories-color-chart-legacy-chart-5: {'618:0': {'r': 0.9960784316062927, 'g': 0.6039215922355652, 'b': 0, 'a': 1}, '618:1': {'r': 1, 'g': 0.125490203499794, 'b': 0.33725491166114807, 'a': 1}, '847:4': {'r': 0.5803921818733215, 'g': 0.40392157435417175, 'b': 0.7411764860153198, 'a': 1}}; - --categories-color-chart-area-orange-fill: {'618:0': {'r': 0.9920479655265808, 'g': 0.8146340250968933, 'b': 0.6118749976158142, 'a': 0.699999988079071}, '618:1': {'r': 0.4588235318660736, 'g': 0.34117648005485535, 'b': 0.21960784494876862, 'a': 0.699999988079071}, '847:4': {'r': 0.9920479655265808, 'g': 0.8146340250968933, 'b': 0.6118749976158142, 'a': 0.699999988079071}}; - --categories-color-chart-area-orange-fill-2: {'618:0': {'r': 0.9708533883094788, 'g': 0.690407395362854, 'b': 0.49409517645835876, 'a': 0.699999988079071}, '618:1': {'r': 0.4627451002597809, 'g': 0.21960784494876862, 'b': 0.054901961237192154, 'a': 0.699999988079071}, '847:4': {'r': 0.9708533883094788, 'g': 0.690407395362854, 'b': 0.49409517645835876, 'a': 0.699999988079071}}; - --categories-color-chart-area-orange-stroke: {'618:0': {'r': 1, 'g': 0.7215686440467834, 'b': 0.4156862795352936, 'a': 1}, '618:1': {'r': 1, 'g': 0.7215686440467834, 'b': 0.4156862795352936, 'a': 1}, '847:4': {'r': 1, 'g': 0.7215686440467834, 'b': 0.4156862795352936, 'a': 1}}; - --categories-color-chart-area-orange-stroke-2: {'618:0': {'r': 1, 'g': 0.4117647111415863, 'b': 0, 'a': 1}, '618:1': {'r': 1, 'g': 0.4745098054409027, 'b': 0.08235294371843338, 'a': 1}, '847:4': {'r': 1, 'g': 0.4117647111415863, 'b': 0, 'a': 1}}; - --categories-color-chart-area-blue-fill: {'618:0': {'r': 0.7496619820594788, 'g': 0.8687251806259155, 'b': 1, 'a': 0.699999988079071}, '618:1': {'r': 0.27843138575553894, 'g': 0.364705890417099, 'b': 0.4588235318660736, 'a': 0.699999988079071}, '847:4': {'r': 0.7496619820594788, 'g': 0.8687251806259155, 'b': 1, 'a': 0.699999988079071}}; - --categories-color-chart-area-blue-stroke: {'618:0': {'r': 0.5568627715110779, 'g': 0.772549033164978, 'b': 1, 'a': 1}, '618:1': {'r': 0.5568627715110779, 'g': 0.772549033164978, 'b': 1, 'a': 1}, '847:4': {'r': 0.5568627715110779, 'g': 0.772549033164978, 'b': 1, 'a': 1}}; - --categories-color-chart-area-blue-fill-2: {'618:0': {'r': 0.6666666865348816, 'g': 0.800000011920929, 'b': 1, 'a': 0.699999988079071}, '618:1': {'r': 0.12156862765550613, 'g': 0.2549019753932953, 'b': 0.4627451002597809, 'a': 0.699999988079071}, '847:4': {'r': 0.6666666865348816, 'g': 0.800000011920929, 'b': 1, 'a': 0.699999988079071}}; - --categories-color-chart-area-blue-stroke-2: {'618:0': {'r': 0.24705882370471954, 'g': 0.5529412031173706, 'b': 1, 'a': 1}, '618:1': {'r': 0.32549020648002625, 'g': 0.6078431606292725, 'b': 1, 'a': 1}, '847:4': {'r': 0.24705882370471954, 'g': 0.5529412031173706, 'b': 1, 'a': 1}}; - --categories-color-chart-area-green-fill: {'618:0': {'r': 0.7252106666564941, 'g': 0.9840070009231567, 'b': 0.822259247303009, 'a': 0.699999988079071}, '618:1': {'r': 0.24705882370471954, 'g': 0.4313725531101227, 'b': 0.3176470696926117, 'a': 0.699999988079071}, '847:4': {'r': 0.7252106666564941, 'g': 0.9840070009231567, 'b': 0.822259247303009, 'a': 0.699999988079071}}; - --categories-color-chart-area-green-stroke: {'618:0': {'r': 0.48235294222831726, 'g': 0.9450980424880981, 'b': 0.6687395572662354, 'a': 1}, '618:1': {'r': 0.48235294222831726, 'g': 0.9450980424880981, 'b': 0.658823549747467, 'a': 1}, '847:4': {'r': 0.48235294222831726, 'g': 0.9450980424880981, 'b': 0.6687395572662354, 'a': 1}}; - --categories-color-chart-area-green-fill-2: {'618:0': {'r': 0.5098943710327148, 'g': 0.8876308798789978, 'b': 0.660988986492157, 'a': 0.699999988079071}, '618:1': {'r': 0.054901961237192154, 'g': 0.3686274588108063, 'b': 0.18039216101169586, 'a': 0.699999988079071}, '847:4': {'r': 0.5098943710327148, 'g': 0.8876308798789978, 'b': 0.660988986492157, 'a': 0.699999988079071}}; - --categories-color-chart-area-green-stroke-2: {'618:0': {'r': 0.09803921729326248, 'g': 0.8196078538894653, 'b': 0.38823530077934265, 'a': 1}, '618:1': {'r': 0.09803921729326248, 'g': 0.8196078538894653, 'b': 0.38823530077934265, 'a': 1}, '847:4': {'r': 0.09803921729326248, 'g': 0.8196078538894653, 'b': 0.38823530077934265, 'a': 1}}; - --categories-color-chart-area-rose-fill: {'618:0': {'r': 1, 'g': 0.8509804010391235, 'b': 0.8705882430076599, 'a': 0.699999988079071}, '618:1': {'r': 0.4588235318660736, 'g': 0.30588236451148987, 'b': 0.32549020648002625, 'a': 0.699999988079071}, '847:4': {'r': 1, 'g': 0.8509804010391235, 'b': 0.8705882430076599, 'a': 0.699999988079071}}; - --categories-color-chart-area-rose-stroke: {'618:0': {'r': 1, 'g': 0.6313725709915161, 'b': 0.6784313917160034, 'a': 1}, '618:1': {'r': 1, 'g': 0.6313725709915161, 'b': 0.6784313917160034, 'a': 1}, '847:4': {'r': 1, 'g': 0.6313725709915161, 'b': 0.6784313917160034, 'a': 1}}; - --categories-color-chart-area-rose-fill-2: {'618:0': {'r': 0.9578414559364319, 'g': 0.5668655633926392, 'b': 0.6601600646972656, 'a': 0.699999988079071}, '618:1': {'r': 0.45490196347236633, 'g': 0.10588235408067703, 'b': 0.1882352977991104, 'a': 0.699999988079071}, '847:4': {'r': 0.9578414559364319, 'g': 0.5668655633926392, 'b': 0.6601600646972656, 'a': 0.699999988079071}}; - --categories-color-chart-area-rose-stroke-2: {'618:0': {'r': 1, 'g': 0.30980393290519714, 'b': 0.4745098054409027, 'a': 1}, '618:1': {'r': 1, 'g': 0.27450981736183167, 'b': 0.43921568989753723, 'a': 1}, '847:4': {'r': 1, 'g': 0.30980393290519714, 'b': 0.4745098054409027, 'a': 1}}; - --categories-color-chart-area-teal-fill: {'618:0': {'r': 0.6622663736343384, 'g': 0.9549305438995361, 'b': 0.9112495183944702, 'a': 0.699999988079071}, '618:1': {'r': 0.2528286874294281, 'g': 0.5769955515861511, 'b': 0.5327908992767334, 'a': 0.699999988079071}, '847:4': {'r': 0.6622663736343384, 'g': 0.9549305438995361, 'b': 0.9112495183944702, 'a': 0.699999988079071}}; - --categories-color-chart-area-teal-stroke: {'618:0': {'r': 0.27450981736183167, 'g': 0.929411768913269, 'b': 0.8352941274642944, 'a': 1}, '618:1': {'r': 0.27450981736183167, 'g': 0.929411768913269, 'b': 0.8352941274642944, 'a': 1}, '847:4': {'r': 0.27450981736183167, 'g': 0.929411768913269, 'b': 0.8352941274642944, 'a': 1}}; - --categories-color-chart-area-teal-fill-2: {'618:0': {'r': 0.4880538582801819, 'g': 0.9056999683380127, 'b': 0.8611510396003723, 'a': 0.699999988079071}, '618:1': {'r': 0.054901961237192154, 'g': 0.3490196168422699, 'b': 0.3176470696926117, 'a': 0.699999988079071}, '847:4': {'r': 0.4880538582801819, 'g': 0.9056999683380127, 'b': 0.8611510396003723, 'a': 0.699999988079071}}; - --categories-color-chart-area-teal-stroke-2: {'618:0': {'r': 0.027450980618596077, 'g': 0.7529411911964417, 'b': 0.6745098233222961, 'a': 1}, '618:1': {'r': 0.10980392247438431, 'g': 0.8117647171020508, 'b': 0.7254902124404907, 'a': 1}, '847:4': {'r': 0.027450980618596077, 'g': 0.7529411911964417, 'b': 0.6745098233222961, 'a': 1}}; - --categories-color-chart-area-purple-fill: {'618:0': {'r': 0.9411764740943909, 'g': 0.8784313797950745, 'b': 1, 'a': 0.699999988079071}, '618:1': {'r': 0.3960784375667572, 'g': 0.3333333432674408, 'b': 0.4627451002597809, 'a': 0.699999988079071}, '847:4': {'r': 0.9411764740943909, 'g': 0.8784313797950745, 'b': 1, 'a': 0.699999988079071}}; - --categories-color-chart-area-purple-stroke: {'618:0': {'r': 0.8549019694328308, 'g': 0.6980392336845398, 'b': 1, 'a': 1}, '618:1': {'r': 0.8549019694328308, 'g': 0.6980392336845398, 'b': 1, 'a': 1}, '847:4': {'r': 0.8549019694328308, 'g': 0.6980392336845398, 'b': 1, 'a': 1}}; - --categories-color-chart-area-purple-fill-2: {'618:0': {'r': 0.8705882430076599, 'g': 0.7098039388656616, 'b': 1, 'a': 0.699999988079071}, '618:1': {'r': 0.32549020648002625, 'g': 0.16470588743686676, 'b': 0.46666666865348816, 'a': 0.699999988079071}, '847:4': {'r': 0.8705882430076599, 'g': 0.7098039388656616, 'b': 1, 'a': 0.699999988079071}}; - --categories-color-chart-area-purple-stroke-2: {'618:0': {'r': 0.7764706015586853, 'g': 0.4941176474094391, 'b': 1, 'a': 1}, '618:1': {'r': 0.6627451181411743, 'g': 0.4156862795352936, 'b': 0.8666666746139526, 'a': 1}, '847:4': {'r': 0.7764706015586853, 'g': 0.4941176474094391, 'b': 1, 'a': 1}}; - --categories-color-obra-shadn-docs-obra-shadcn-ui-docs-1: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:16:2015'}, '618:1': {'r': 0.06697625666856766, 'g': 0.08797097951173782, 'b': 0.15845328569412231, 'a': 1}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5102'}}; - --categories-color-chart-area-amber-fill: {'618:0': {'r': 1, 'g': 0.929411768913269, 'b': 0.6745098233222961, 'a': 0.699999988079071}, '618:1': {'r': 0.45490196347236633, 'g': 0.3843137323856354, 'b': 0.12941177189350128, 'a': 0.699999988079071}, '847:4': {'r': 1, 'g': 0.929411768913269, 'b': 0.6745098233222961, 'a': 0.699999988079071}}; - --categories-color-obra-shadn-docs-obra-shadcn-ui-docs-2: {'618:0': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:534:30510'}, '618:1': {'r': 0.12658406794071198, 'g': 0.11423555761575699, 'b': 0.10479257255792618, 'a': 1}, '847:4': {'type': 'VARIABLE_ALIAS', 'id': 'VariableID:55:5102'}}; - --categories-color-chart-area-amber-stroke: {'618:0': {'r': 1, 'g': 0.8235294222831726, 'b': 0.1882352977991104, 'a': 1}, '618:1': {'r': 1, 'g': 0.8235294222831726, 'b': 0.1882352977991104, 'a': 1}, '847:4': {'r': 1, 'g': 0.8235294222831726, 'b': 0.1882352977991104, 'a': 1}}; - --categories-color-chart-area-amber-fill-2: {'618:0': {'r': 0.9960784316062927, 'g': 0.8392156958580017, 'b': 0.6000000238418579, 'a': 0.699999988079071}, '618:1': {'r': 0.45098039507865906, 'g': 0.29411765933036804, 'b': 0.054901961237192154, 'a': 0.699999988079071}, '847:4': {'r': 0.9960784316062927, 'g': 0.8392156958580017, 'b': 0.6000000238418579, 'a': 0.699999988079071}}; - --categories-color-chart-area-amber-stroke-2: {'618:0': {'r': 0.9960784316062927, 'g': 0.6039215922355652, 'b': 0, 'a': 1}, '618:1': {'r': 1, 'g': 0.6470588445663452, 'b': 0.03921568766236305, 'a': 1}, '847:4': {'r': 0.9960784316062927, 'g': 0.6039215922355652, 'b': 0, 'a': 1}}; - --categories-color-chart-static-blue-1: {'618:0': {'r': 0.5568627715110779, 'g': 0.772549033164978, 'b': 1, 'a': 1}, '618:1': {'r': 0.5568627715110779, 'g': 0.772549033164978, 'b': 1, 'a': 1}, '847:4': {'r': 0.5568627715110779, 'g': 0.772549033164978, 'b': 1, 'a': 1}}; - --categories-color-chart-static-rose-1: {'618:0': {'r': 1, 'g': 0.6313725709915161, 'b': 0.6784313917160034, 'a': 1}, '618:1': {'r': 1, 'g': 0.6313725709915161, 'b': 0.6784313917160034, 'a': 1}, '847:4': {'r': 1, 'g': 0.6313725709915161, 'b': 0.6784313917160034, 'a': 1}}; - --categories-color-chart-static-rose-2: {'618:0': {'r': 1, 'g': 0.125490203499794, 'b': 0.33725491166114807, 'a': 1}, '618:1': {'r': 1, 'g': 0.125490203499794, 'b': 0.33725491166114807, 'a': 1}, '847:4': {'r': 1, 'g': 0.125490203499794, 'b': 0.33725491166114807, 'a': 1}}; - --categories-color-chart-static-rose-3: {'618:0': {'r': 0.9254902005195618, 'g': 0, 'b': 0.24705882370471954, 'a': 1}, '618:1': {'r': 0.9254902005195618, 'g': 0, 'b': 0.24705882370471954, 'a': 1}, '847:4': {'r': 0.9254902005195618, 'g': 0, 'b': 0.24705882370471954, 'a': 1}}; - --categories-color-chart-static-rose-4: {'618:0': {'r': 0.7803921699523926, 'g': 0, 'b': 0.21176470816135406, 'a': 1}, '618:1': {'r': 0.7803921699523926, 'g': 0, 'b': 0.21176470816135406, 'a': 1}, '847:4': {'r': 0.7803921699523926, 'g': 0, 'b': 0.21176470816135406, 'a': 1}}; - --categories-color-chart-static-rose-5: {'618:0': {'r': 0.6470588445663452, 'g': 0, 'b': 0.21176470816135406, 'a': 1}, '618:1': {'r': 0.6470588445663452, 'g': 0, 'b': 0.21176470816135406, 'a': 1}, '847:4': {'r': 0.6470588445663452, 'g': 0, 'b': 0.21176470816135406, 'a': 1}}; - --categories-color-chart-static-purple-1: {'618:0': {'r': 0.8549019694328308, 'g': 0.6980392336845398, 'b': 1, 'a': 1}, '618:1': {'r': 0.8549019694328308, 'g': 0.6980392336845398, 'b': 1, 'a': 1}, '847:4': {'r': 0.8549019694328308, 'g': 0.6980392336845398, 'b': 1, 'a': 1}}; - --categories-color-chart-static-purple-2: {'618:0': {'r': 0.6784313917160034, 'g': 0.27450981736183167, 'b': 1, 'a': 1}, '618:1': {'r': 0.6784313917160034, 'g': 0.27450981736183167, 'b': 1, 'a': 1}, '847:4': {'r': 0.6784313917160034, 'g': 0.27450981736183167, 'b': 1, 'a': 1}}; - --categories-color-chart-static-purple-3: {'618:0': {'r': 0.5960784554481506, 'g': 0.062745101749897, 'b': 0.9803921580314636, 'a': 1}, '618:1': {'r': 0.5960784554481506, 'g': 0.062745101749897, 'b': 0.9803921580314636, 'a': 1}, '847:4': {'r': 0.5960784554481506, 'g': 0.062745101749897, 'b': 0.9803921580314636, 'a': 1}}; - --categories-color-chart-static-purple-4: {'618:0': {'r': 0.5098039507865906, 'g': 0, 'b': 0.8588235378265381, 'a': 1}, '618:1': {'r': 0.5098039507865906, 'g': 0, 'b': 0.8588235378265381, 'a': 1}, '847:4': {'r': 0.5098039507865906, 'g': 0, 'b': 0.8588235378265381, 'a': 1}}; - --categories-color-chart-static-purple-5: {'618:0': {'r': 0.4313725531101227, 'g': 0.06666667014360428, 'b': 0.6901960968971252, 'a': 1}, '618:1': {'r': 0.4313725531101227, 'g': 0.06666667014360428, 'b': 0.6901960968971252, 'a': 1}, '847:4': {'r': 0.4313725531101227, 'g': 0.06666667014360428, 'b': 0.6901960968971252, 'a': 1}}; - --categories-color-chart-static-orange-1: {'618:0': {'r': 1, 'g': 0.7215686440467834, 'b': 0.4156862795352936, 'a': 1}, '618:1': {'r': 1, 'g': 0.7215686440467834, 'b': 0.4156862795352936, 'a': 1}, '847:4': {'r': 1, 'g': 0.7215686440467834, 'b': 0.4156862795352936, 'a': 1}}; - --categories-color-chart-static-orange-2: {'618:0': {'r': 1, 'g': 0.4117647111415863, 'b': 0, 'a': 1}, '618:1': {'r': 1, 'g': 0.4117647111415863, 'b': 0, 'a': 1}, '847:4': {'r': 1, 'g': 0.4117647111415863, 'b': 0, 'a': 1}}; - --categories-color-chart-static-orange-3: {'618:0': {'r': 0.9607843160629272, 'g': 0.29019609093666077, 'b': 0, 'a': 1}, '618:1': {'r': 0.9607843160629272, 'g': 0.29019609093666077, 'b': 0, 'a': 1}, '847:4': {'r': 0.9607843160629272, 'g': 0.29019609093666077, 'b': 0, 'a': 1}}; - --categories-color-chart-static-orange-4: {'618:0': {'r': 0.7921568751335144, 'g': 0.2078431397676468, 'b': 0, 'a': 1}, '618:1': {'r': 0.7921568751335144, 'g': 0.2078431397676468, 'b': 0, 'a': 1}, '847:4': {'r': 0.7921568751335144, 'g': 0.2078431397676468, 'b': 0, 'a': 1}}; - --categories-color-chart-static-orange-5: {'618:0': {'r': 0.6235294342041016, 'g': 0.1764705926179886, 'b': 0, 'a': 1}, '618:1': {'r': 0.6235294342041016, 'g': 0.1764705926179886, 'b': 0, 'a': 1}, '847:4': {'r': 0.6235294342041016, 'g': 0.1764705926179886, 'b': 0, 'a': 1}}; - --categories-color-chart-static-teal-1: {'618:0': {'r': 0.27450981736183167, 'g': 0.929411768913269, 'b': 0.8352941274642944, 'a': 1}, '618:1': {'r': 0.27450981736183167, 'g': 0.929411768913269, 'b': 0.8352941274642944, 'a': 1}, '847:4': {'r': 0.27450981736183167, 'g': 0.929411768913269, 'b': 0.8352941274642944, 'a': 1}}; - --categories-color-chart-static-teal-2: {'618:0': {'r': 0, 'g': 0.7333333492279053, 'b': 0.6549019813537598, 'a': 1}, '618:1': {'r': 0, 'g': 0.7333333492279053, 'b': 0.6549019813537598, 'a': 1}, '847:4': {'r': 0, 'g': 0.7333333492279053, 'b': 0.6549019813537598, 'a': 1}}; - --categories-color-chart-static-teal-3: {'618:0': {'r': 0, 'g': 0.5882353186607361, 'b': 0.5372549295425415, 'a': 1}, '618:1': {'r': 0, 'g': 0.5882353186607361, 'b': 0.5372549295425415, 'a': 1}, '847:4': {'r': 0, 'g': 0.5882353186607361, 'b': 0.5372549295425415, 'a': 1}}; - --categories-color-chart-static-teal-4: {'618:0': {'r': 0, 'g': 0.47058823704719543, 'b': 0.43529412150382996, 'a': 1}, '618:1': {'r': 0, 'g': 0.47058823704719543, 'b': 0.43529412150382996, 'a': 1}, '847:4': {'r': 0, 'g': 0.47058823704719543, 'b': 0.43529412150382996, 'a': 1}}; - --categories-color-chart-static-teal-5: {'618:0': {'r': 0, 'g': 0.37254902720451355, 'b': 0.3529411852359772, 'a': 1}, '618:1': {'r': 0, 'g': 0.37254902720451355, 'b': 0.3529411852359772, 'a': 1}, '847:4': {'r': 0, 'g': 0.37254902720451355, 'b': 0.3529411852359772, 'a': 1}}; - --categories-color-chart-static-blue-2: {'618:0': {'r': 0.16862745583057404, 'g': 0.49803921580314636, 'b': 1, 'a': 1}, '618:1': {'r': 0.16862745583057404, 'g': 0.49803921580314636, 'b': 1, 'a': 1}, '847:4': {'r': 0.16862745583057404, 'g': 0.49803921580314636, 'b': 1, 'a': 1}}; - --categories-color-chart-static-blue-3: {'618:0': {'r': 0.08235294371843338, 'g': 0.364705890417099, 'b': 0.9882352948188782, 'a': 1}, '618:1': {'r': 0.08235294371843338, 'g': 0.364705890417099, 'b': 0.9882352948188782, 'a': 1}, '847:4': {'r': 0.08235294371843338, 'g': 0.364705890417099, 'b': 0.9882352948188782, 'a': 1}}; - --categories-color-chart-static-blue-4: {'618:0': {'r': 0.0784313753247261, 'g': 0.27843138575553894, 'b': 0.9019607901573181, 'a': 1}, '618:1': {'r': 0.0784313753247261, 'g': 0.27843138575553894, 'b': 0.9019607901573181, 'a': 1}, '847:4': {'r': 0.0784313753247261, 'g': 0.27843138575553894, 'b': 0.9019607901573181, 'a': 1}}; - --categories-color-chart-static-blue-5: {'618:0': {'r': 0.09803921729326248, 'g': 0.23529411852359772, 'b': 0.7215686440467834, 'a': 1}, '618:1': {'r': 0.09803921729326248, 'g': 0.23529411852359772, 'b': 0.7215686440467834, 'a': 1}, '847:4': {'r': 0.09803921729326248, 'g': 0.23529411852359772, 'b': 0.7215686440467834, 'a': 1}}; - --categories-color-chart-static-amber-1: {'618:0': {'r': 1, 'g': 0.8235294222831726, 'b': 0.1882352977991104, 'a': 1}, '618:1': {'r': 1, 'g': 0.8235294222831726, 'b': 0.1882352977991104, 'a': 1}, '847:4': {'r': 1, 'g': 0.8235294222831726, 'b': 0.1882352977991104, 'a': 1}}; - --categories-color-chart-static-amber-2: {'618:0': {'r': 0.9960784316062927, 'g': 0.6039215922355652, 'b': 0, 'a': 1}, '618:1': {'r': 0.9960784316062927, 'g': 0.6039215922355652, 'b': 0, 'a': 1}, '847:4': {'r': 0.9960784316062927, 'g': 0.6039215922355652, 'b': 0, 'a': 1}}; - --categories-color-chart-static-amber-3: {'618:0': {'r': 0.8823529481887817, 'g': 0.4431372582912445, 'b': 0, 'a': 1}, '618:1': {'r': 0.8823529481887817, 'g': 0.4431372582912445, 'b': 0, 'a': 1}, '847:4': {'r': 0.8823529481887817, 'g': 0.4431372582912445, 'b': 0, 'a': 1}}; - --categories-color-chart-static-amber-4: {'618:0': {'r': 0.7333333492279053, 'g': 0.3019607961177826, 'b': 0, 'a': 1}, '618:1': {'r': 0.7333333492279053, 'g': 0.3019607961177826, 'b': 0, 'a': 1}, '847:4': {'r': 0.7333333492279053, 'g': 0.3019607961177826, 'b': 0, 'a': 1}}; - --categories-color-chart-static-amber-5: {'618:0': {'r': 0.5921568870544434, 'g': 0.23529411852359772, 'b': 0, 'a': 1}, '618:1': {'r': 0.5921568870544434, 'g': 0.23529411852359772, 'b': 0, 'a': 1}, '847:4': {'r': 0.5921568870544434, 'g': 0.23529411852359772, 'b': 0, 'a': 1}}; - --categories-color-chart-static-green-1: {'618:0': {'r': 0.48235294222831726, 'g': 0.9450980424880981, 'b': 0.658823549747467, 'a': 1}, '618:1': {'r': 0.48235294222831726, 'g': 0.9450980424880981, 'b': 0.658823549747467, 'a': 1}, '847:4': {'r': 0.48235294222831726, 'g': 0.9450980424880981, 'b': 0.658823549747467, 'a': 1}}; - --categories-color-chart-static-green-2: {'618:0': {'r': 0, 'g': 0.7882353067398071, 'b': 0.3176470696926117, 'a': 1}, '618:1': {'r': 0, 'g': 0.7882353067398071, 'b': 0.3176470696926117, 'a': 1}, '847:4': {'r': 0, 'g': 0.7882353067398071, 'b': 0.3176470696926117, 'a': 1}}; - --categories-color-chart-static-green-3: {'618:0': {'r': 0, 'g': 0.6509804129600525, 'b': 0.24313725531101227, 'a': 1}, '618:1': {'r': 0, 'g': 0.6509804129600525, 'b': 0.24313725531101227, 'a': 1}, '847:4': {'r': 0, 'g': 0.6509804129600525, 'b': 0.24313725531101227, 'a': 1}}; - --categories-color-chart-static-green-4: {'618:0': {'r': 0, 'g': 0.5098039507865906, 'b': 0.21176470816135406, 'a': 1}, '618:1': {'r': 0, 'g': 0.5098039507865906, 'b': 0.21176470816135406, 'a': 1}, '847:4': {'r': 0, 'g': 0.5098039507865906, 'b': 0.21176470816135406, 'a': 1}}; - --categories-color-chart-static-green-5: {'618:0': {'r': 0.003921568859368563, 'g': 0.4000000059604645, 'b': 0.1882352977991104, 'a': 1}, '618:1': {'r': 0.003921568859368563, 'g': 0.4000000059604645, 'b': 0.1882352977991104, 'a': 1}, '847:4': {'r': 0.003921568859368563, 'g': 0.4000000059604645, 'b': 0.1882352977991104, 'a': 1}}; - --categories-color-2xs-color: {'848:1': {'r': 0, 'g': 0, 'b': 0, 'a': 0.05000000074505806}, '851:1': {'r': 0, 'g': 0, 'b': 0, 'a': 0.05000000074505806}}; - --categories-color-xs-color: {'848:1': {'r': 0, 'g': 0, 'b': 0, 'a': 0.05000000074505806}, '851:1': {'r': 0, 'g': 0, 'b': 0, 'a': 0.05000000074505806}}; - --categories-color-sm-color: {'848:1': {'r': 0, 'g': 0, 'b': 0, 'a': 0.10000000149011612}, '851:1': {'r': 0, 'g': 0, 'b': 0, 'a': 0.10000000149011612}}; - --categories-color-md-color: {'848:1': {'r': 0, 'g': 0, 'b': 0, 'a': 0.10000000149011612}, '851:1': {'r': 0, 'g': 0, 'b': 0, 'a': 0.10000000149011612}}; - --categories-color-lg-color: {'848:1': {'r': 0, 'g': 0, 'b': 0, 'a': 0.10000000149011612}, '851:1': {'r': 0, 'g': 0, 'b': 0, 'a': 0.10000000149011612}}; - --categories-color-xl-color: {'848:1': {'r': 0, 'g': 0, 'b': 0, 'a': 0.10000000149011612}, '851:1': {'r': 0, 'g': 0, 'b': 0, 'a': 0.10000000149011612}}; - --categories-color-2xl-color: {'848:1': {'r': 0, 'g': 0, 'b': 0, 'a': 0.25}, '851:1': {'r': 0, 'g': 0, 'b': 0, 'a': 0.25}}; - --categories-typography-heading-1: None; - --categories-typography-heading-2: None; - --categories-typography-paragraph-small-regular: None; - --categories-typography-paragraph-small-bold: None; - --categories-typography-heading-3: None; - --categories-typography-paragraph-bold: None; - --categories-typography-paragraph-regular: None; - --categories-typography-paragraph-mini-regular: None; - --categories-typography-heading-4: None; - --categories-typography-monospaced: None; - --categories-typography-paragraph-medium: None; - --categories-typography-paragraph-small-medium: None; - --categories-typography-paragraph-mini-bold: None; - --categories-typography-paragraph-mini-medium: None; - --categories-effect-shadow-sm: None; - --categories-effect-shadow-lg: None; - --categories-effect-shadow-2xs: None; - --categories-effect-shadow-xs: None; - --categories-effect-shadow-md: None; - --categories-effect-shadow-xl: None; - --categories-effect-shadow-2xl: None; - --categories-effect-focus-ring: None; - --categories-effect-focus-ring-error: None; - --categories-effect-focus-ring-sidebar: None; -} -`; - -// Add styles to document -const styleSheet = document.createElement('style'); -styleSheet.textContent = tokenStyles; -document.head.appendChild(styleSheet); - -const preview: Preview = { - parameters: { - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/i, - }, - }, - backgrounds: { - default: 'light', - values: [ - { name: 'light', value: '#FFFFFF' }, - { name: 'dark', value: '#1F2937' }, - ], - }, - }, -}; - -export default preview; \ No newline at end of file diff --git a/dss-mvp1/dss/__init__.py b/dss-mvp1/dss/__init__.py deleted file mode 100644 index 78e2a58..0000000 --- a/dss-mvp1/dss/__init__.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -Design System Server (DSS) - Consolidated Platform -A modern design system orchestration platform with comprehensive tooling -""" - -__version__ = "1.0.0-consolidated" - -# Core models -from dss.models import ( - Theme, - Component, - ComponentVariant, - Project, - ProjectMetadata, - DesignToken as ModelDesignToken, - TokenCategory as ModelTokenCategory, -) - -# Validators -from dss.validators import ( - ProjectValidator, - ValidationResult, - ValidationError, - ValidationStage, -) - -# Tools -from dss.tools import ( - StyleDictionaryTool, - StyleDictionaryWrapper, - ShadcnTool, - ShadcnWrapper, - FigmaWrapper, - FigmaAPIError, -) - -# Ingest (multi-source token extraction) -from dss.ingest import ( - DesignToken, - TokenSource, - TokenCollection, - CSSTokenSource, - SCSSTokenSource, - TailwindTokenSource, - JSONTokenSource, - TokenMerger, - MergeStrategy, -) - -# Analyze (code analysis and scanning) -from dss.analyze import ( - ProjectAnalysis, - StylePattern, - QuickWin, - ProjectScanner, - ReactAnalyzer, - StyleAnalyzer, - DependencyGraph, - QuickWinFinder, -) - -# Storybook (integration and generation) -from dss.storybook import ( - StorybookScanner, - StoryGenerator, - ThemeGenerator, -) - -# Settings and configuration -from dss.settings import ( - DSSSettings, - DSSManager, - settings, - manager, -) - -# Status dashboard -from dss.status import ( - StatusDashboard, - HealthMetric, -) - -# Translations (dictionary system) -from dss.translations import ( - TranslationSource, - MappingType, - TokenMapping, - ComponentMapping, - PatternMapping, - CustomProp, - TranslationMappings, - TranslationDictionary, - TranslationRegistry, - ResolvedToken, - ResolvedTheme, - TranslationDictionaryLoader, - TokenResolver, - ThemeMerger, - TranslationValidator, - TranslationDictionaryWriter, - DSS_CANONICAL_TOKENS, - DSS_CANONICAL_COMPONENTS, - DSS_TOKEN_ALIASES, - DSS_COMPONENT_VARIANTS, - is_valid_dss_token, - resolve_alias, - get_canonical_token_categories, -) - -# Project Management -from dss.project import ( - DSSProject, - ProjectConfig, - FigmaSource, - FigmaFile, - OutputConfig, - ProjectStatus, - ProjectManager, - ProjectRegistry, - FigmaProjectSync, -) - -__all__ = [ - # Version - "__version__", - # Models - "Theme", - "Component", - "ComponentVariant", - "Project", - "ProjectMetadata", - "ModelDesignToken", - "ModelTokenCategory", - # Validators - "ProjectValidator", - "ValidationResult", - "ValidationError", - "ValidationStage", - # Tools - "StyleDictionaryTool", - "StyleDictionaryWrapper", - "ShadcnTool", - "ShadcnWrapper", - "FigmaWrapper", - "FigmaAPIError", - # Ingest - "DesignToken", - "TokenSource", - "TokenCollection", - "CSSTokenSource", - "SCSSTokenSource", - "TailwindTokenSource", - "JSONTokenSource", - "TokenMerger", - "MergeStrategy", - # Analyze - "ProjectAnalysis", - "StylePattern", - "QuickWin", - "ProjectScanner", - "ReactAnalyzer", - "StyleAnalyzer", - "DependencyGraph", - "QuickWinFinder", - # Storybook - "StorybookScanner", - "StoryGenerator", - "ThemeGenerator", - # Settings - "DSSSettings", - "DSSManager", - "settings", - "manager", - # Status - "StatusDashboard", - "HealthMetric", - # Translations - "TranslationSource", - "MappingType", - "TokenMapping", - "ComponentMapping", - "PatternMapping", - "CustomProp", - "TranslationMappings", - "TranslationDictionary", - "TranslationRegistry", - "ResolvedToken", - "ResolvedTheme", - "TranslationDictionaryLoader", - "TokenResolver", - "ThemeMerger", - "TranslationValidator", - "TranslationDictionaryWriter", - "DSS_CANONICAL_TOKENS", - "DSS_CANONICAL_COMPONENTS", - "DSS_TOKEN_ALIASES", - "DSS_COMPONENT_VARIANTS", - "is_valid_dss_token", - "resolve_alias", - "get_canonical_token_categories", - # Project Management - "DSSProject", - "ProjectConfig", - "FigmaSource", - "FigmaFile", - "OutputConfig", - "ProjectStatus", - "ProjectManager", - "ProjectRegistry", - "FigmaProjectSync", -] diff --git a/dss-mvp1/dss/api/__init__.py b/dss-mvp1/dss/api/__init__.py deleted file mode 100644 index 1aa6a0f..0000000 --- a/dss-mvp1/dss/api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""FastAPI routes and application""" diff --git a/dss-mvp1/dss/api/export_import_routes.py b/dss-mvp1/dss/api/export_import_routes.py deleted file mode 100644 index 37a854a..0000000 --- a/dss-mvp1/dss/api/export_import_routes.py +++ /dev/null @@ -1,522 +0,0 @@ -""" -FastAPI routes for DSS Export/Import system - -Provides REST API endpoints for project export, import, merge, and analysis. -All operations support both synchronous and asynchronous (background job) modes. -""" - -from fastapi import APIRouter, File, UploadFile, HTTPException, BackgroundTasks, Query -from fastapi.responses import FileResponse -from pydantic import BaseModel -from pathlib import Path -from datetime import datetime -from typing import Optional, List, Dict, Any -import logging - -from dss.export_import.service import DSSProjectService, ExportSummary, ImportSummary, MergeSummary -from dss.models.project import Project - -logger = logging.getLogger(__name__) -router = APIRouter(prefix="/api/projects", tags=["export_import"]) - -# Initialize service layer -service = DSSProjectService(busy_timeout_ms=5000) - -# In-memory job tracking (replace with Redis/database in production) -_jobs: Dict[str, Dict[str, Any]] = {} - - -# ============================================================================ -# Pydantic Models for API Responses -# ============================================================================ - -class ExportResponse(BaseModel): - """Response from export endpoint""" - success: bool - file_size_bytes: Optional[int] = None - token_count: Optional[int] = None - component_count: Optional[int] = None - error: Optional[str] = None - duration_seconds: Optional[float] = None - - -class ImportResponse(BaseModel): - """Response from import endpoint""" - success: bool - project_name: Optional[str] = None - token_count: Optional[int] = None - component_count: Optional[int] = None - migration_performed: Optional[bool] = None - warnings: Optional[List[str]] = None - error: Optional[str] = None - duration_seconds: Optional[float] = None - job_id: Optional[str] = None - - -class MergeResponse(BaseModel): - """Response from merge endpoint""" - success: bool - new_items: Optional[int] = None - updated_items: Optional[int] = None - conflicts: Optional[int] = None - resolution_strategy: Optional[str] = None - error: Optional[str] = None - duration_seconds: Optional[float] = None - job_id: Optional[str] = None - - -class AnalysisResponse(BaseModel): - """Response from analysis endpoint""" - is_valid: bool - project_name: Optional[str] = None - schema_version: Optional[str] = None - token_count: Optional[int] = None - component_count: Optional[int] = None - migration_needed: Optional[bool] = None - errors: Optional[List[str]] = None - warnings: Optional[List[str]] = None - - -class JobStatus(BaseModel): - """Status of a background job""" - job_id: str - status: str # pending, running, completed, failed - result: Optional[Dict[str, Any]] = None - error: Optional[str] = None - created_at: str - completed_at: Optional[str] = None - - -# ============================================================================ -# Export Endpoints -# ============================================================================ - -@router.post("/{project_id}/export", response_class=FileResponse) -async def export_project( - project_id: str, - background_tasks: Optional[BackgroundTasks] = None, - background: bool = Query(False, description="Run as background job") -) -> FileResponse: - """ - Export a project to a .dss archive file - - Args: - project_id: ID of project to export - background: If true, schedule as background job (for large projects) - - Returns: - .dss archive file download - - Examples: - ```bash - # Export synchronously - curl -X POST http://localhost:8000/api/projects/my-project/export \ - -o my-project.dss - - # Export as background job - curl -X POST "http://localhost:8000/api/projects/my-project/export?background=true" - ``` - """ - try: - # Load project (adapt to your data source) - project = _load_project(project_id) - if not project: - raise HTTPException(status_code=404, detail=f"Project not found: {project_id}") - - # Export - output_path = Path("/tmp") / f"{project_id}_export.dss" - result: ExportSummary = service.export_project(project, output_path) - - if not result.success: - raise HTTPException(status_code=400, detail=result.error) - - # Return file for download - return FileResponse( - result.archive_path, - media_type="application/zip", - filename=f"{project.name}.dss" - ) - - except HTTPException: - raise - except Exception as e: - logger.error(f"Export failed for {project_id}: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - -# ============================================================================ -# Import Endpoints -# ============================================================================ - -@router.post("/import", response_model=ImportResponse) -async def import_project( - file: UploadFile = File(...), - strategy: str = Query("replace", description="Import strategy: replace or merge"), - background: bool = Query(False, description="Run as background job") -) -> ImportResponse: - """ - Import a project from a .dss archive file - - Args: - file: .dss archive file to import - strategy: Import strategy (replace=full restoration, merge=smart update) - background: If true, schedule as background job (for large archives) - - Returns: - Import result summary - - Examples: - ```bash - # Import synchronously - curl -X POST http://localhost:8000/api/projects/import \ - -F "file=@my-project.dss" - - # Import with merge strategy - curl -X POST "http://localhost:8000/api/projects/import?strategy=merge" \ - -F "file=@updates.dss" - - # Import as background job - curl -X POST "http://localhost:8000/api/projects/import?background=true" \ - -F "file=@large-project.dss" - ``` - """ - archive_path = None - job_id = None - - try: - # Save uploaded file - archive_path = Path("/tmp") / f"import_{datetime.now().timestamp()}.dss" - contents = await file.read() - archive_path.write_bytes(contents) - - # Check if should run as background job - if service._should_schedule_background(archive_path): - # Schedule background job - job_id = _create_job_id() - _jobs[job_id] = { - "status": "pending", - "created_at": datetime.now().isoformat(), - "type": "import", - "archive_path": str(archive_path), - "strategy": strategy - } - - # In production: queue with Celery, RQ, or similar - # For now: return job ID for polling - return ImportResponse( - success=True, - job_id=job_id, - duration_seconds=0 - ) - - # Run synchronously - result: ImportSummary = service.import_project(archive_path, strategy) - - if not result.success: - raise HTTPException(status_code=400, detail=result.error) - - return ImportResponse( - success=True, - project_name=result.project_name, - token_count=result.item_counts.get("tokens") if result.item_counts else None, - component_count=result.item_counts.get("components") if result.item_counts else None, - migration_performed=result.migration_performed, - warnings=result.warnings or [], - duration_seconds=result.duration_seconds - ) - - except HTTPException: - raise - except Exception as e: - logger.error(f"Import failed: {e}") - raise HTTPException(status_code=500, detail=str(e)) - finally: - # Cleanup uploaded file after async processing - if archive_path and archive_path.exists() and job_id is None: - try: - archive_path.unlink() - except Exception: - pass - - -# ============================================================================ -# Merge Endpoints -# ============================================================================ - -@router.post("/{project_id}/merge", response_model=MergeResponse) -async def merge_project( - project_id: str, - file: UploadFile = File(...), - strategy: str = Query("keep_local", description="Conflict resolution: overwrite, keep_local, or fork"), - background: bool = Query(False, description="Run as background job") -) -> MergeResponse: - """ - Merge updates from a .dss archive into a project - - Args: - project_id: ID of project to merge into - file: .dss archive with updates - strategy: Conflict resolution strategy - background: If true, schedule as background job - - Returns: - Merge result summary - - Examples: - ```bash - # Merge with keep_local strategy (preserve local changes) - curl -X POST "http://localhost:8000/api/projects/my-project/merge?strategy=keep_local" \ - -F "file=@updates.dss" - - # Merge with overwrite strategy (accept remote changes) - curl -X POST "http://localhost:8000/api/projects/my-project/merge?strategy=overwrite" \ - -F "file=@updates.dss" - - # Merge as background job (for large archives) - curl -X POST "http://localhost:8000/api/projects/my-project/merge?background=true" \ - -F "file=@large-update.dss" - ``` - """ - archive_path = None - job_id = None - - try: - # Load project - project = _load_project(project_id) - if not project: - raise HTTPException(status_code=404, detail=f"Project not found: {project_id}") - - # Save uploaded file - archive_path = Path("/tmp") / f"merge_{datetime.now().timestamp()}.dss" - contents = await file.read() - archive_path.write_bytes(contents) - - # Check if should run as background job - if service._should_schedule_background(archive_path): - job_id = _create_job_id() - _jobs[job_id] = { - "status": "pending", - "created_at": datetime.now().isoformat(), - "type": "merge", - "project_id": project_id, - "archive_path": str(archive_path), - "strategy": strategy - } - return MergeResponse( - success=True, - job_id=job_id, - duration_seconds=0 - ) - - # Run synchronously - result: MergeSummary = service.merge_project(project, archive_path, strategy) - - if not result.success: - raise HTTPException(status_code=400, detail=result.error) - - return MergeResponse( - success=True, - new_items=result.new_items_count, - updated_items=result.updated_items_count, - conflicts=result.conflicts_count, - resolution_strategy=result.resolution_strategy, - duration_seconds=result.duration_seconds - ) - - except HTTPException: - raise - except Exception as e: - logger.error(f"Merge failed for {project_id}: {e}") - raise HTTPException(status_code=500, detail=str(e)) - finally: - if archive_path and archive_path.exists() and job_id is None: - try: - archive_path.unlink() - except Exception: - pass - - -# ============================================================================ -# Analysis Endpoints -# ============================================================================ - -@router.post("/{project_id}/analyze-merge") -async def analyze_merge( - project_id: str, - file: UploadFile = File(...) -) -> AnalysisResponse: - """ - Analyze merge without applying it (safe preview) - - Args: - project_id: ID of project to analyze merge into - file: .dss archive to analyze - - Returns: - Merge analysis (what changes would happen) - - Examples: - ```bash - curl -X POST http://localhost:8000/api/projects/my-project/analyze-merge \ - -F "file=@updates.dss" - ``` - """ - archive_path = None - - try: - # Load project - project = _load_project(project_id) - if not project: - raise HTTPException(status_code=404, detail=f"Project not found: {project_id}") - - # Save uploaded file - archive_path = Path("/tmp") / f"analyze_{datetime.now().timestamp()}.dss" - contents = await file.read() - archive_path.write_bytes(contents) - - # Analyze - analysis = service.analyze_merge(project, archive_path) - - return AnalysisResponse( - is_valid=analysis.is_valid, - new_items=len(analysis.new_items.get("tokens", [])), - conflicts=len(analysis.conflicted_items) - ) - - except HTTPException: - raise - except Exception as e: - logger.error(f"Merge analysis failed for {project_id}: {e}") - raise HTTPException(status_code=500, detail=str(e)) - finally: - if archive_path and archive_path.exists(): - try: - archive_path.unlink() - except Exception: - pass - - -@router.post("/analyze-archive") -async def analyze_archive( - file: UploadFile = File(...) -) -> AnalysisResponse: - """ - Analyze a .dss archive without importing it (safe preview) - - Args: - file: .dss archive to analyze - - Returns: - Archive analysis details - - Examples: - ```bash - curl -X POST http://localhost:8000/api/projects/analyze-archive \ - -F "file=@project.dss" - ``` - """ - archive_path = None - - try: - # Save uploaded file - archive_path = Path("/tmp") / f"analyze_archive_{datetime.now().timestamp()}.dss" - contents = await file.read() - archive_path.write_bytes(contents) - - # Analyze - analysis = service.analyze_import(archive_path) - - return AnalysisResponse( - is_valid=analysis.is_valid, - project_name=analysis.project_name, - schema_version=analysis.schema_version, - token_count=analysis.content_summary.get("tokens", {}).get("count"), - component_count=analysis.content_summary.get("components", {}).get("count"), - migration_needed=analysis.migration_needed, - errors=[e.message for e in analysis.errors], - warnings=analysis.warnings - ) - - except Exception as e: - logger.error(f"Archive analysis failed: {e}") - raise HTTPException(status_code=500, detail=str(e)) - finally: - if archive_path and archive_path.exists(): - try: - archive_path.unlink() - except Exception: - pass - - -# ============================================================================ -# Job Status Endpoint -# ============================================================================ - -@router.get("/jobs/{job_id}", response_model=JobStatus) -async def get_job_status(job_id: str) -> JobStatus: - """ - Get status of a background job - - Args: - job_id: ID of the job (returned from async endpoint) - - Returns: - Current job status and result (if completed) - - Examples: - ```bash - curl http://localhost:8000/api/projects/jobs/job-123 - ``` - """ - job = _jobs.get(job_id) - if not job: - raise HTTPException(status_code=404, detail=f"Job not found: {job_id}") - - return JobStatus( - job_id=job_id, - status=job.get("status", "unknown"), - result=job.get("result"), - error=job.get("error"), - created_at=job.get("created_at", ""), - completed_at=job.get("completed_at") - ) - - -# ============================================================================ -# Helper Functions -# ============================================================================ - -def _load_project(project_id: str) -> Optional[Project]: - """ - Load a project by ID - - ADAPT THIS to your actual data source (database, API, etc.) - """ - try: - # Example: Load from database - # return db.query(Project).filter(Project.id == project_id).first() - - # For now: return a dummy project - # In production: implement actual loading - logger.warning(f"Using dummy project for {project_id} - implement _load_project()") - return Project( - name=project_id, - description="Auto-loaded project", - author="system" - ) - except Exception as e: - logger.error(f"Failed to load project {project_id}: {e}") - return None - - -def _create_job_id() -> str: - """Generate unique job ID""" - import uuid - return str(uuid.uuid4())[:8] - - -# ============================================================================ -# Export router for inclusion in FastAPI app -# ============================================================================ - -__all__ = ["router"] diff --git a/dss-mvp1/dss/tools/__init__.py b/dss-mvp1/dss/tools/__init__.py deleted file mode 100644 index d1530cd..0000000 --- a/dss-mvp1/dss/tools/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Wrappers for external design system tools""" - -from .style_dictionary import StyleDictionaryTool, StyleDictionaryWrapper -from .shadcn import ShadcnTool, ShadcnWrapper -from .figma import FigmaWrapper, FigmaAPIError - -__all__ = [ - "StyleDictionaryTool", - "StyleDictionaryWrapper", - "ShadcnTool", - "ShadcnWrapper", - "FigmaWrapper", - "FigmaAPIError" -] diff --git a/dss-mvp1/dss/tools/figma.py b/dss-mvp1/dss/tools/figma.py deleted file mode 100644 index 78399d7..0000000 --- a/dss-mvp1/dss/tools/figma.py +++ /dev/null @@ -1,316 +0,0 @@ -""" -Figma API wrapper for design token extraction -Based on Figmagic architecture and W3C DTCG format standards -""" - -import json -import requests -from pathlib import Path -from typing import Dict, List, Optional, Any -from dss.models.theme import Theme, DesignToken, TokenCategory - - -class FigmaWrapper: - """ - Wrapper for Figma REST API - Extracts design tokens from Figma Variables and converts to W3C DTCG format - - Architecture: - Figma Variables API → W3C DTCG format → DSS Theme model → StyleDictionary → outputs - """ - - FIGMA_API_BASE = "https://api.figma.com/v1" - - def __init__(self, api_token: str, file_key: str, use_cache: bool = True): - """ - Initialize Figma wrapper - - Args: - api_token: Figma personal access token - file_key: Figma file key (from URL) - use_cache: Whether to cache API responses - """ - if not api_token or not file_key: - raise ValueError("Figma API token and file key are required") - - self.api_token = api_token - self.file_key = file_key - self.use_cache = use_cache - self.cache_path = Path.home() / ".dss" / "figma_cache.json" - - self.headers = { - "X-Figma-Token": self.api_token - } - - def get_variables(self) -> Dict[str, Any]: - """ - Fetch variables from Figma file using Variables API - - Returns: - Raw Figma Variables API response - - Raises: - FigmaAPIError: If API request fails - """ - # Check cache first - if self.use_cache and self.cache_path.exists(): - with open(self.cache_path, 'r') as f: - return json.load(f) - - # Fetch from API - url = f"{self.FIGMA_API_BASE}/files/{self.file_key}/variables/local" - - try: - response = requests.get(url, headers=self.headers) - response.raise_for_status() - - data = response.json() - - # Cache response - if self.use_cache: - self.cache_path.parent.mkdir(parents=True, exist_ok=True) - with open(self.cache_path, 'w') as f: - json.dump(data, f, indent=2) - - return data - - except requests.exceptions.HTTPError as e: - if e.response.status_code == 403: - raise FigmaAPIError("Invalid Figma API token (403 Forbidden)") - elif e.response.status_code == 404: - raise FigmaAPIError(f"Figma file '{self.file_key}' not found (404)") - else: - raise FigmaAPIError(f"Figma API error: {e}") - except Exception as e: - raise FigmaAPIError(f"Failed to fetch Figma variables: {e}") - - def extract_themes(self) -> Dict[str, Theme]: - """ - Extract themes from Figma Variables - - Figma uses "variable collections" with "modes" for themes. - Example: Collection "Colors" might have modes "Light" and "Dark" - - Returns: - Dict mapping theme name to DSS Theme object - """ - figma_data = self.get_variables() - - # Build mode ID → theme name mapping - mode_map = self._build_mode_map(figma_data.get("meta", {}).get("variableCollections", {})) - - # Extract variables and convert to DTCG format - variables = figma_data.get("meta", {}).get("variables", {}) - dtcg_tokens = self._convert_to_dtcg(variables) - - # Structure tokens by theme - themes = self._structure_by_theme(dtcg_tokens, mode_map) - - return themes - - def _build_mode_map(self, variable_collections: Dict[str, Any]) -> Dict[str, str]: - """ - Build mapping of mode ID → theme name - - Args: - variable_collections: Figma variable collections data - - Returns: - Dict mapping mode ID to theme name (e.g., {"331:7": "Light"}) - """ - mode_map = {} - - for collection_id, collection in variable_collections.items(): - modes = collection.get("modes", []) - for mode in modes: - mode_id = mode.get("modeId") - mode_name = mode.get("name") - if mode_id and mode_name: - mode_map[mode_id] = mode_name - - return mode_map - - def _convert_to_dtcg(self, variables: Dict[str, Any]) -> Dict[str, Any]: - """ - Convert Figma variables to W3C DTCG format - - DTCG format: - { - "color": { - "primary": { - "$value": "#0066cc", - "$type": "color", - "$description": "Primary brand color" - } - } - } - - Args: - variables: Figma variables data - - Returns: - DTCG-formatted token tree - """ - tokens = {} - - for var_id, variable in variables.items(): - # Skip remote variables - if variable.get("remote", False): - continue - - name = variable.get("name", "") - resolved_type = variable.get("resolvedType", "") - values_by_mode = variable.get("valuesByMode", {}) - description = variable.get("description", "") - - # Convert Figma type to DTCG type - dtcg_type = self._map_figma_type_to_dtcg(resolved_type) - - # Parse name into nested structure (e.g., "colors/primary/500" → colors.primary.500) - path_parts = name.split("/") - - # Create token object with values by mode - token_obj = { - "$type": dtcg_type, - "valuesByMode": values_by_mode - } - - if description: - token_obj["$description"] = description - - # Set in nested structure - self._set_nested(tokens, path_parts, token_obj) - - return tokens - - def _structure_by_theme(self, dtcg_tokens: Dict[str, Any], mode_map: Dict[str, str]) -> Dict[str, Theme]: - """ - Structure tokens by theme using mode mapping - - Args: - dtcg_tokens: DTCG tokens with valuesByMode - mode_map: Mapping of mode ID to theme name - - Returns: - Dict of theme name → DSS Theme - """ - themes = {} - - # Initialize themes - for mode_name in set(mode_map.values()): - themes[mode_name] = Theme( - name=f"DSS {mode_name}", - version="1.0.0", - tokens={} - ) - - # Recursively extract tokens for each theme - def extract_tokens(node: Dict[str, Any], path: str = ""): - for key, value in node.items(): - if key.startswith("$"): - # Skip metadata keys - continue - - current_path = f"{path}/{key}" if path else key - - if isinstance(value, dict) and "valuesByMode" in value: - # This is a token leaf node - dtcg_type = value.get("$type", "other") - description = value.get("$description", "") - values_by_mode = value["valuesByMode"] - - # Create token for each mode - for mode_id, token_value in values_by_mode.items(): - theme_name = mode_map.get(mode_id) - if theme_name and theme_name in themes: - # Convert path to token name (use last part for simplicity) - token_name = key - - # Format value based on type - formatted_value = self._format_value(token_value, dtcg_type) - - # Map DTCG type to DSS TokenCategory - category = self._map_dtcg_type_to_category(dtcg_type) - - # Create DesignToken - design_token = DesignToken( - name=token_name, - value=formatted_value, - type=dtcg_type, - category=category, - description=description or f"{token_name} token" - ) - - themes[theme_name].tokens[token_name] = design_token - - elif isinstance(value, dict): - # Recurse into nested groups - extract_tokens(value, current_path) - - extract_tokens(dtcg_tokens) - - return themes - - def _map_figma_type_to_dtcg(self, figma_type: str) -> str: - """Map Figma variable type to W3C DTCG type""" - type_map = { - "COLOR": "color", - "FLOAT": "number", # Could be dimension, duration, etc. - "STRING": "string", - "BOOLEAN": "boolean" - } - return type_map.get(figma_type, "other") - - def _map_dtcg_type_to_category(self, dtcg_type: str) -> TokenCategory: - """Map DTCG type to DSS TokenCategory""" - category_map = { - "color": TokenCategory.COLOR, - "dimension": TokenCategory.SPACING, - "fontFamily": TokenCategory.TYPOGRAPHY, - "fontSize": TokenCategory.TYPOGRAPHY, - "fontWeight": TokenCategory.TYPOGRAPHY, - "lineHeight": TokenCategory.TYPOGRAPHY, - "borderRadius": TokenCategory.RADIUS, - "shadow": TokenCategory.SHADOW, - "border": TokenCategory.BORDER, - } - return category_map.get(dtcg_type, TokenCategory.OTHER) - - def _format_value(self, value: Any, dtcg_type: str) -> str: - """ - Format Figma value to string representation - - Args: - value: Raw Figma value - dtcg_type: DTCG type - - Returns: - Formatted value string - """ - if dtcg_type == "color" and isinstance(value, dict): - # Figma color format: {r: 0-1, g: 0-1, b: 0-1, a: 0-1} - r = int(value.get("r", 0) * 255) - g = int(value.get("g", 0) * 255) - b = int(value.get("b", 0) * 255) - a = value.get("a", 1) - - if a == 1: - return f"rgb({r}, {g}, {b})" - else: - return f"rgba({r}, {g}, {b}, {a})" - - return str(value) - - def _set_nested(self, obj: Dict, path: List[str], value: Any): - """Set value in nested dictionary using path""" - current = obj - for part in path[:-1]: - if part not in current: - current[part] = {} - current = current[part] - current[path[-1]] = value - - -class FigmaAPIError(Exception): - """Exception raised for Figma API errors""" - pass diff --git a/dss-mvp1/dss/tools/shadcn.py b/dss-mvp1/dss/tools/shadcn.py deleted file mode 100644 index 5afefa8..0000000 --- a/dss-mvp1/dss/tools/shadcn.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -Shadcn CLI wrapper for component management -""" - -import subprocess -from pathlib import Path -from typing import List, Optional, Dict, Any - - -class ShadcnWrapper: - """ - Wrapper for shadcn/ui CLI - Manages shadcn component installation and configuration - """ - - def __init__(self, shadcn_path: str = "npx shadcn-ui@latest"): - """ - Initialize Shadcn wrapper - - Args: - shadcn_path: Path to shadcn executable (default: npx shadcn-ui@latest) - """ - self.shadcn_path = shadcn_path - - def add_component( - self, - component_name: str, - project_path: Path, - overwrite: bool = False - ) -> Dict[str, Any]: - """ - Add a shadcn component to project - - Args: - component_name: Component to add (e.g., 'button', 'card') - project_path: Project root directory - overwrite: Whether to overwrite existing components - - Returns: - Dict with installation result - """ - cmd = [ - "npx", "shadcn-ui@latest", "add", component_name, - "--yes" # Auto-confirm - ] - - if overwrite: - cmd.append("--overwrite") - - result = subprocess.run( - cmd, - cwd=project_path, - capture_output=True, - text=True - ) - - return { - "success": result.returncode == 0, - "component": component_name, - "stdout": result.stdout, - "stderr": result.stderr - } - - def init_shadcn(self, project_path: Path) -> Dict[str, Any]: - """ - Initialize shadcn in a project - - Args: - project_path: Project root directory - - Returns: - Dict with initialization result - """ - cmd = ["npx", "shadcn-ui@latest", "init", "--yes"] - - result = subprocess.run( - cmd, - cwd=project_path, - capture_output=True, - text=True - ) - - return { - "success": result.returncode == 0, - "stdout": result.stdout, - "stderr": result.stderr - } - - def list_available_components(self) -> List[str]: - """ - List available shadcn components - - Returns: - List of component names - """ - # Hardcoded list of common shadcn components - # In a real implementation, this would query the shadcn registry - return [ - "accordion", "alert", "alert-dialog", "aspect-ratio", - "avatar", "badge", "button", "calendar", "card", - "checkbox", "collapsible", "command", "context-menu", - "dialog", "dropdown-menu", "form", "hover-card", - "input", "label", "menubar", "navigation-menu", - "popover", "progress", "radio-group", "scroll-area", - "select", "separator", "sheet", "skeleton", "slider", - "switch", "table", "tabs", "textarea", "toast", - "toggle", "tooltip" - ] - - -# Alias for backward compatibility -ShadcnTool = ShadcnWrapper diff --git a/dss-mvp1/dss/tools/style_dictionary.py b/dss-mvp1/dss/tools/style_dictionary.py deleted file mode 100644 index 586e857..0000000 --- a/dss-mvp1/dss/tools/style_dictionary.py +++ /dev/null @@ -1,247 +0,0 @@ -""" -Style Dictionary wrapper for design token transformation -Converts DSS tokens to various output formats (CSS, SCSS, JSON) -""" - -import json -import subprocess -import tempfile -from pathlib import Path -from typing import Dict, List, Optional, Any - -from dss.models.theme import Theme, DesignToken, TokenCategory - - -class StyleDictionaryWrapper: - """ - Wrapper for Amazon Style Dictionary CLI - Transforms design tokens into platform-specific outputs - """ - - def __init__(self, sd_path: str = "npx style-dictionary"): - """ - Initialize Style Dictionary wrapper - - Args: - sd_path: Path to style-dictionary executable (default: npx style-dictionary) - """ - self.sd_path = sd_path - - def transform_theme( - self, - theme: Theme, - output_format: str = "css", - output_path: Optional[Path] = None - ) -> Dict[str, Any]: - """ - Transform a DSS theme using style-dictionary - - Args: - theme: DSS Theme to transform - output_format: Output format (css, scss, json, js) - output_path: Optional output directory - - Returns: - Dict with transformation result - """ - # Create temporary directory for style-dictionary config - with tempfile.TemporaryDirectory() as tmpdir: - tmppath = Path(tmpdir) - - # Convert DSS theme to style-dictionary format - sd_tokens = self._convert_theme_to_sd_format(theme) - - # Write tokens to JSON file - tokens_file = tmppath / "tokens.json" - with open(tokens_file, "w") as f: - json.dump(sd_tokens, f, indent=2) - - # Create style-dictionary config - config = self._create_sd_config(output_format, tmppath) - config_file = tmppath / "config.json" - with open(config_file, "w") as f: - json.dump(config, f, indent=2) - - # Run style-dictionary build - result = self._run_sd_build(config_file, tmppath) - - # Read output files - output_files = self._read_output_files(tmppath, output_format) - - return { - "success": result.returncode == 0, - "output_format": output_format, - "files": output_files, - "errors": result.stderr if result.returncode != 0 else None - } - - def _convert_theme_to_sd_format(self, theme: Theme) -> Dict[str, Any]: - """ - Convert DSS theme to style-dictionary token format - - Style Dictionary format: - { - "color": { - "primary": { "value": "#0066cc" } - } - } - """ - sd_tokens = {} - - for token_name, token in theme.tokens.items(): - # Group tokens by category - category = token.category.value if token.category else "other" - - if category not in sd_tokens: - sd_tokens[category] = {} - - # Convert token to SD format - sd_tokens[category][token_name] = { - "value": token.value, - "type": token.type, - } - - if token.description: - sd_tokens[category][token_name]["comment"] = token.description - - return sd_tokens - - def _create_sd_config(self, output_format: str, build_path: Path) -> Dict[str, Any]: - """ - Create style-dictionary configuration - - Args: - output_format: Desired output format - build_path: Build directory path - - Returns: - Style Dictionary config dict - """ - config = { - "source": ["tokens.json"], - "platforms": {} - } - - if output_format == "css": - config["platforms"]["css"] = { - "transformGroup": "css", - "buildPath": str(build_path) + "/", - "files": [{ - "destination": "theme.css", - "format": "css/variables" - }] - } - elif output_format == "scss": - config["platforms"]["scss"] = { - "transformGroup": "scss", - "buildPath": str(build_path) + "/", - "files": [{ - "destination": "theme.scss", - "format": "scss/variables" - }] - } - elif output_format == "json": - config["platforms"]["json"] = { - "transformGroup": "js", - "buildPath": str(build_path) + "/", - "files": [{ - "destination": "theme.json", - "format": "json/nested" - }] - } - elif output_format == "js": - config["platforms"]["js"] = { - "transformGroup": "js", - "buildPath": str(build_path) + "/", - "files": [{ - "destination": "theme.js", - "format": "javascript/module" - }] - } - - return config - - def _run_sd_build(self, config_file: Path, cwd: Path) -> subprocess.CompletedProcess: - """ - Run style-dictionary build command - - Args: - config_file: Path to config.json - cwd: Working directory - - Returns: - Subprocess result - """ - cmd = [ - "npx", "style-dictionary", "build", - "--config", str(config_file) - ] - - result = subprocess.run( - cmd, - cwd=cwd, - capture_output=True, - text=True - ) - - return result - - def _read_output_files(self, build_path: Path, output_format: str) -> Dict[str, str]: - """ - Read generated output files - - Args: - build_path: Directory containing built files - output_format: Output format used - - Returns: - Dict of filename -> content - """ - files = {} - - # Map format to expected file - format_files = { - "css": "theme.css", - "scss": "theme.scss", - "json": "theme.json", - "js": "theme.js" - } - - filename = format_files.get(output_format) - if filename: - filepath = build_path / filename - if filepath.exists(): - with open(filepath, "r") as f: - files[filename] = f.read() - - return files - - def convert_tokens_to_css_vars(self, theme: Theme) -> str: - """ - Convert DSS theme tokens to CSS custom properties - - Args: - theme: DSS Theme - - Returns: - CSS string with :root variables - """ - css_lines = [":root {"] - - for token_name, token in theme.tokens.items(): - # Convert token name to CSS variable format - css_var_name = f"--{token_name}" - - # Add comment if description exists - if token.description: - css_lines.append(f" /* {token.description} */") - - css_lines.append(f" {css_var_name}: {token.value};") - - css_lines.append("}") - - return "\n".join(css_lines) - - -# Alias for backward compatibility -StyleDictionaryTool = StyleDictionaryWrapper diff --git a/dss-mvp1/scripts/run_migrations.py b/dss-mvp1/scripts/run_migrations.py deleted file mode 100755 index 0456bc5..0000000 --- a/dss-mvp1/scripts/run_migrations.py +++ /dev/null @@ -1,295 +0,0 @@ -#!/usr/bin/env python3 -""" -DSS Database Migration Runner - -This script runs SQL migrations in the correct order, with proper error handling -and transaction safety. - -Usage: - python run_migrations.py # Run all pending migrations - python run_migrations.py --check # Show pending migrations only - python run_migrations.py --rollback 0001 # Rollback specific migration (CAREFUL!) -""" - -import os -import sys -import sqlite3 -import argparse -from pathlib import Path -from datetime import datetime -import json - -# Add parent directory to path for imports -sys.path.insert(0, str(Path(__file__).parent.parent)) - - -class MigrationRunner: - """Manages database migrations with version tracking and rollback support""" - - def __init__(self, db_path: Path = None): - """ - Initialize migration runner - - Args: - db_path: Path to database file. If None, uses default DSS location. - """ - if db_path is None: - # Default DSS database location - db_path = Path.cwd() / ".dss" / "dss.db" - - self.db_path = Path(db_path) - self.migrations_dir = Path(__file__).parent.parent / "dss" / "storage" / "migrations" - self.migrations_table = "_dss_migrations" - - def _ensure_migrations_table(self, conn: sqlite3.Connection): - """Create migrations tracking table if it doesn't exist""" - conn.execute(f""" - CREATE TABLE IF NOT EXISTS {self.migrations_table} ( - id TEXT PRIMARY KEY, - applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - description TEXT, - checksum TEXT, - status TEXT DEFAULT 'applied' - ) - """) - conn.commit() - - def _get_migration_checksum(self, migration_file: Path) -> str: - """Calculate checksum of migration file for integrity verification""" - import hashlib - content = migration_file.read_text() - return hashlib.sha256(content.encode()).hexdigest() - - def _get_applied_migrations(self, conn: sqlite3.Connection) -> dict: - """Get dictionary of applied migrations""" - cursor = conn.execute(f"SELECT id, checksum FROM {self.migrations_table}") - return {row[0]: row[1] for row in cursor.fetchall()} - - def _get_pending_migrations(self) -> list: - """Get list of pending migrations in order""" - applied = {} - try: - conn = sqlite3.connect(self.db_path) - self._ensure_migrations_table(conn) - applied = self._get_applied_migrations(conn) - conn.close() - except Exception as e: - print(f"Warning: Could not read migration history: {e}") - - pending = [] - if self.migrations_dir.exists(): - for migration_file in sorted(self.migrations_dir.glob("*.sql")): - migration_id = migration_file.stem # e.g., "0002_add_uuid_columns" - if migration_id not in applied: - pending.append({ - 'id': migration_id, - 'file': migration_file, - 'checksum': self._get_migration_checksum(migration_file), - 'status': 'pending' - }) - - return pending - - def check(self) -> bool: - """Check for pending migrations without applying them""" - pending = self._get_pending_migrations() - - if not pending: - print("✓ No pending migrations - database is up to date") - return True - - print(f"Found {len(pending)} pending migration(s):\n") - for migration in pending: - print(f" - {migration['id']}") - print(f" File: {migration['file'].name}") - print(f" Checksum: {migration['checksum'][:16]}...") - - return False - - def run(self, dry_run: bool = False) -> bool: - """ - Run all pending migrations - - Args: - dry_run: If True, show migrations but don't apply them - - Returns: - True if successful, False if any migration failed - """ - pending = self._get_pending_migrations() - - if not pending: - print("✓ No pending migrations - database is up to date") - return True - - print(f"Found {len(pending)} pending migration(s)") - if dry_run: - print("\nDRY RUN - No changes will be applied\n") - else: - print("Running migrations...\n") - - # Backup database before running migrations - if not dry_run: - backup_path = self.db_path.with_suffix(f".backup-{datetime.now().strftime('%Y%m%d-%H%M%S')}") - import shutil - try: - shutil.copy2(self.db_path, backup_path) - print(f"✓ Database backed up to: {backup_path}\n") - except Exception as e: - print(f"✗ Failed to create backup: {e}") - print(" Aborting migration") - return False - - conn = sqlite3.connect(self.db_path) - conn.row_factory = sqlite3.Row - self._ensure_migrations_table(conn) - - try: - for migration in pending: - migration_id = migration['id'] - migration_file = migration['file'] - - print(f"Running: {migration_id}") - - # Read migration SQL - sql_content = migration_file.read_text() - - if not dry_run: - try: - # Execute migration with transaction - conn.executescript(sql_content) - - # Record migration as applied - conn.execute(f""" - INSERT INTO {self.migrations_table} - (id, description, checksum, status) - VALUES (?, ?, ?, 'applied') - """, (migration_id, migration_file.name, migration['checksum'])) - - conn.commit() - print(f" ✓ Migration applied successfully\n") - except sqlite3.Error as e: - conn.rollback() - print(f" ✗ Migration failed: {e}") - print(f" ✗ Changes rolled back") - return False - else: - # Dry run: just show what would happen - print(" (DRY RUN - Would execute)") - lines = sql_content.split('\n')[:5] # Show first 5 lines - for line in lines: - if line.strip() and not line.strip().startswith('--'): - print(f" {line[:70]}") - if len(sql_content.split('\n')) > 5: - print(f" ... ({len(sql_content.split(chr(10)))} lines total)") - print() - - if not dry_run: - print("\n✓ All migrations applied successfully") - return True - else: - print("✓ Dry run complete - no changes made") - return True - - finally: - conn.close() - - def status(self) -> None: - """Show migration status""" - try: - conn = sqlite3.connect(self.db_path) - self._ensure_migrations_table(conn) - - cursor = conn.execute(f""" - SELECT id, applied_at, status FROM {self.migrations_table} - ORDER BY applied_at - """) - - applied = cursor.fetchall() - print(f"Applied migrations ({len(applied)}):\n") - - if applied: - for row in applied: - print(f" ✓ {row[0]}") - print(f" Applied at: {row[1]}") - else: - print(" (none)") - - pending = self._get_pending_migrations() - print(f"\nPending migrations ({len(pending)}):\n") - - if pending: - for migration in pending: - print(f" ⏳ {migration['id']}") - else: - print(" (none)") - - conn.close() - except Exception as e: - print(f"Error reading migration status: {e}") - - -def main(): - parser = argparse.ArgumentParser( - description="DSS Database Migration Runner", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - python run_migrations.py # Run all pending migrations - python run_migrations.py --check # Check for pending migrations - python run_migrations.py --dry-run # Show what would be applied - python run_migrations.py --status # Show migration status -""" - ) - - parser.add_argument( - '--check', - action='store_true', - help='Check for pending migrations without applying them' - ) - parser.add_argument( - '--dry-run', - action='store_true', - help='Show migrations that would be applied without applying them' - ) - parser.add_argument( - '--status', - action='store_true', - help='Show migration status (applied and pending)' - ) - parser.add_argument( - '--db', - type=Path, - help='Path to database file (default: .dss/dss.db)' - ) - - args = parser.parse_args() - - runner = MigrationRunner(db_path=args.db) - - try: - if args.status: - runner.status() - return 0 - elif args.check: - success = runner.check() - return 0 if success else 1 - elif args.dry_run: - success = runner.run(dry_run=True) - return 0 if success else 1 - else: - # Run migrations - success = runner.run(dry_run=False) - return 0 if success else 1 - except KeyboardInterrupt: - print("\nMigration cancelled by user") - return 1 - except Exception as e: - print(f"Error: {e}") - import traceback - traceback.print_exc() - return 1 - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/dss-mvp1/setup.sh b/dss-mvp1/setup.sh deleted file mode 100755 index 43df2f7..0000000 --- a/dss-mvp1/setup.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/bin/bash -# DSS MVP1 Setup Script -# Configures DSS for dss.overbits.luz.uy - -set -e # Exit on error - -echo "🚀 DSS MVP1 Setup for dss.overbits.luz.uy" -echo "==========================================" -echo "" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Check if running as overbits user -if [ "$USER" != "overbits" ]; then - echo -e "${YELLOW}⚠️ Warning: This script is designed for user 'overbits'${NC}" - echo -e " Current user: $USER" - read -p "Continue anyway? (y/n) " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - exit 1 - fi -fi - -# 1. Check dependencies -echo "📦 Checking dependencies..." -python3 -m dss.settings check-deps || { - echo -e "${RED}❌ Dependency check failed${NC}" - echo " Installing Python dependencies..." - pip install -r requirements.txt - echo " Installing Node dependencies..." - npm install -} - -# 2. Create necessary directories -echo "" -echo "📁 Creating directories..." -mkdir -p ~/.dss/{cache,logs,backups} -mkdir -p dist/tokens -mkdir -p components -echo -e "${GREEN}✅ Directories created${NC}" - -# 3. Check for API keys -echo "" -echo "🔑 Checking API keys..." - -if [ ! -f .env ]; then - echo -e "${YELLOW}⚠️ .env file not found - already created${NC}" -fi - -# Check if keys are configured -if grep -q "^ANTHROPIC_API_KEY=$" .env 2>/dev/null; then - echo -e "${YELLOW}⚠️ ANTHROPIC_API_KEY not set in .env${NC}" - echo " Get your key from: https://console.anthropic.com/settings/keys" -fi - -if grep -q "^FIGMA_TOKEN=$" .env 2>/dev/null; then - echo -e "${YELLOW}⚠️ FIGMA_TOKEN not set in .env${NC}" - echo " Get your token from: https://www.figma.com/developers/api#access-tokens" -fi - -if grep -q "^JWT_SECRET=$" .env 2>/dev/null; then - echo -e "${YELLOW}⚠️ JWT_SECRET not set in .env${NC}" - echo " Generate with: openssl rand -hex 32" - read -p "Generate JWT_SECRET now? (y/n) " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - JWT_SECRET=$(openssl rand -hex 32) - sed -i "s/^JWT_SECRET=$/JWT_SECRET=$JWT_SECRET/" .env - echo -e "${GREEN}✅ JWT_SECRET generated and saved to .env${NC}" - fi -fi - -# 4. Run tests -echo "" -read -p "Run tests to verify setup? (y/n) " -n 1 -r -echo -if [[ $REPLY =~ ^[Yy]$ ]]; then - echo "🧪 Running tests..." - python3 -m dss.settings test || { - echo -e "${RED}❌ Tests failed${NC}" - echo " Check the output above for errors" - exit 1 - } - echo -e "${GREEN}✅ All tests passed!${NC}" -fi - -# 5. System info -echo "" -echo "📊 System Information:" -python3 -m dss.settings info - -# 6. Final instructions -echo "" -echo "==========================================" -echo -e "${GREEN}✅ Setup Complete!${NC}" -echo "" -echo "Next steps:" -echo " 1. Add your API keys to .env:" -echo " - ANTHROPIC_API_KEY (https://console.anthropic.com/settings/keys)" -echo " - FIGMA_TOKEN (https://www.figma.com/developers/api#access-tokens)" -echo "" -echo " 2. Configure your Figma file:" -echo " - Add FIGMA_FILE_KEY to .env" -echo " - Format: figma.com/file/{FILE_KEY}/..." -echo "" -echo " 3. Start the server:" -echo " python3 -m uvicorn dss.api.server:app --host 0.0.0.0 --port 3456" -echo "" -echo " 4. Visit: https://dss.overbits.luz.uy" -echo "" -echo "Commands:" -echo " python3 -m dss.settings test # Run tests" -echo " python3 -m dss.settings reset # Reset to fresh state" -echo " python3 -m dss.settings info # Show system info" -echo "" -echo "See SETTINGS.md for full documentation" -echo "==========================================" diff --git a/dss-mvp1/stories/Welcome.stories.js b/dss-mvp1/stories/Welcome.stories.js deleted file mode 100644 index f1ddf04..0000000 --- a/dss-mvp1/stories/Welcome.stories.js +++ /dev/null @@ -1,40 +0,0 @@ -export default { - title: "DSS/Welcome", - tags: ["autodocs"], -}; - -export const GettingStarted = { - render: () => { - const div = document.createElement("div"); - div.innerHTML = ` -
-

Design System Server

-

- Welcome to DSS Storybook. This is your interactive component library. -

- -
-

Getting Started

-
    -
  1. Go to the Admin UI to configure your design system
  2. -
  3. Import components from Figma or your component library
  4. -
  5. Click "Initialize Storybook" to generate component stories
  6. -
  7. Browse your components in the sidebar
  8. -
-
- -
- No components loaded yet -

- Initialize your design system from the Admin UI to populate this Storybook. -

-
- -

- DSS v1.0.0 | Open Admin UI -

-
- `; - return div; - }, -}; diff --git a/dss-mvp1/tests/conftest.py b/dss-mvp1/tests/conftest.py deleted file mode 100644 index 0afa925..0000000 --- a/dss-mvp1/tests/conftest.py +++ /dev/null @@ -1,82 +0,0 @@ -import pytest -from pathlib import Path - -@pytest.fixture(scope="function") -def mock_react_project(tmp_path: Path) -> Path: - """ - Creates a temporary mock React project structure for testing. - """ - project_dir = tmp_path / "test-project" - project_dir.mkdir() - - # Create src directory - src_dir = project_dir / "src" - src_dir.mkdir() - - # Create components directory - components_dir = src_dir / "components" - components_dir.mkdir() - - # Component A - (components_dir / "ComponentA.jsx").write_text(""" -import React from 'react'; -import './ComponentA.css'; - -const ComponentA = () => { - return
Component A
; -}; - -export default ComponentA; - """) - - (components_dir / "ComponentA.css").write_text(""" -.component-a { - color: blue; -} - """) - - # Component B - (components_dir / "ComponentB.tsx").write_text(""" -import React from 'react'; -import ComponentA from './ComponentA'; - -const ComponentB = () => { - return ( -
- -
- ); -}; - -export default ComponentB; - """) - - # App.js - (src_dir / "App.js").write_text(""" -import React from 'react'; -import ComponentB from './components/ComponentB'; - -function App() { - return ( -
- -
- ); -} - -export default App; - """) - - # package.json - (project_dir / "package.json").write_text(""" -{ - "name": "test-project", - "version": "0.1.0", - "private": true, - "dependencies": { - "react": "^18.0.0" - } -} - """) - - return project_dir diff --git a/dss/__init__.py b/dss/__init__.py new file mode 100644 index 0000000..67ea4e6 --- /dev/null +++ b/dss/__init__.py @@ -0,0 +1,152 @@ +""" +DSS - Design System Server + +A Model Context Protocol (MCP) server that provides Claude Code with 40+ design system tools. +Supports local development and remote team deployment. + +Usage: + from dss import settings, Projects, Components + from dss.mcp import MCPServer + from dss.storage import Projects, Components, Tokens +""" + +__version__ = "1.0.0" + +# Settings & Configuration +from dss.settings import settings, DSSSettings, DSSManager, manager + +# Storage Layer +from dss.storage.json_store import ( + Projects, + Components, + Tokens, + Styles, + SyncHistory, + ActivityLog, + Teams, + Cache, + FigmaFiles, + CodeMetrics, + TestResults, + TokenDrift, + Integrations, + IntegrationHealth, + get_stats, +) + +# Analyze +from dss.analyze.base import ( + ProjectAnalysis, + QuickWin, + ComponentInfo, + StylePattern, + Framework, + StylingApproach, +) +from dss.analyze.scanner import ProjectScanner + +# Ingest +from dss.ingest.base import ( + DesignToken, + TokenCollection, + TokenSource, + TokenType, + TokenCategory, +) + +# Export/Import +from dss.export_import.service import DSSArchiveExporter, DSSArchiveImporter +from dss.export_import.smart_merger import SmartMerger + +# Storybook +from dss.storybook.generator import StoryGenerator +from dss.storybook.scanner import StorybookScanner + +# Translations +from dss.translations.dictionary import TranslationDictionary +from dss.translations.resolver import TokenResolver + +# Services +from dss.services.project_manager import ProjectManager +from dss.services.config_service import ConfigService, DSSConfig +from dss.services.sandboxed_fs import SandboxedFS + +# Figma +from dss.figma.figma_tools import FigmaToolSuite + +# Project +from dss.project.manager import DSSProject + +# Models +from dss.models.theme import Theme +from dss.models.component import Component +from dss.models.project import Project + +# Validators +from dss.validators.schema import ProjectValidator, ValidationResult + +__all__ = [ + # Version + "__version__", + # Settings + "settings", + "DSSSettings", + "DSSManager", + "manager", + # Storage + "Projects", + "Components", + "Tokens", + "Styles", + "SyncHistory", + "ActivityLog", + "Teams", + "Cache", + "FigmaFiles", + "CodeMetrics", + "TestResults", + "TokenDrift", + "Integrations", + "IntegrationHealth", + "get_stats", + # Analyze + "ProjectAnalysis", + "QuickWin", + "ComponentInfo", + "StylePattern", + "Framework", + "StylingApproach", + "ProjectScanner", + # Ingest + "DesignToken", + "TokenCollection", + "TokenSource", + "TokenType", + "TokenCategory", + # Export/Import + "DSSArchiveExporter", + "DSSArchiveImporter", + "SmartMerger", + # Storybook + "StoryGenerator", + "StorybookScanner", + # Translations + "TranslationDictionary", + "TokenResolver", + # Services + "ProjectManager", + "ConfigService", + "DSSConfig", + "SandboxedFS", + # Figma + "FigmaToolSuite", + # Project + "DSSProject", + # Models + "Theme", + "Component", + "Project", + # Validators + "ProjectValidator", + "ValidationResult", +] diff --git a/dss-mvp1/dss/analyze/__init__.py b/dss/analyze/__init__.py similarity index 100% rename from dss-mvp1/dss/analyze/__init__.py rename to dss/analyze/__init__.py diff --git a/dss-mvp1/dss/analyze/base.py b/dss/analyze/base.py similarity index 100% rename from dss-mvp1/dss/analyze/base.py rename to dss/analyze/base.py diff --git a/dss-mvp1/dss/analyze/graph.py b/dss/analyze/graph.py similarity index 100% rename from dss-mvp1/dss/analyze/graph.py rename to dss/analyze/graph.py diff --git a/dss-mvp1/dss/analyze/parser.js b/dss/analyze/parser.js similarity index 100% rename from dss-mvp1/dss/analyze/parser.js rename to dss/analyze/parser.js diff --git a/dss-mvp1/dss/analyze/project_analyzer.py b/dss/analyze/project_analyzer.py similarity index 100% rename from dss-mvp1/dss/analyze/project_analyzer.py rename to dss/analyze/project_analyzer.py diff --git a/dss-mvp1/dss/analyze/quick_wins.py b/dss/analyze/quick_wins.py similarity index 100% rename from dss-mvp1/dss/analyze/quick_wins.py rename to dss/analyze/quick_wins.py diff --git a/dss-mvp1/dss/analyze/react.py b/dss/analyze/react.py similarity index 100% rename from dss-mvp1/dss/analyze/react.py rename to dss/analyze/react.py diff --git a/dss-mvp1/dss/analyze/scanner.py b/dss/analyze/scanner.py similarity index 100% rename from dss-mvp1/dss/analyze/scanner.py rename to dss/analyze/scanner.py diff --git a/dss-mvp1/dss/analyze/styles.py b/dss/analyze/styles.py similarity index 100% rename from dss-mvp1/dss/analyze/styles.py rename to dss/analyze/styles.py diff --git a/tools/auth/__init__.py b/dss/auth/__init__.py similarity index 100% rename from tools/auth/__init__.py rename to dss/auth/__init__.py diff --git a/tools/auth/atlassian_auth.py b/dss/auth/atlassian_auth.py similarity index 100% rename from tools/auth/atlassian_auth.py rename to dss/auth/atlassian_auth.py diff --git a/dss-mvp1/stories/generated/.gitkeep b/dss/core_tokens/__init__.py similarity index 100% rename from dss-mvp1/stories/generated/.gitkeep rename to dss/core_tokens/__init__.py diff --git a/dss-mvp1/dss/core_tokens/components.json b/dss/core_tokens/components.json similarity index 100% rename from dss-mvp1/dss/core_tokens/components.json rename to dss/core_tokens/components.json diff --git a/dss-mvp1/dss/core_tokens/manifest.json b/dss/core_tokens/manifest.json similarity index 100% rename from dss-mvp1/dss/core_tokens/manifest.json rename to dss/core_tokens/manifest.json diff --git a/dss-mvp1/dss/core_tokens/themes.json b/dss/core_tokens/themes.json similarity index 100% rename from dss-mvp1/dss/core_tokens/themes.json rename to dss/core_tokens/themes.json diff --git a/dss-mvp1/dss/core_tokens/tokens.json b/dss/core_tokens/tokens.json similarity index 100% rename from dss-mvp1/dss/core_tokens/tokens.json rename to dss/core_tokens/tokens.json diff --git a/dss-mvp1/dss/export_import/__init__.py b/dss/export_import/__init__.py similarity index 100% rename from dss-mvp1/dss/export_import/__init__.py rename to dss/export_import/__init__.py diff --git a/dss-mvp1/dss/export_import/examples.py b/dss/export_import/examples.py similarity index 100% rename from dss-mvp1/dss/export_import/examples.py rename to dss/export_import/examples.py diff --git a/dss-mvp1/dss/export_import/exporter.py b/dss/export_import/exporter.py similarity index 100% rename from dss-mvp1/dss/export_import/exporter.py rename to dss/export_import/exporter.py diff --git a/dss-mvp1/dss/export_import/importer.py b/dss/export_import/importer.py similarity index 100% rename from dss-mvp1/dss/export_import/importer.py rename to dss/export_import/importer.py diff --git a/dss-mvp1/dss/export_import/merger.py b/dss/export_import/merger.py similarity index 100% rename from dss-mvp1/dss/export_import/merger.py rename to dss/export_import/merger.py diff --git a/dss-mvp1/dss/export_import/migrations.py b/dss/export_import/migrations.py similarity index 100% rename from dss-mvp1/dss/export_import/migrations.py rename to dss/export_import/migrations.py diff --git a/dss-mvp1/dss/export_import/security.py b/dss/export_import/security.py similarity index 100% rename from dss-mvp1/dss/export_import/security.py rename to dss/export_import/security.py diff --git a/dss-mvp1/dss/export_import/service.py b/dss/export_import/service.py similarity index 100% rename from dss-mvp1/dss/export_import/service.py rename to dss/export_import/service.py diff --git a/dss/figma/__init__.py b/dss/figma/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/figma/figma_tools.py b/dss/figma/figma_tools.py similarity index 100% rename from tools/figma/figma_tools.py rename to dss/figma/figma_tools.py diff --git a/dss-mvp1/dss/ingest/__init__.py b/dss/ingest/__init__.py similarity index 100% rename from dss-mvp1/dss/ingest/__init__.py rename to dss/ingest/__init__.py diff --git a/dss-mvp1/dss/ingest/base.py b/dss/ingest/base.py similarity index 100% rename from dss-mvp1/dss/ingest/base.py rename to dss/ingest/base.py diff --git a/dss-mvp1/dss/ingest/css.py b/dss/ingest/css.py similarity index 100% rename from dss-mvp1/dss/ingest/css.py rename to dss/ingest/css.py diff --git a/dss-mvp1/dss/ingest/json_tokens.py b/dss/ingest/json_tokens.py similarity index 100% rename from dss-mvp1/dss/ingest/json_tokens.py rename to dss/ingest/json_tokens.py diff --git a/dss-mvp1/dss/ingest/merge.py b/dss/ingest/merge.py similarity index 100% rename from dss-mvp1/dss/ingest/merge.py rename to dss/ingest/merge.py diff --git a/dss-mvp1/dss/ingest/scss.py b/dss/ingest/scss.py similarity index 100% rename from dss-mvp1/dss/ingest/scss.py rename to dss/ingest/scss.py diff --git a/dss-mvp1/dss/ingest/tailwind.py b/dss/ingest/tailwind.py similarity index 100% rename from dss-mvp1/dss/ingest/tailwind.py rename to dss/ingest/tailwind.py diff --git a/tools/dss_mcp/__init__.py b/dss/mcp/__init__.py similarity index 100% rename from tools/dss_mcp/__init__.py rename to dss/mcp/__init__.py diff --git a/tools/dss_mcp/audit.py b/dss/mcp/audit.py similarity index 100% rename from tools/dss_mcp/audit.py rename to dss/mcp/audit.py diff --git a/tools/dss_mcp/config.py b/dss/mcp/config.py similarity index 100% rename from tools/dss_mcp/config.py rename to dss/mcp/config.py diff --git a/dss/mcp/context/__init__.py b/dss/mcp/context/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/dss_mcp/context/project_context.py b/dss/mcp/context/project_context.py similarity index 100% rename from tools/dss_mcp/context/project_context.py rename to dss/mcp/context/project_context.py diff --git a/tools/dss_mcp/handler.py b/dss/mcp/handler.py similarity index 100% rename from tools/dss_mcp/handler.py rename to dss/mcp/handler.py diff --git a/dss/mcp/integrations/__init__.py b/dss/mcp/integrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/dss_mcp/integrations/base.py b/dss/mcp/integrations/base.py similarity index 100% rename from tools/dss_mcp/integrations/base.py rename to dss/mcp/integrations/base.py diff --git a/tools/dss_mcp/integrations/confluence.py b/dss/mcp/integrations/confluence.py similarity index 100% rename from tools/dss_mcp/integrations/confluence.py rename to dss/mcp/integrations/confluence.py diff --git a/tools/dss_mcp/integrations/figma.py b/dss/mcp/integrations/figma.py similarity index 100% rename from tools/dss_mcp/integrations/figma.py rename to dss/mcp/integrations/figma.py diff --git a/tools/dss_mcp/integrations/jira.py b/dss/mcp/integrations/jira.py similarity index 100% rename from tools/dss_mcp/integrations/jira.py rename to dss/mcp/integrations/jira.py diff --git a/tools/dss_mcp/integrations/storybook.py b/dss/mcp/integrations/storybook.py similarity index 100% rename from tools/dss_mcp/integrations/storybook.py rename to dss/mcp/integrations/storybook.py diff --git a/tools/dss_mcp/integrations/translations.py b/dss/mcp/integrations/translations.py similarity index 100% rename from tools/dss_mcp/integrations/translations.py rename to dss/mcp/integrations/translations.py diff --git a/tools/dss_mcp/operations.py b/dss/mcp/operations.py similarity index 100% rename from tools/dss_mcp/operations.py rename to dss/mcp/operations.py diff --git a/tools/dss_mcp/plugin_registry.py b/dss/mcp/plugin_registry.py similarity index 100% rename from tools/dss_mcp/plugin_registry.py rename to dss/mcp/plugin_registry.py diff --git a/tools/dss_mcp/plugins/__init__.py b/dss/mcp/plugins/__init__.py similarity index 100% rename from tools/dss_mcp/plugins/__init__.py rename to dss/mcp/plugins/__init__.py diff --git a/tools/dss_mcp/plugins/_template.py b/dss/mcp/plugins/_template.py similarity index 100% rename from tools/dss_mcp/plugins/_template.py rename to dss/mcp/plugins/_template.py diff --git a/tools/dss_mcp/plugins/hello_world.py b/dss/mcp/plugins/hello_world.py similarity index 100% rename from tools/dss_mcp/plugins/hello_world.py rename to dss/mcp/plugins/hello_world.py diff --git a/tools/dss_mcp/requirements.txt b/dss/mcp/requirements.txt similarity index 100% rename from tools/dss_mcp/requirements.txt rename to dss/mcp/requirements.txt diff --git a/tools/dss_mcp/security.py b/dss/mcp/security.py similarity index 100% rename from tools/dss_mcp/security.py rename to dss/mcp/security.py diff --git a/tools/dss_mcp/server.py b/dss/mcp/server.py similarity index 100% rename from tools/dss_mcp/server.py rename to dss/mcp/server.py diff --git a/dss/mcp/tools/__init__.py b/dss/mcp/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/dss_mcp/tools/analysis_tools.py b/dss/mcp/tools/analysis_tools.py similarity index 100% rename from tools/dss_mcp/tools/analysis_tools.py rename to dss/mcp/tools/analysis_tools.py diff --git a/tools/dss_mcp/tools/debug_tools.py b/dss/mcp/tools/debug_tools.py similarity index 100% rename from tools/dss_mcp/tools/debug_tools.py rename to dss/mcp/tools/debug_tools.py diff --git a/tools/dss_mcp/tools/project_tools.py b/dss/mcp/tools/project_tools.py similarity index 100% rename from tools/dss_mcp/tools/project_tools.py rename to dss/mcp/tools/project_tools.py diff --git a/tools/dss_mcp/tools/workflow_tools.py b/dss/mcp/tools/workflow_tools.py similarity index 100% rename from tools/dss_mcp/tools/workflow_tools.py rename to dss/mcp/tools/workflow_tools.py diff --git a/dss-mvp1/dss/models/__init__.py b/dss/models/__init__.py similarity index 100% rename from dss-mvp1/dss/models/__init__.py rename to dss/models/__init__.py diff --git a/dss-mvp1/dss/models/component.py b/dss/models/component.py similarity index 100% rename from dss-mvp1/dss/models/component.py rename to dss/models/component.py diff --git a/dss-mvp1/dss/models/project.py b/dss/models/project.py similarity index 100% rename from dss-mvp1/dss/models/project.py rename to dss/models/project.py diff --git a/dss-mvp1/dss/models/team_dashboard.py b/dss/models/team_dashboard.py similarity index 100% rename from dss-mvp1/dss/models/team_dashboard.py rename to dss/models/team_dashboard.py diff --git a/dss-mvp1/dss/models/theme.py b/dss/models/theme.py similarity index 100% rename from dss-mvp1/dss/models/theme.py rename to dss/models/theme.py diff --git a/dss-mvp1/dss/project/__init__.py b/dss/project/__init__.py similarity index 100% rename from dss-mvp1/dss/project/__init__.py rename to dss/project/__init__.py diff --git a/dss-mvp1/dss/project/core.py b/dss/project/core.py similarity index 100% rename from dss-mvp1/dss/project/core.py rename to dss/project/core.py diff --git a/dss-mvp1/dss/project/figma.py b/dss/project/figma.py similarity index 100% rename from dss-mvp1/dss/project/figma.py rename to dss/project/figma.py diff --git a/dss-mvp1/dss/project/manager.py b/dss/project/manager.py similarity index 100% rename from dss-mvp1/dss/project/manager.py rename to dss/project/manager.py diff --git a/dss-mvp1/dss/project/models.py b/dss/project/models.py similarity index 100% rename from dss-mvp1/dss/project/models.py rename to dss/project/models.py diff --git a/dss-mvp1/dss/project/sync.py b/dss/project/sync.py similarity index 100% rename from dss-mvp1/dss/project/sync.py rename to dss/project/sync.py diff --git a/tools/api/services/__init__.py b/dss/services/__init__.py similarity index 100% rename from tools/api/services/__init__.py rename to dss/services/__init__.py diff --git a/tools/api/services/config_service.py b/dss/services/config_service.py similarity index 100% rename from tools/api/services/config_service.py rename to dss/services/config_service.py diff --git a/tools/api/services/project_manager.py b/dss/services/project_manager.py similarity index 100% rename from tools/api/services/project_manager.py rename to dss/services/project_manager.py diff --git a/tools/api/services/sandboxed_fs.py b/dss/services/sandboxed_fs.py similarity index 100% rename from tools/api/services/sandboxed_fs.py rename to dss/services/sandboxed_fs.py diff --git a/dss-mvp1/dss/settings.py b/dss/settings.py similarity index 96% rename from dss-mvp1/dss/settings.py rename to dss/settings.py index a999442..3077e31 100644 --- a/dss-mvp1/dss/settings.py +++ b/dss/settings.py @@ -25,11 +25,13 @@ class DSSSettings(BaseSettings): DSS_DIR: Path = Path(__file__).parent TESTS_DIR: Path = PROJECT_ROOT / "tests" CACHE_DIR: Path = Path.home() / ".dss" / "cache" + DATA_DIR: Path = Path.home() / ".dss" / "data" # API Configuration ANTHROPIC_API_KEY: Optional[str] = None FIGMA_TOKEN: Optional[str] = None FIGMA_FILE_KEY: Optional[str] = None + FIGMA_CACHE_TTL: int = 300 # 5 minutes # Database DATABASE_PATH: Path = Path.home() / ".dss" / "dss.db" @@ -41,12 +43,26 @@ class DSSSettings(BaseSettings): # Server Configuration SERVER_HOST: str = "0.0.0.0" # Host to bind server to SERVER_PORT: int = 3456 + SERVER_ENV: str = "development" # development or production + LOG_LEVEL: str = "INFO" + + # MCP Server Configuration + MCP_HOST: str = "127.0.0.1" + MCP_PORT: int = 3457 # Storybook Configuration STORYBOOK_HOST: str = "0.0.0.0" # Host for Storybook server (uses SERVER_HOST if not set) STORYBOOK_PORT: int = 6006 # Default Storybook port STORYBOOK_AUTO_OPEN: bool = False # Don't auto-open browser + @property + def is_production(self) -> bool: + return self.SERVER_ENV == "production" + + @property + def figma_configured(self) -> bool: + return bool(self.FIGMA_TOKEN) + class DSSManager: """Management utilities for DSS projects and system health.""" diff --git a/dss-mvp1/dss/status/__init__.py b/dss/status/__init__.py similarity index 100% rename from dss-mvp1/dss/status/__init__.py rename to dss/status/__init__.py diff --git a/dss-mvp1/dss/status/dashboard.py b/dss/status/dashboard.py similarity index 100% rename from dss-mvp1/dss/status/dashboard.py rename to dss/status/dashboard.py diff --git a/dss/storage/__init__.py b/dss/storage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/storage/json_store.py b/dss/storage/json_store.py similarity index 100% rename from tools/storage/json_store.py rename to dss/storage/json_store.py diff --git a/dss-mvp1/dss/storybook/__init__.py b/dss/storybook/__init__.py similarity index 100% rename from dss-mvp1/dss/storybook/__init__.py rename to dss/storybook/__init__.py diff --git a/dss-mvp1/dss/storybook/config.py b/dss/storybook/config.py similarity index 100% rename from dss-mvp1/dss/storybook/config.py rename to dss/storybook/config.py diff --git a/dss-mvp1/dss/storybook/generator.py b/dss/storybook/generator.py similarity index 100% rename from dss-mvp1/dss/storybook/generator.py rename to dss/storybook/generator.py diff --git a/dss-mvp1/dss/storybook/scanner.py b/dss/storybook/scanner.py similarity index 100% rename from dss-mvp1/dss/storybook/scanner.py rename to dss/storybook/scanner.py diff --git a/dss-mvp1/dss/storybook/theme.py b/dss/storybook/theme.py similarity index 100% rename from dss-mvp1/dss/storybook/theme.py rename to dss/storybook/theme.py diff --git a/dss-mvp1/dss/themes/__init__.py b/dss/themes/__init__.py similarity index 100% rename from dss-mvp1/dss/themes/__init__.py rename to dss/themes/__init__.py diff --git a/dss-mvp1/dss/themes/default_themes.py b/dss/themes/default_themes.py similarity index 100% rename from dss-mvp1/dss/themes/default_themes.py rename to dss/themes/default_themes.py diff --git a/dss-mvp1/dss/translations/__init__.py b/dss/translations/__init__.py similarity index 100% rename from dss-mvp1/dss/translations/__init__.py rename to dss/translations/__init__.py diff --git a/dss-mvp1/dss/translations/canonical.py b/dss/translations/canonical.py similarity index 100% rename from dss-mvp1/dss/translations/canonical.py rename to dss/translations/canonical.py diff --git a/dss-mvp1/dss/translations/loader.py b/dss/translations/loader.py similarity index 100% rename from dss-mvp1/dss/translations/loader.py rename to dss/translations/loader.py diff --git a/dss-mvp1/dss/translations/merger.py b/dss/translations/merger.py similarity index 100% rename from dss-mvp1/dss/translations/merger.py rename to dss/translations/merger.py diff --git a/dss-mvp1/dss/translations/models.py b/dss/translations/models.py similarity index 100% rename from dss-mvp1/dss/translations/models.py rename to dss/translations/models.py diff --git a/dss-mvp1/dss/translations/resolver.py b/dss/translations/resolver.py similarity index 100% rename from dss-mvp1/dss/translations/resolver.py rename to dss/translations/resolver.py diff --git a/dss-mvp1/dss/translations/validator.py b/dss/translations/validator.py similarity index 100% rename from dss-mvp1/dss/translations/validator.py rename to dss/translations/validator.py diff --git a/dss-mvp1/dss/translations/writer.py b/dss/translations/writer.py similarity index 100% rename from dss-mvp1/dss/translations/writer.py rename to dss/translations/writer.py diff --git a/dss-mvp1/dss/validators/__init__.py b/dss/validators/__init__.py similarity index 100% rename from dss-mvp1/dss/validators/__init__.py rename to dss/validators/__init__.py diff --git a/dss-mvp1/dss/validators/schema.py b/dss/validators/schema.py similarity index 100% rename from dss-mvp1/dss/validators/schema.py rename to dss/validators/schema.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0d904bb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,74 @@ +[project] +name = "dss" +version = "1.0.0" +description = "Design System Server - MCP for Claude Code" +readme = "README.md" +requires-python = ">=3.10" +license = {text = "MIT"} +authors = [ + {name = "DSS Team"} +] +keywords = ["design-system", "mcp", "claude", "figma", "storybook"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +dependencies = [ + "fastapi>=0.104.0", + "uvicorn[standard]>=0.24.0", + "pydantic>=2.0.0", + "pydantic-settings>=2.0.0", + "python-dotenv>=1.0.0", + "httpx>=0.25.0", + "mcp>=0.9.0", + "anthropic>=0.40.0", + "sse-starlette>=1.8.0", + "aiohttp>=3.9.0", + "atlassian-python-api>=3.41.0", + "cryptography>=42.0.0", + "structlog>=23.2.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-asyncio>=0.21.0", + "pytest-cov>=4.0.0", + "black>=23.0.0", + "ruff>=0.1.0", +] +redis = [ + "celery[redis]>=5.3.0", + "redis>=5.0.0", +] + +[project.scripts] +dss = "dss.mcp.server:main" +dss-api = "apps.api.server:main" + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +include = ["dss*", "apps*"] + +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["tests"] +asyncio_mode = "auto" + +[tool.black] +line-length = 100 +target-version = ["py310", "py311", "py312"] + +[tool.ruff] +line-length = 100 +select = ["E", "F", "I", "N", "W"] +ignore = ["E501"] diff --git a/pytest.ini b/pytest.ini index 29783b2..481903e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -30,6 +30,6 @@ addopts = asyncio_mode = auto # Coverage (if pytest-cov installed) -# --cov=tools +# --cov=dss # --cov-report=html # --cov-report=term-missing diff --git a/scripts/dss b/scripts/dss index 89ab58c..20b3ece 100755 --- a/scripts/dss +++ b/scripts/dss @@ -15,8 +15,8 @@ set -e -DSS_ROOT="$(cd "$(dirname "$0")" && pwd)" -API_DIR="$DSS_ROOT/tools/api" +DSS_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +API_DIR="$DSS_ROOT/apps/api" UI_DIR="$DSS_ROOT/admin-ui" VENV_DIR="$DSS_ROOT/.venv" PID_FILE="$DSS_ROOT/.dss/dss.pid" @@ -68,7 +68,7 @@ check_deps() { # Check if dependencies installed if ! python3 -c "import fastapi" 2>/dev/null; then log "Installing dependencies..." - pip install -q -r "$API_DIR/requirements.txt" + pip install -q -r "$DSS_ROOT/requirements.txt" fi } @@ -99,8 +99,8 @@ start_server() { log "Starting DSS server..." fi - # Export Python path - export PYTHONPATH="$DSS_ROOT/tools:$PYTHONPATH" + # Export Python path (project root for dss package) + export PYTHONPATH="$DSS_ROOT:$PYTHONPATH" export PORT="$PORT" if [ "$dev_mode" = "true" ]; then diff --git a/dss-mvp1/babel.config.json b/storybook/babel.config.json similarity index 100% rename from dss-mvp1/babel.config.json rename to storybook/babel.config.json diff --git a/dss-mvp1/config.yaml b/storybook/config.yaml similarity index 100% rename from dss-mvp1/config.yaml rename to storybook/config.yaml diff --git a/dss-mvp1/package-lock.json b/storybook/package-lock.json similarity index 100% rename from dss-mvp1/package-lock.json rename to storybook/package-lock.json diff --git a/dss-mvp1/package.json b/storybook/package.json similarity index 100% rename from dss-mvp1/package.json rename to storybook/package.json diff --git a/tests/conftest.py b/tests/conftest.py index c67bec8..0afa925 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,98 +1,82 @@ -""" -Pytest configuration and shared fixtures. -""" - import pytest -import tempfile -import shutil from pathlib import Path -from tools.ingest.base import DesignToken, TokenCollection, TokenType - -@pytest.fixture -def temp_dir(): - """Create a temporary directory for tests.""" - temp_path = tempfile.mkdtemp() - yield Path(temp_path) - shutil.rmtree(temp_path, ignore_errors=True) - - -@pytest.fixture -def sample_css(): - """Sample CSS custom properties.""" - return """ - :root { - --color-primary: #3B82F6; - --color-secondary: #10B981; - --spacing-sm: 8px; - --spacing-md: 16px; - --spacing-lg: 24px; - --font-size-base: 16px; - } +@pytest.fixture(scope="function") +def mock_react_project(tmp_path: Path) -> Path: """ - - -@pytest.fixture -def sample_scss(): - """Sample SCSS variables.""" - return """ - $primary-color: #3B82F6; - $secondary-color: #10B981; - $font-family-sans: 'Inter', sans-serif; - $font-size-base: 16px; - $spacing-md: 16px; + Creates a temporary mock React project structure for testing. """ + project_dir = tmp_path / "test-project" + project_dir.mkdir() + # Create src directory + src_dir = project_dir / "src" + src_dir.mkdir() -@pytest.fixture -def sample_json_tokens(): - """Sample JSON design tokens (W3C format).""" - return { - "color": { - "primary": { - "500": {"value": "#3B82F6", "type": "color"}, - "600": {"value": "#2563EB", "type": "color"} - }, - "secondary": { - "500": {"value": "#10B981", "type": "color"} - } - }, - "spacing": { - "sm": {"value": "8px", "type": "dimension"}, - "md": {"value": "16px", "type": "dimension"}, - "lg": {"value": "24px", "type": "dimension"} - } - } + # Create components directory + components_dir = src_dir / "components" + components_dir.mkdir() + # Component A + (components_dir / "ComponentA.jsx").write_text(""" +import React from 'react'; +import './ComponentA.css'; -@pytest.fixture -def sample_token_collection(): - """Create a sample token collection.""" - tokens = [ - DesignToken(name="color.primary", value="#3B82F6", type=TokenType.COLOR), - DesignToken(name="color.secondary", value="#10B981", type=TokenType.COLOR), - DesignToken(name="spacing.md", value="16px", type=TokenType.SPACING), - ] - return TokenCollection(tokens=tokens, name="Sample Collection") +const ComponentA = () => { + return
Component A
; +}; +export default ComponentA; + """) -@pytest.fixture -def tailwind_config_path(temp_dir): - """Create a temporary Tailwind config file.""" - config_content = """ -module.exports = { - theme: { - colors: { - blue: '#0000FF', - red: '#FF0000' - }, - spacing: { - '1': '4px', - '2': '8px' - } - } + (components_dir / "ComponentA.css").write_text(""" +.component-a { + color: blue; } -""" - config_file = temp_dir / "tailwind.config.js" - config_file.write_text(config_content) - return config_file + """) + + # Component B + (components_dir / "ComponentB.tsx").write_text(""" +import React from 'react'; +import ComponentA from './ComponentA'; + +const ComponentB = () => { + return ( +
+ +
+ ); +}; + +export default ComponentB; + """) + + # App.js + (src_dir / "App.js").write_text(""" +import React from 'react'; +import ComponentB from './components/ComponentB'; + +function App() { + return ( +
+ +
+ ); +} + +export default App; + """) + + # package.json + (project_dir / "package.json").write_text(""" +{ + "name": "test-project", + "version": "0.1.0", + "private": true, + "dependencies": { + "react": "^18.0.0" + } +} + """) + + return project_dir diff --git a/tests/test_ingestion.py b/tests/test_ingestion.py deleted file mode 100644 index a48ca2d..0000000 --- a/tests/test_ingestion.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -Tests for token ingestion from various sources. -""" - -import pytest -import json -from tools.ingest.css import CSSTokenSource -from tools.ingest.scss import SCSSTokenSource -from tools.ingest.json_tokens import JSONTokenSource - - -@pytest.mark.asyncio -async def test_css_ingestion(sample_css): - """Test CSS custom property extraction.""" - parser = CSSTokenSource() - result = await parser.extract(sample_css) - - assert len(result.tokens) >= 5 - assert any(t.name == "color.primary" for t in result.tokens) - assert any(t.value == "#3B82F6" for t in result.tokens) - - -@pytest.mark.asyncio -async def test_scss_ingestion(sample_scss): - """Test SCSS variable extraction.""" - parser = SCSSTokenSource() - result = await parser.extract(sample_scss) - - assert len(result.tokens) >= 4 - # SCSS converts $primary-color to primary.color (dashes to dots) - assert any(t.name == "primary.color" for t in result.tokens) - - -@pytest.mark.asyncio -async def test_json_ingestion(sample_json_tokens): - """Test JSON token extraction (W3C format).""" - parser = JSONTokenSource() - result = await parser.extract(json.dumps(sample_json_tokens)) - - assert len(result.tokens) >= 6 - # Check color tokens - primary_tokens = [t for t in result.tokens if "primary" in t.name] - assert len(primary_tokens) >= 2 - - # Check spacing tokens - spacing_tokens = [t for t in result.tokens if "spacing" in t.name] - assert len(spacing_tokens) == 3 - - -@pytest.mark.asyncio -async def test_empty_css(): - """Test handling of empty CSS.""" - parser = CSSTokenSource() - # Use CSS syntax marker so parser detects as content, not file path - result = await parser.extract(":root {}") - - assert len(result.tokens) == 0 - assert result.name - - -@pytest.mark.asyncio -async def test_invalid_json(): - """Test handling of invalid JSON.""" - parser = JSONTokenSource() - - # Parser wraps JSONDecodeError in ValueError - with pytest.raises(ValueError, match="Invalid JSON"): - await parser.extract("invalid json{") diff --git a/tests/test_json_store.py b/tests/test_json_store.py deleted file mode 100644 index 7bde2db..0000000 --- a/tests/test_json_store.py +++ /dev/null @@ -1,308 +0,0 @@ -""" -Tests for JSON file storage layer. - -Tests the new json_store module that replaced SQLite. -""" - -import pytest -import json -import tempfile -import shutil -from pathlib import Path -from datetime import datetime - -# Temporarily override DATA_DIR for tests -import tools.storage.json_store as json_store - - -@pytest.fixture -def temp_storage(tmp_path): - """Create temporary storage directory for tests.""" - # Save original paths - original_data_dir = json_store.DATA_DIR - original_system_dir = json_store.SYSTEM_DIR - original_projects_dir = json_store.PROJECTS_DIR - original_teams_dir = json_store.TEAMS_DIR - - # Override with temp paths - json_store.DATA_DIR = tmp_path / "data" - json_store.SYSTEM_DIR = json_store.DATA_DIR / "_system" - json_store.PROJECTS_DIR = json_store.DATA_DIR / "projects" - json_store.TEAMS_DIR = json_store.DATA_DIR / "teams" - json_store.Cache.CACHE_DIR = json_store.SYSTEM_DIR / "cache" - - # Initialize directories - json_store.init_storage() - - yield tmp_path - - # Restore original paths - json_store.DATA_DIR = original_data_dir - json_store.SYSTEM_DIR = original_system_dir - json_store.PROJECTS_DIR = original_projects_dir - json_store.TEAMS_DIR = original_teams_dir - json_store.Cache.CACHE_DIR = original_system_dir / "cache" - - -class TestCache: - """Tests for TTL-based cache.""" - - def test_cache_set_and_get(self, temp_storage): - """Test basic cache operations.""" - json_store.Cache.set("test_key", {"foo": "bar"}, ttl=60) - result = json_store.Cache.get("test_key") - - assert result == {"foo": "bar"} - - def test_cache_expiry(self, temp_storage): - """Test that expired cache returns None.""" - import time - json_store.Cache.set("expired_key", "value", ttl=1) - time.sleep(1.1) # Wait for expiry - result = json_store.Cache.get("expired_key") - - assert result is None - - def test_cache_delete(self, temp_storage): - """Test cache deletion.""" - json_store.Cache.set("delete_me", "value") - json_store.Cache.delete("delete_me") - result = json_store.Cache.get("delete_me") - - assert result is None - - def test_cache_clear_all(self, temp_storage): - """Test clearing all cache.""" - json_store.Cache.set("key1", "value1") - json_store.Cache.set("key2", "value2") - json_store.Cache.clear_all() - - assert json_store.Cache.get("key1") is None - assert json_store.Cache.get("key2") is None - - -class TestProjects: - """Tests for project operations.""" - - def test_create_project(self, temp_storage): - """Test project creation.""" - project = json_store.Projects.create( - id="test-project", - name="Test Project", - description="A test project" - ) - - assert project["id"] == "test-project" - assert project["name"] == "Test Project" - assert project["status"] == "active" - - def test_get_project(self, temp_storage): - """Test project retrieval.""" - json_store.Projects.create(id="get-test", name="Get Test") - project = json_store.Projects.get("get-test") - - assert project is not None - assert project["name"] == "Get Test" - - def test_list_projects(self, temp_storage): - """Test listing projects.""" - json_store.Projects.create(id="proj1", name="Project 1") - json_store.Projects.create(id="proj2", name="Project 2") - - projects = json_store.Projects.list() - - assert len(projects) == 2 - - def test_update_project(self, temp_storage): - """Test project update.""" - json_store.Projects.create(id="update-test", name="Original") - updated = json_store.Projects.update("update-test", name="Updated") - - assert updated["name"] == "Updated" - - def test_delete_project(self, temp_storage): - """Test project deletion (archives).""" - json_store.Projects.create(id="delete-test", name="Delete Me") - result = json_store.Projects.delete("delete-test") - - assert result is True - assert json_store.Projects.get("delete-test") is None - - def test_project_creates_token_structure(self, temp_storage): - """Test that project creation initializes token folders.""" - json_store.Projects.create(id="token-test", name="Token Test") - - tokens_dir = json_store.PROJECTS_DIR / "token-test" / "tokens" - assert tokens_dir.exists() - assert (tokens_dir / "colors.json").exists() - assert (tokens_dir / "spacing.json").exists() - - -class TestTokens: - """Tests for token operations.""" - - def test_get_all_tokens(self, temp_storage): - """Test getting all tokens for a project.""" - json_store.Projects.create(id="tokens-proj", name="Tokens Project") - tokens = json_store.Tokens.get_all("tokens-proj") - - assert "colors" in tokens - assert "spacing" in tokens - assert "typography" in tokens - - def test_set_and_get_tokens(self, temp_storage): - """Test setting and getting tokens by type.""" - json_store.Projects.create(id="set-tokens", name="Set Tokens") - - json_store.Tokens.set_by_type("set-tokens", "colors", { - "primary": "#3B82F6", - "secondary": "#10B981" - }) - - colors = json_store.Tokens.get_by_type("set-tokens", "colors") - - assert colors["primary"] == "#3B82F6" - assert colors["secondary"] == "#10B981" - - def test_merge_tokens_last_strategy(self, temp_storage): - """Test merging tokens with LAST strategy.""" - json_store.Projects.create(id="merge-test", name="Merge Test") - - json_store.Tokens.set_by_type("merge-test", "colors", { - "primary": "#old", - "secondary": "#keep" - }) - - merged = json_store.Tokens.merge("merge-test", "colors", { - "primary": "#new", - "tertiary": "#added" - }, strategy="LAST") - - assert merged["primary"] == "#new" - assert merged["secondary"] == "#keep" - assert merged["tertiary"] == "#added" - - -class TestComponents: - """Tests for component operations.""" - - def test_upsert_components(self, temp_storage): - """Test bulk component upsert.""" - json_store.Projects.create(id="comp-proj", name="Component Project") - - count = json_store.Components.upsert("comp-proj", [ - {"name": "Button", "properties": {"variant": "primary"}}, - {"name": "Card", "properties": {"shadow": "md"}} - ]) - - assert count == 2 - - def test_list_components(self, temp_storage): - """Test listing components.""" - json_store.Projects.create(id="list-comp", name="List Components") - json_store.Components.upsert("list-comp", [ - {"name": "Button"}, - {"name": "Input"} - ]) - - components = json_store.Components.list("list-comp") - - assert len(components) == 2 - names = [c["name"] for c in components] - assert "Button" in names - assert "Input" in names - - -class TestActivityLog: - """Tests for activity logging.""" - - def test_log_activity(self, temp_storage): - """Test logging an activity.""" - json_store.ActivityLog.log( - action="test_action", - entity_type="test", - entity_name="Test Entity", - project_id="test-proj" - ) - - recent = json_store.ActivityLog.recent(limit=1) - - assert len(recent) == 1 - assert recent[0]["action"] == "test_action" - - def test_activity_auto_category(self, temp_storage): - """Test that activity auto-detects category.""" - json_store.ActivityLog.log(action="extract_tokens") - - recent = json_store.ActivityLog.recent(limit=1) - - assert recent[0]["category"] == "design_system" - - -class TestTeams: - """Tests for team operations.""" - - def test_create_team(self, temp_storage): - """Test team creation.""" - team = json_store.Teams.create( - id="test-team", - name="Test Team", - description="A test team" - ) - - assert team["id"] == "test-team" - assert team["name"] == "Test Team" - - def test_add_member(self, temp_storage): - """Test adding team member.""" - json_store.Teams.create(id="member-team", name="Member Team") - json_store.Teams.add_member("member-team", "user-123", "DEVELOPER") - - members = json_store.Teams.get_members("member-team") - - assert len(members) == 1 - assert members[0]["user_id"] == "user-123" - assert members[0]["role"] == "DEVELOPER" - - def test_get_user_role(self, temp_storage): - """Test getting user role in team.""" - json_store.Teams.create(id="role-team", name="Role Team") - json_store.Teams.add_member("role-team", "admin-user", "SUPER_ADMIN") - - role = json_store.Teams.get_user_role("role-team", "admin-user") - - assert role == "SUPER_ADMIN" - - -class TestSyncHistory: - """Tests for sync history.""" - - def test_sync_lifecycle(self, temp_storage): - """Test sync start and complete.""" - json_store.Projects.create(id="sync-proj", name="Sync Project") - - sync_id = json_store.SyncHistory.start("sync-proj", "tokens") - json_store.SyncHistory.complete("sync-proj", sync_id, "success", items_synced=10) - - recent = json_store.SyncHistory.recent("sync-proj", limit=5) - - # Should have both start and complete records - completed = [r for r in recent if r.get("status") == "success"] - assert len(completed) >= 1 - - -class TestStats: - """Tests for storage statistics.""" - - def test_get_stats(self, temp_storage): - """Test getting storage stats.""" - json_store.Projects.create(id="stats-proj", name="Stats Project") - json_store.Teams.create(id="stats-team", name="Stats Team") - - stats = json_store.get_stats() - - # Stats count directories, verify basic structure - assert "projects" in stats - assert "teams" in stats - assert "total_size_mb" in stats - assert stats["total_size_mb"] >= 0 diff --git a/tests/test_merge.py b/tests/test_merge.py deleted file mode 100644 index b6557ba..0000000 --- a/tests/test_merge.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -Tests for token merging and conflict resolution. -""" - -import pytest -from tools.ingest.merge import TokenMerger, MergeStrategy -from tools.ingest.base import TokenCollection, DesignToken, TokenType - - -def test_merge_no_conflicts(): - """Test merging collections with no conflicts.""" - col1 = TokenCollection([ - DesignToken(name="color.red", value="#FF0000", type=TokenType.COLOR) - ]) - col2 = TokenCollection([ - DesignToken(name="color.blue", value="#0000FF", type=TokenType.COLOR) - ]) - - merger = TokenMerger(strategy=MergeStrategy.LAST) - result = merger.merge([col1, col2]) - - assert len(result.collection.tokens) == 2 - assert len(result.conflicts) == 0 - - -def test_merge_strategy_first(): - """Test FIRST merge strategy.""" - col1 = TokenCollection([ - DesignToken(name="color.primary", value="#FF0000", type=TokenType.COLOR, source="css") - ]) - col2 = TokenCollection([ - DesignToken(name="color.primary", value="#0000FF", type=TokenType.COLOR, source="figma") - ]) - - merger = TokenMerger(strategy=MergeStrategy.FIRST) - result = merger.merge([col1, col2]) - - assert len(result.collection.tokens) == 1 - assert len(result.conflicts) == 1 - # Should keep first value - token = result.collection.tokens[0] - assert token.value == "#FF0000" - assert token.source == "css" - - -def test_merge_strategy_last(): - """Test LAST merge strategy.""" - col1 = TokenCollection([ - DesignToken(name="color.primary", value="#FF0000", type=TokenType.COLOR, source="css") - ]) - col2 = TokenCollection([ - DesignToken(name="color.primary", value="#0000FF", type=TokenType.COLOR, source="figma") - ]) - - merger = TokenMerger(strategy=MergeStrategy.LAST) - result = merger.merge([col1, col2]) - - assert len(result.collection.tokens) == 1 - assert len(result.conflicts) == 1 - # Should keep last value - token = result.collection.tokens[0] - assert token.value == "#0000FF" - assert token.source == "figma" - - -def test_merge_strategy_prefer_figma(): - """Test PREFER_FIGMA merge strategy.""" - col1 = TokenCollection([ - DesignToken(name="color.primary", value="#FF0000", type=TokenType.COLOR, source="css") - ]) - col2 = TokenCollection([ - DesignToken(name="color.primary", value="#3B82F6", type=TokenType.COLOR, source="figma") - ]) - col3 = TokenCollection([ - DesignToken(name="color.primary", value="#0000FF", type=TokenType.COLOR, source="scss") - ]) - - merger = TokenMerger(strategy=MergeStrategy.PREFER_FIGMA) - result = merger.merge([col1, col2, col3]) - - # Should prefer Figma value - token = result.collection.tokens[0] - assert token.value == "#3B82F6" - assert token.source == "figma" - - -def test_merge_multiple_collections(): - """Test merging many collections.""" - collections = [ - TokenCollection([ - DesignToken(name=f"color.{i}", value=f"#{i:06X}", type=TokenType.COLOR) - ]) - for i in range(10) - ] - - merger = TokenMerger(strategy=MergeStrategy.LAST) - result = merger.merge(collections) - - assert len(result.collection.tokens) == 10 - assert len(result.conflicts) == 0 - - -def test_merge_empty_collections(): - """Test merging empty collections.""" - merger = TokenMerger(strategy=MergeStrategy.LAST) - result = merger.merge([]) - - assert len(result.collection.tokens) == 0 - assert len(result.conflicts) == 0 diff --git a/dss-mvp1/tests/test_project_analyzer.py b/tests/test_project_analyzer.py similarity index 100% rename from dss-mvp1/tests/test_project_analyzer.py rename to tests/test_project_analyzer.py diff --git a/tools/analysis/project_analyzer.py b/tools/analysis/project_analyzer.py deleted file mode 100644 index 1f34ad8..0000000 --- a/tools/analysis/project_analyzer.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -import json -import networkx as nx -from pyast_ts import parse -import cssutils -import logging - -# Configure logging -logging.basicConfig(level=logging.INFO) -log = logging.getLogger(__name__) - -# Configure cssutils to ignore noisy error messages -cssutils.log.setLevel(logging.CRITICAL) - -def analyze_react_project(project_path: str) -> dict: - """ - Analyzes a React project, building a graph of its components and styles. - - Args: - project_path: The root path of the React project. - - Returns: - A dictionary containing the component graph and analysis report. - """ - log.info(f"Starting analysis of project at: {project_path}") - graph = nx.DiGraph() - - # Supported extensions for react/js/ts files - supported_exts = ('.js', '.jsx', '.ts', '.tsx') - - for root, _, files in os.walk(project_path): - for file in files: - file_path = os.path.join(root, file) - relative_path = os.path.relpath(file_path, project_path) - - if file.endswith(supported_exts): - graph.add_node(relative_path, type='file', language='typescript') - try: - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - # Placeholder for AST parsing and analysis - # ast = parse(content) - # For now, we'll just add the node - - except Exception as e: - log.error(f"Could not process file {file_path}: {e}") - - elif file.endswith('.css'): - graph.add_node(relative_path, type='file', language='css') - try: - # Placeholder for CSS parsing - # sheet = cssutils.parseFile(file_path) - pass - except Exception as e: - log.error(f"Could not parse css file {file_path}: {e}") - - log.info(f"Analysis complete. Found {graph.number_of_nodes()} files.") - - # Convert graph to a serializable format - serializable_graph = nx.node_link_data(graph) - - return serializable_graph - -def save_analysis(project_path: str, analysis_data: dict): - """ - Saves the analysis data to a file in the project's .dss directory. - """ - dss_dir = os.path.join(project_path, '.dss') - os.makedirs(dss_dir, exist_ok=True) - - output_path = os.path.join(dss_dir, 'analysis_graph.json') - - with open(output_path, 'w', encoding='utf-8') as f: - json.dump(analysis_data, f, indent=2) - - log.info(f"Analysis data saved to {output_path}") - -if __name__ == '__main__': - # Example usage: - # Replace '.' with the actual path to a React project for testing. - # In a real scenario, this would be called by the MCP. - target_project_path = '.' - analysis_result = analyze_react_project(target_project_path) - save_analysis(target_project_path, analysis_result) diff --git a/tools/analyze/__init__.py b/tools/analyze/__init__.py deleted file mode 100644 index 4ddb7f0..0000000 --- a/tools/analyze/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -DSS Code Analysis Module - -Provides tools for analyzing React projects, detecting style patterns, -building dependency graphs, and identifying quick-win improvements. -""" - -from .base import ( - ProjectAnalysis, - StylePattern, - QuickWin, - QuickWinType, - QuickWinPriority, - Location, - ComponentInfo, - StyleFile, -) -from .scanner import ProjectScanner -from .react import ReactAnalyzer -from .styles import StyleAnalyzer -from .graph import DependencyGraph -from .quick_wins import QuickWinFinder - -__all__ = [ - # Data classes - "ProjectAnalysis", - "StylePattern", - "QuickWin", - "QuickWinType", - "QuickWinPriority", - "Location", - "ComponentInfo", - "StyleFile", - # Analyzers - "ProjectScanner", - "ReactAnalyzer", - "StyleAnalyzer", - "DependencyGraph", - "QuickWinFinder", -] diff --git a/tools/analyze/base.py b/tools/analyze/base.py deleted file mode 100644 index 9ba13f7..0000000 --- a/tools/analyze/base.py +++ /dev/null @@ -1,298 +0,0 @@ -""" -Base classes and data structures for code analysis. -""" - -from dataclasses import dataclass, field -from datetime import datetime -from enum import Enum -from typing import List, Dict, Any, Optional, Set -from pathlib import Path - - -class QuickWinType(str, Enum): - """Types of quick-win improvements.""" - INLINE_STYLE = "inline_style" # Inline styles that can be extracted - DUPLICATE_VALUE = "duplicate_value" # Duplicate color/spacing values - UNUSED_STYLE = "unused_style" # Unused CSS/SCSS - HARDCODED_VALUE = "hardcoded_value" # Hardcoded values that should be tokens - NAMING_INCONSISTENCY = "naming" # Inconsistent naming patterns - DEPRECATED_PATTERN = "deprecated" # Deprecated styling patterns - ACCESSIBILITY = "accessibility" # A11y improvements - PERFORMANCE = "performance" # Performance improvements - - -class QuickWinPriority(str, Enum): - """Priority levels for quick-wins.""" - CRITICAL = "critical" # Must fix - breaking issues - HIGH = "high" # Should fix - significant improvement - MEDIUM = "medium" # Nice to fix - moderate improvement - LOW = "low" # Optional - minor improvement - - -class StylingApproach(str, Enum): - """Detected styling approaches in a project.""" - CSS_MODULES = "css-modules" - STYLED_COMPONENTS = "styled-components" - EMOTION = "emotion" - TAILWIND = "tailwind" - INLINE_STYLES = "inline-styles" - CSS_IN_JS = "css-in-js" - SASS_SCSS = "sass-scss" - LESS = "less" - VANILLA_CSS = "vanilla-css" - CSS_VARIABLES = "css-variables" - - -class Framework(str, Enum): - """Detected UI frameworks.""" - REACT = "react" - NEXT = "next" - VUE = "vue" - NUXT = "nuxt" - ANGULAR = "angular" - SVELTE = "svelte" - SOLID = "solid" - UNKNOWN = "unknown" - - -@dataclass -class Location: - """Represents a location in source code.""" - file_path: str - line: int - column: int = 0 - end_line: Optional[int] = None - end_column: Optional[int] = None - - def __str__(self) -> str: - return f"{self.file_path}:{self.line}" - - def to_dict(self) -> Dict[str, Any]: - return { - "file": self.file_path, - "line": self.line, - "column": self.column, - "end_line": self.end_line, - "end_column": self.end_column, - } - - -@dataclass -class StyleFile: - """Represents a style file in the project.""" - path: str - type: str # css, scss, less, styled, etc. - size_bytes: int = 0 - line_count: int = 0 - variable_count: int = 0 - selector_count: int = 0 - imports: List[str] = field(default_factory=list) - imported_by: List[str] = field(default_factory=list) - - def to_dict(self) -> Dict[str, Any]: - return { - "path": self.path, - "type": self.type, - "size_bytes": self.size_bytes, - "line_count": self.line_count, - "variable_count": self.variable_count, - "selector_count": self.selector_count, - "imports": self.imports, - "imported_by": self.imported_by, - } - - -@dataclass -class ComponentInfo: - """Information about a React component.""" - name: str - path: str - type: str = "functional" # functional, class, forwardRef, memo - props: List[str] = field(default_factory=list) - has_styles: bool = False - style_files: List[str] = field(default_factory=list) - inline_style_count: int = 0 - imports: List[str] = field(default_factory=list) - exports: List[str] = field(default_factory=list) - children: List[str] = field(default_factory=list) # Child components used - line_count: int = 0 - - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "path": self.path, - "type": self.type, - "props": self.props, - "has_styles": self.has_styles, - "style_files": self.style_files, - "inline_style_count": self.inline_style_count, - "imports": self.imports, - "exports": self.exports, - "children": self.children, - "line_count": self.line_count, - } - - -@dataclass -class StylePattern: - """A detected style pattern in code.""" - type: StylingApproach - locations: List[Location] = field(default_factory=list) - count: int = 0 - examples: List[str] = field(default_factory=list) - - def to_dict(self) -> Dict[str, Any]: - return { - "type": self.type.value, - "count": self.count, - "locations": [loc.to_dict() for loc in self.locations[:10]], - "examples": self.examples[:5], - } - - -@dataclass -class TokenCandidate: - """A value that could be extracted as a design token.""" - value: str # The actual value (e.g., "#3B82F6") - suggested_name: str # Suggested token name - category: str # colors, spacing, typography, etc. - occurrences: int = 1 # How many times it appears - locations: List[Location] = field(default_factory=list) - confidence: float = 0.0 # 0-1 confidence score - - def to_dict(self) -> Dict[str, Any]: - return { - "value": self.value, - "suggested_name": self.suggested_name, - "category": self.category, - "occurrences": self.occurrences, - "locations": [loc.to_dict() for loc in self.locations[:5]], - "confidence": self.confidence, - } - - -@dataclass -class QuickWin: - """A quick improvement opportunity.""" - type: QuickWinType - priority: QuickWinPriority - title: str - description: str - location: Optional[Location] = None - affected_files: List[str] = field(default_factory=list) - estimated_impact: str = "" # e.g., "Remove 50 lines of duplicate code" - fix_suggestion: str = "" # Suggested fix - auto_fixable: bool = False # Can be auto-fixed - - def to_dict(self) -> Dict[str, Any]: - return { - "type": self.type.value, - "priority": self.priority.value, - "title": self.title, - "description": self.description, - "location": self.location.to_dict() if self.location else None, - "affected_files": self.affected_files, - "estimated_impact": self.estimated_impact, - "fix_suggestion": self.fix_suggestion, - "auto_fixable": self.auto_fixable, - } - - -@dataclass -class ProjectAnalysis: - """Complete analysis result for a project.""" - # Basic info - project_path: str - analyzed_at: datetime = field(default_factory=datetime.now) - - # Framework detection - framework: Framework = Framework.UNKNOWN - framework_version: str = "" - - # Styling detection - styling_approaches: List[StylePattern] = field(default_factory=list) - primary_styling: Optional[StylingApproach] = None - - # Components - components: List[ComponentInfo] = field(default_factory=list) - component_count: int = 0 - - # Style files - style_files: List[StyleFile] = field(default_factory=list) - style_file_count: int = 0 - - # Issues and opportunities - inline_style_locations: List[Location] = field(default_factory=list) - token_candidates: List[TokenCandidate] = field(default_factory=list) - quick_wins: List[QuickWin] = field(default_factory=list) - - # Dependency graph - dependency_graph: Dict[str, List[str]] = field(default_factory=dict) - - # Statistics - stats: Dict[str, Any] = field(default_factory=dict) - - def __post_init__(self): - if not self.stats: - self.stats = { - "total_files_scanned": 0, - "total_lines": 0, - "component_count": 0, - "style_file_count": 0, - "inline_style_count": 0, - "token_candidates": 0, - "quick_wins_count": 0, - } - - def to_dict(self) -> Dict[str, Any]: - return { - "project_path": self.project_path, - "analyzed_at": self.analyzed_at.isoformat(), - "framework": self.framework.value, - "framework_version": self.framework_version, - "styling_approaches": [sp.to_dict() for sp in self.styling_approaches], - "primary_styling": self.primary_styling.value if self.primary_styling else None, - "component_count": self.component_count, - "style_file_count": self.style_file_count, - "inline_style_count": len(self.inline_style_locations), - "token_candidates_count": len(self.token_candidates), - "quick_wins_count": len(self.quick_wins), - "stats": self.stats, - } - - def summary(self) -> str: - """Generate human-readable summary.""" - lines = [ - f"Project Analysis: {self.project_path}", - "=" * 50, - f"Framework: {self.framework.value} {self.framework_version}", - f"Components: {self.component_count}", - f"Style files: {self.style_file_count}", - "", - "Styling Approaches:", - ] - - for sp in self.styling_approaches: - lines.append(f" • {sp.type.value}: {sp.count} occurrences") - - lines.extend([ - "", - f"Inline styles found: {len(self.inline_style_locations)}", - f"Token candidates: {len(self.token_candidates)}", - f"Quick wins: {len(self.quick_wins)}", - "", - "Quick Wins by Priority:", - ]) - - by_priority = {} - for qw in self.quick_wins: - if qw.priority not in by_priority: - by_priority[qw.priority] = [] - by_priority[qw.priority].append(qw) - - for priority in [QuickWinPriority.CRITICAL, QuickWinPriority.HIGH, - QuickWinPriority.MEDIUM, QuickWinPriority.LOW]: - if priority in by_priority: - lines.append(f" [{priority.value.upper()}] {len(by_priority[priority])} items") - - return "\n".join(lines) diff --git a/tools/analyze/graph.py b/tools/analyze/graph.py deleted file mode 100644 index 1a5ed5d..0000000 --- a/tools/analyze/graph.py +++ /dev/null @@ -1,419 +0,0 @@ -""" -Dependency Graph Builder - -Builds component and style dependency graphs for visualization -and analysis of project structure. -""" - -import re -import json -from pathlib import Path -from typing import List, Dict, Any, Optional, Set, Tuple -from dataclasses import dataclass, field -from collections import defaultdict - - -@dataclass -class GraphNode: - """A node in the dependency graph.""" - id: str - name: str - type: str # 'component', 'style', 'util', 'hook' - path: str - size: int = 0 # file size or importance metric - children: List[str] = field(default_factory=list) - parents: List[str] = field(default_factory=list) - metadata: Dict[str, Any] = field(default_factory=dict) - - def to_dict(self) -> Dict[str, Any]: - return { - 'id': self.id, - 'name': self.name, - 'type': self.type, - 'path': self.path, - 'size': self.size, - 'children': self.children, - 'parents': self.parents, - 'metadata': self.metadata, - } - - -@dataclass -class GraphEdge: - """An edge in the dependency graph.""" - source: str - target: str - type: str # 'import', 'uses', 'styles' - weight: int = 1 - - def to_dict(self) -> Dict[str, Any]: - return { - 'source': self.source, - 'target': self.target, - 'type': self.type, - 'weight': self.weight, - } - - -class DependencyGraph: - """ - Builds and analyzes dependency graphs for a project. - - Tracks: - - Component imports/exports - - Style file dependencies - - Component usage relationships - """ - - def __init__(self, root_path: str): - self.root = Path(root_path).resolve() - self.nodes: Dict[str, GraphNode] = {} - self.edges: List[GraphEdge] = [] - - async def build(self, depth: int = 3) -> Dict[str, Any]: - """ - Build the full dependency graph. - - Args: - depth: Maximum depth for traversing dependencies - - Returns: - Graph representation with nodes and edges - """ - # Clear existing graph - self.nodes.clear() - self.edges.clear() - - # Find all relevant files - await self._scan_files() - - # Build edges from imports - await self._build_import_edges() - - # Build edges from component usage - await self._build_usage_edges() - - return self.to_dict() - - async def _scan_files(self) -> None: - """Scan project files and create nodes.""" - skip_dirs = {'node_modules', '.git', 'dist', 'build', '.next'} - - # Component files - for ext in ['*.jsx', '*.tsx']: - for file_path in self.root.rglob(ext): - if any(skip in file_path.parts for skip in skip_dirs): - continue - - rel_path = str(file_path.relative_to(self.root)) - node_id = self._path_to_id(rel_path) - - self.nodes[node_id] = GraphNode( - id=node_id, - name=file_path.stem, - type='component', - path=rel_path, - size=file_path.stat().st_size, - ) - - # Style files - for ext in ['*.css', '*.scss', '*.sass', '*.less']: - for file_path in self.root.rglob(ext): - if any(skip in file_path.parts for skip in skip_dirs): - continue - - rel_path = str(file_path.relative_to(self.root)) - node_id = self._path_to_id(rel_path) - - self.nodes[node_id] = GraphNode( - id=node_id, - name=file_path.stem, - type='style', - path=rel_path, - size=file_path.stat().st_size, - ) - - # Utility/Hook files - for ext in ['*.js', '*.ts']: - for file_path in self.root.rglob(ext): - if any(skip in file_path.parts for skip in skip_dirs): - continue - - name = file_path.stem.lower() - rel_path = str(file_path.relative_to(self.root)) - node_id = self._path_to_id(rel_path) - - # Classify file type - if 'hook' in name or name.startswith('use'): - node_type = 'hook' - elif any(x in name for x in ['util', 'helper', 'lib']): - node_type = 'util' - else: - continue # Skip other JS/TS files - - self.nodes[node_id] = GraphNode( - id=node_id, - name=file_path.stem, - type=node_type, - path=rel_path, - size=file_path.stat().st_size, - ) - - async def _build_import_edges(self) -> None: - """Build edges from import statements.""" - import_pattern = re.compile( - r'import\s+(?:\{[^}]+\}|\*\s+as\s+\w+|\w+)?\s*(?:,\s*\{[^}]+\})?\s*from\s+["\']([^"\']+)["\']', - re.MULTILINE - ) - - for node_id, node in self.nodes.items(): - if node.type not in ['component', 'hook', 'util']: - continue - - file_path = self.root / node.path - if not file_path.exists(): - continue - - try: - content = file_path.read_text(encoding='utf-8', errors='ignore') - - for match in import_pattern.finditer(content): - import_path = match.group(1) - - # Resolve relative imports - target_id = self._resolve_import(node.path, import_path) - - if target_id and target_id in self.nodes: - # Add edge - self.edges.append(GraphEdge( - source=node_id, - target=target_id, - type='import', - )) - - # Update parent/child relationships - node.children.append(target_id) - self.nodes[target_id].parents.append(node_id) - - except Exception: - continue - - async def _build_usage_edges(self) -> None: - """Build edges from component usage in JSX.""" - # Pattern to find JSX component usage - jsx_pattern = re.compile(r'<([A-Z][A-Za-z0-9]*)') - - # Build name -> id mapping for components - name_to_id = {} - for node_id, node in self.nodes.items(): - if node.type == 'component': - name_to_id[node.name] = node_id - - for node_id, node in self.nodes.items(): - if node.type != 'component': - continue - - file_path = self.root / node.path - if not file_path.exists(): - continue - - try: - content = file_path.read_text(encoding='utf-8', errors='ignore') - - used_components = set() - for match in jsx_pattern.finditer(content): - comp_name = match.group(1) - if comp_name in name_to_id and name_to_id[comp_name] != node_id: - used_components.add(name_to_id[comp_name]) - - for target_id in used_components: - self.edges.append(GraphEdge( - source=node_id, - target=target_id, - type='uses', - )) - - except Exception: - continue - - def _path_to_id(self, path: str) -> str: - """Convert file path to node ID.""" - # Remove extension and normalize - path = re.sub(r'\.(jsx?|tsx?|css|scss|sass|less)$', '', path) - return path.replace('/', '_').replace('\\', '_').replace('.', '_') - - def _resolve_import(self, source_path: str, import_path: str) -> Optional[str]: - """Resolve import path to node ID.""" - if not import_path.startswith('.'): - return None # Skip node_modules imports - - source_dir = Path(source_path).parent - - # Handle various import patterns - if import_path.startswith('./'): - resolved = source_dir / import_path[2:] - elif import_path.startswith('../'): - resolved = source_dir / import_path - else: - resolved = source_dir / import_path - - # Try to resolve with extensions - extensions = ['.tsx', '.ts', '.jsx', '.js', '.css', '.scss', '/index.tsx', '/index.ts', '/index.jsx', '/index.js'] - - resolved_str = str(resolved) - for ext in extensions: - test_id = self._path_to_id(resolved_str + ext) - if test_id in self.nodes: - return test_id - - # Try without additional extension (if path already has one) - test_id = self._path_to_id(resolved_str) - if test_id in self.nodes: - return test_id - - return None - - def to_dict(self) -> Dict[str, Any]: - """Convert graph to dictionary for serialization.""" - return { - 'nodes': [node.to_dict() for node in self.nodes.values()], - 'edges': [edge.to_dict() for edge in self.edges], - 'stats': { - 'total_nodes': len(self.nodes), - 'total_edges': len(self.edges), - 'components': len([n for n in self.nodes.values() if n.type == 'component']), - 'styles': len([n for n in self.nodes.values() if n.type == 'style']), - 'hooks': len([n for n in self.nodes.values() if n.type == 'hook']), - 'utils': len([n for n in self.nodes.values() if n.type == 'util']), - } - } - - def to_json(self, pretty: bool = True) -> str: - """Convert graph to JSON string.""" - return json.dumps(self.to_dict(), indent=2 if pretty else None) - - def get_component_tree(self) -> Dict[str, List[str]]: - """Get simplified component dependency tree.""" - tree = {} - for node_id, node in self.nodes.items(): - if node.type == 'component': - tree[node.name] = [ - self.nodes[child_id].name - for child_id in node.children - if child_id in self.nodes and self.nodes[child_id].type == 'component' - ] - return tree - - def find_orphans(self) -> List[str]: - """Find components with no parents (not imported anywhere).""" - orphans = [] - for node_id, node in self.nodes.items(): - if node.type == 'component' and not node.parents: - # Exclude entry points (index, App, etc.) - if node.name.lower() not in ['app', 'index', 'main', 'root']: - orphans.append(node.path) - return orphans - - def find_hubs(self, min_connections: int = 5) -> List[Dict[str, Any]]: - """Find highly connected nodes (potential refactoring targets).""" - hubs = [] - for node_id, node in self.nodes.items(): - connections = len(node.children) + len(node.parents) - if connections >= min_connections: - hubs.append({ - 'name': node.name, - 'path': node.path, - 'type': node.type, - 'imports': len(node.children), - 'imported_by': len(node.parents), - 'total_connections': connections, - }) - - hubs.sort(key=lambda x: x['total_connections'], reverse=True) - return hubs - - def find_circular_dependencies(self) -> List[List[str]]: - """Find circular dependency chains.""" - cycles = [] - visited = set() - rec_stack = set() - - def dfs(node_id: str, path: List[str]) -> None: - visited.add(node_id) - rec_stack.add(node_id) - path.append(node_id) - - for child_id in self.nodes.get(node_id, GraphNode('', '', '', '')).children: - if child_id not in visited: - dfs(child_id, path.copy()) - elif child_id in rec_stack: - # Found cycle - cycle_start = path.index(child_id) - cycle = path[cycle_start:] + [child_id] - cycles.append([self.nodes[n].name for n in cycle]) - - rec_stack.remove(node_id) - - for node_id in self.nodes: - if node_id not in visited: - dfs(node_id, []) - - return cycles - - def get_subgraph(self, node_id: str, depth: int = 2) -> Dict[str, Any]: - """Get subgraph centered on a specific node.""" - if node_id not in self.nodes: - return {'nodes': [], 'edges': []} - - # BFS to find nodes within depth - included_nodes = {node_id} - frontier = {node_id} - - for _ in range(depth): - new_frontier = set() - for nid in frontier: - node = self.nodes.get(nid) - if node: - new_frontier.update(node.children) - new_frontier.update(node.parents) - included_nodes.update(new_frontier) - frontier = new_frontier - - # Filter nodes and edges - subgraph_nodes = [ - self.nodes[nid].to_dict() - for nid in included_nodes - if nid in self.nodes - ] - - subgraph_edges = [ - edge.to_dict() - for edge in self.edges - if edge.source in included_nodes and edge.target in included_nodes - ] - - return { - 'nodes': subgraph_nodes, - 'edges': subgraph_edges, - 'center': node_id, - 'depth': depth, - } - - def get_style_dependencies(self) -> Dict[str, List[str]]: - """Get mapping of components to their style dependencies.""" - style_deps = {} - - for node_id, node in self.nodes.items(): - if node.type != 'component': - continue - - style_children = [ - self.nodes[child_id].path - for child_id in node.children - if child_id in self.nodes and self.nodes[child_id].type == 'style' - ] - - if style_children: - style_deps[node.path] = style_children - - return style_deps diff --git a/tools/analyze/quick_wins.py b/tools/analyze/quick_wins.py deleted file mode 100644 index 296aaa2..0000000 --- a/tools/analyze/quick_wins.py +++ /dev/null @@ -1,418 +0,0 @@ -""" -Quick-Win Finder - -Identifies easy improvement opportunities in a codebase: -- Inline styles that can be extracted -- Duplicate values that should be tokens -- Unused styles -- Naming inconsistencies -- Accessibility issues -""" - -import re -from pathlib import Path -from typing import List, Dict, Any, Optional -from dataclasses import dataclass - -from .base import ( - QuickWin, - QuickWinType, - QuickWinPriority, - Location, - ProjectAnalysis, -) -from .styles import StyleAnalyzer -from .react import ReactAnalyzer - - -class QuickWinFinder: - """ - Finds quick improvement opportunities in a project. - - Categories: - - INLINE_STYLE: Inline styles that can be extracted to CSS/tokens - - DUPLICATE_VALUE: Repeated values that should be tokens - - UNUSED_STYLE: CSS that's defined but not used - - HARDCODED_VALUE: Magic numbers/colors that should be tokens - - NAMING_INCONSISTENCY: Inconsistent naming patterns - - DEPRECATED_PATTERN: Outdated styling approaches - - ACCESSIBILITY: A11y improvements - """ - - def __init__(self, root_path: str): - self.root = Path(root_path).resolve() - self.style_analyzer = StyleAnalyzer(root_path) - self.react_analyzer = ReactAnalyzer(root_path) - - async def find_all(self) -> List[QuickWin]: - """ - Find all quick-win opportunities. - - Returns: - List of QuickWin objects sorted by priority - """ - quick_wins = [] - - # Find inline styles - inline_wins = await self._find_inline_style_wins() - quick_wins.extend(inline_wins) - - # Find duplicate values - duplicate_wins = await self._find_duplicate_value_wins() - quick_wins.extend(duplicate_wins) - - # Find unused styles - unused_wins = await self._find_unused_style_wins() - quick_wins.extend(unused_wins) - - # Find hardcoded values - hardcoded_wins = await self._find_hardcoded_value_wins() - quick_wins.extend(hardcoded_wins) - - # Find naming inconsistencies - naming_wins = await self._find_naming_inconsistency_wins() - quick_wins.extend(naming_wins) - - # Find accessibility issues - a11y_wins = await self._find_accessibility_wins() - quick_wins.extend(a11y_wins) - - # Sort by priority - priority_order = { - QuickWinPriority.CRITICAL: 0, - QuickWinPriority.HIGH: 1, - QuickWinPriority.MEDIUM: 2, - QuickWinPriority.LOW: 3, - } - quick_wins.sort(key=lambda x: priority_order[x.priority]) - - return quick_wins - - async def _find_inline_style_wins(self) -> List[QuickWin]: - """Find inline styles that should be extracted.""" - wins = [] - - inline_styles = await self.react_analyzer.find_inline_styles() - - if not inline_styles: - return wins - - # Group by file - by_file = {} - for style in inline_styles: - file_path = style['file'] - if file_path not in by_file: - by_file[file_path] = [] - by_file[file_path].append(style) - - # Create quick-wins for files with multiple inline styles - for file_path, styles in by_file.items(): - if len(styles) >= 3: # Only flag if 3+ inline styles - wins.append(QuickWin( - type=QuickWinType.INLINE_STYLE, - priority=QuickWinPriority.HIGH, - title=f"Extract {len(styles)} inline styles", - description=f"File {file_path} has {len(styles)} inline style declarations that could be extracted to CSS classes or design tokens.", - location=Location(file_path, styles[0]['line']), - affected_files=[file_path], - estimated_impact=f"Reduce inline styles, improve maintainability", - fix_suggestion="Extract repeated style properties to CSS classes or design tokens. Use className instead of style prop.", - auto_fixable=True, - )) - - # Create summary if many files have inline styles - total_inline = len(inline_styles) - if total_inline >= 10: - wins.insert(0, QuickWin( - type=QuickWinType.INLINE_STYLE, - priority=QuickWinPriority.HIGH, - title=f"Project has {total_inline} inline styles", - description=f"Found {total_inline} inline style declarations across {len(by_file)} files. Consider migrating to CSS classes or design tokens.", - affected_files=list(by_file.keys())[:10], - estimated_impact=f"Improve code maintainability and bundle size", - fix_suggestion="Run 'dss migrate inline-styles' to preview migration options.", - auto_fixable=True, - )) - - return wins - - async def _find_duplicate_value_wins(self) -> List[QuickWin]: - """Find duplicate values that should be tokens.""" - wins = [] - - analysis = await self.style_analyzer.analyze() - duplicates = analysis.get('duplicates', []) - - # Find high-occurrence duplicates - for dup in duplicates[:10]: # Top 10 duplicates - if dup['count'] >= 5: # Only if used 5+ times - priority = QuickWinPriority.HIGH if dup['count'] >= 10 else QuickWinPriority.MEDIUM - - wins.append(QuickWin( - type=QuickWinType.DUPLICATE_VALUE, - priority=priority, - title=f"Duplicate value '{dup['value']}' used {dup['count']} times", - description=f"The value '{dup['value']}' appears {dup['count']} times across {len(dup['files'])} files. This should be a design token.", - affected_files=dup['files'], - estimated_impact=f"Create single source of truth, easier theme updates", - fix_suggestion=f"Create token for this value and replace all occurrences.", - auto_fixable=True, - )) - - return wins - - async def _find_unused_style_wins(self) -> List[QuickWin]: - """Find unused CSS styles.""" - wins = [] - - unused = await self.style_analyzer.find_unused_styles() - - if len(unused) >= 5: - wins.append(QuickWin( - type=QuickWinType.UNUSED_STYLE, - priority=QuickWinPriority.MEDIUM, - title=f"Found {len(unused)} potentially unused CSS classes", - description=f"These CSS classes are defined but don't appear to be used in the codebase. Review and remove if confirmed unused.", - affected_files=list(set(u['file'] for u in unused))[:10], - estimated_impact=f"Reduce CSS bundle size by removing dead code", - fix_suggestion="Review each class and remove if unused. Some may be dynamically generated.", - auto_fixable=False, # Needs human review - )) - - return wins - - async def _find_hardcoded_value_wins(self) -> List[QuickWin]: - """Find hardcoded magic values.""" - wins = [] - - analysis = await self.style_analyzer.analyze() - candidates = analysis.get('token_candidates', []) - - # Find high-confidence candidates - high_confidence = [c for c in candidates if c.confidence >= 0.7] - - if high_confidence: - wins.append(QuickWin( - type=QuickWinType.HARDCODED_VALUE, - priority=QuickWinPriority.MEDIUM, - title=f"Found {len(high_confidence)} values that should be tokens", - description="These hardcoded values appear multiple times and should be extracted as design tokens for consistency.", - estimated_impact="Improve theme consistency and make updates easier", - fix_suggestion="Use 'dss extract-tokens' to create tokens from these values.", - auto_fixable=True, - )) - - # Add specific wins for top candidates - for candidate in high_confidence[:5]: - wins.append(QuickWin( - type=QuickWinType.HARDCODED_VALUE, - priority=QuickWinPriority.LOW, - title=f"Extract '{candidate.value}' as token", - description=f"Value '{candidate.value}' appears {candidate.occurrences} times. Suggested token: {candidate.suggested_name}", - location=candidate.locations[0] if candidate.locations else None, - affected_files=[loc.file_path for loc in candidate.locations[:5]], - estimated_impact=f"Single source of truth for this value", - fix_suggestion=f"Create token '{candidate.suggested_name}' with value '{candidate.value}'", - auto_fixable=True, - )) - - return wins - - async def _find_naming_inconsistency_wins(self) -> List[QuickWin]: - """Find naming inconsistencies.""" - wins = [] - - naming = await self.style_analyzer.analyze_naming_consistency() - - if naming.get('inconsistencies'): - primary = naming.get('primary_pattern', 'unknown') - inconsistent_count = len(naming['inconsistencies']) - - wins.append(QuickWin( - type=QuickWinType.NAMING_INCONSISTENCY, - priority=QuickWinPriority.LOW, - title=f"Found {inconsistent_count} naming inconsistencies", - description=f"The project primarily uses {primary} naming, but {inconsistent_count} classes use different conventions.", - affected_files=list(set(i['file'] for i in naming['inconsistencies']))[:10], - estimated_impact="Improve code consistency and readability", - fix_suggestion=f"Standardize all class names to use {primary} convention.", - auto_fixable=True, - )) - - return wins - - async def _find_accessibility_wins(self) -> List[QuickWin]: - """Find accessibility issues.""" - wins = [] - skip_dirs = {'node_modules', '.git', 'dist', 'build'} - - a11y_issues = [] - - for ext in ['*.jsx', '*.tsx']: - for file_path in self.root.rglob(ext): - if any(skip in file_path.parts for skip in skip_dirs): - continue - - try: - content = file_path.read_text(encoding='utf-8', errors='ignore') - rel_path = str(file_path.relative_to(self.root)) - - # Check for images without alt - img_no_alt = re.findall(r']+(?]*>', content) - if img_no_alt: - for match in img_no_alt[:3]: - if 'alt=' not in match: - line = content[:content.find(match)].count('\n') + 1 - a11y_issues.append({ - 'type': 'img-no-alt', - 'file': rel_path, - 'line': line, - }) - - # Check for buttons without accessible text - icon_only_buttons = re.findall( - r']*>\s*<(?:svg|Icon|img)[^>]*/?>\s*', - content, - re.IGNORECASE - ) - if icon_only_buttons: - a11y_issues.append({ - 'type': 'icon-button-no-label', - 'file': rel_path, - }) - - # Check for click handlers on non-interactive elements - div_onclick = re.findall(r']+onClick', content) - if div_onclick: - a11y_issues.append({ - 'type': 'div-click-handler', - 'file': rel_path, - 'count': len(div_onclick), - }) - - except Exception: - continue - - # Group issues by type - if a11y_issues: - img_issues = [i for i in a11y_issues if i['type'] == 'img-no-alt'] - if img_issues: - wins.append(QuickWin( - type=QuickWinType.ACCESSIBILITY, - priority=QuickWinPriority.HIGH, - title=f"Found {len(img_issues)} images without alt text", - description="Images should have alt attributes for screen readers. Empty alt='' is acceptable for decorative images.", - affected_files=list(set(i['file'] for i in img_issues))[:10], - estimated_impact="Improve accessibility for screen reader users", - fix_suggestion="Add descriptive alt text to images or alt='' for decorative images.", - auto_fixable=False, - )) - - div_issues = [i for i in a11y_issues if i['type'] == 'div-click-handler'] - if div_issues: - wins.append(QuickWin( - type=QuickWinType.ACCESSIBILITY, - priority=QuickWinPriority.MEDIUM, - title=f"Found click handlers on div elements", - description="Using onClick on div elements makes them inaccessible to keyboard users. Use button or add proper ARIA attributes.", - affected_files=list(set(i['file'] for i in div_issues))[:10], - estimated_impact="Improve keyboard navigation accessibility", - fix_suggestion="Replace
with