diff --git a/.gitignore b/.gitignore index 4cad133b..fbd20ac1 100644 --- a/.gitignore +++ b/.gitignore @@ -126,6 +126,7 @@ Loading complete # Artifacts packages/theming/tests/e2e/*.css packages/theming/tests/e2e/*.css.map +packages/theming/tokens/output/ # Agents and related files .opencode diff --git a/biome.json b/biome.json index f8668ff7..c0e6b784 100644 --- a/biome.json +++ b/biome.json @@ -4,6 +4,7 @@ "includes": [ "packages/theming/**/*.{ts,js,mjs,cjs}", "packages/mcp/src/**/*.ts", + "packages/plugins/**/*.ts", "!!**/dist" ] }, diff --git a/package-lock.json b/package-lock.json index b5281b16..ad94e69c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,8 @@ "license": "MIT", "workspaces": [ "packages/theming", - "packages/mcp" + "packages/mcp", + "packages/plugins" ], "devDependencies": { "@biomejs/biome": "^2.4.9", @@ -451,6 +452,27 @@ "keyv": "^5.6.0" } }, + "node_modules/@clack/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.1.0.tgz", + "integrity": "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.1.0.tgz", + "integrity": "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/core": "1.1.0", + "sisteransi": "^1.0.5" + } + }, "node_modules/@csstools/css-calc": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", @@ -1104,6 +1126,20 @@ "hono": "^4" } }, + "node_modules/@humanwhocodes/momoa": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-3.3.10.tgz", + "integrity": "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@igniteui-theming/plugins": { + "resolved": "packages/plugins", + "link": true + }, "node_modules/@jest/diff-sequences": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", @@ -1433,6 +1469,16 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/runtime": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", + "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@oxc-project/types": { "version": "0.122.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", @@ -2031,6 +2077,356 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rushstack/node-core-library": { "version": "5.20.3", "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.20.3.tgz", @@ -2300,11 +2696,509 @@ "node": ">=6" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, + "node_modules/@terrazzo/cli": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@terrazzo/cli/-/cli-2.0.0.tgz", + "integrity": "sha512-0MYaj8CMmKeWxzYcbb8iCh63aFdQ4uYMNYgeiqyKmxkpeDFGox9lzZITgSc1hBO4VTnape7PrMcRE3QUy3u7bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/prompts": "^1.1.0", + "@hono/node-server": "^1.19.11", + "@humanwhocodes/momoa": "^3.3.10", + "@terrazzo/json-schema-tools": "^0.2.0", + "@terrazzo/parser": "^2.0.0", + "@terrazzo/token-tools": "^2.0.0", + "chokidar": "^5.0.0", + "detect-package-manager": "^3.0.2", + "dtcg-examples": "^1.0.3", + "escodegen": "^2.1.0", + "merge-anything": "^5.1.7", + "meriyah": "^7.1.0", + "mime": "^4.1.0", + "picocolors": "^1.1.1", + "scule": "^1.3.0", + "vite": "8.0.0-beta.16", + "vite-node": "^5.3.0", + "yaml": "^2.8.2", + "yaml-to-momoa": "0.0.9" + }, + "bin": { + "terrazzo": "bin/cli.js", + "tz": "bin/cli.js" + } + }, + "node_modules/@terrazzo/cli/node_modules/@oxc-project/types": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@terrazzo/cli/node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.6.tgz", + "integrity": "sha512-kvjTSWGcrv+BaR2vge57rsKiYdVR8V8CoS0vgKrc570qRBfty4bT+1X0z3j2TaVV+kAYzA0PjeB9+mdZyqUZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@terrazzo/cli/node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.6.tgz", + "integrity": "sha512-+tJhD21KvGNtUrpLXrZQlT+j5HZKiEwR2qtcZb3vNOUpvoT9QjEykr75ZW/Kr0W89gose/HVXU6351uVZD8Qvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@terrazzo/cli/node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.6.tgz", + "integrity": "sha512-DKNhjMk38FAWaHwUt1dFR3rA/qRAvn2NUvSG2UGvxvlMxSmN/qqww/j4ABAbXhNRXtGQNmrAINMXRuwHl16ZHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@terrazzo/cli/node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.6.tgz", + "integrity": "sha512-8TThsRkCPAnfyMBShxrGdtoOE6h36QepqRQI97iFaQSCRbHFWHcDHppcojZnzXoruuhPnjMEygzaykvPVJsMRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@terrazzo/cli/node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.6.tgz", + "integrity": "sha512-ZfmFoOwPUZCWtGOVC9/qbQzfc0249FrRUOzV2XabSMUV60Crp211OWLQN1zmQAsRIVWRcEwhJ46Z1mXGo/L/nQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@terrazzo/cli/node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.6.tgz", + "integrity": "sha512-ZsGzbNETxPodGlLTYHaCSGVhNN/rvkMDCJYHdT7PZr5jFJRmBfmDi2awhF64Dt2vxrJqY6VeeYSgOzEbHRsb7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@terrazzo/cli/node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.6.tgz", + "integrity": "sha512-elPpdevtCdUOqziemR86C4CSCr/5sUxalzDrf/CJdMT+kZt2C556as++qHikNOz0vuFf52h+GJNXZM08eWgGPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@terrazzo/cli/node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.6.tgz", + "integrity": "sha512-IBwXsf56o3xhzAyaZxdM1CX8UFiBEUFCjiVUgny67Q8vPIqkjzJj0YKhd3TbBHanuxThgBa59f6Pgutg2OGk5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@terrazzo/cli/node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.6.tgz", + "integrity": "sha512-vOk7G8V9Zm+8a6PL6JTpCea61q491oYlGtO6CvnsbhNLlKdf0bbCPytFzGQhYmCKZDKkEbmnkcIprTEGCURnwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@terrazzo/cli/node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.6.tgz", + "integrity": "sha512-ASjEDI4MRv7XCQb2JVaBzfEYO98JKCGrAgoW6M03fJzH/ilCnC43Mb3ptB9q/lzsaahoJyIBoAGKAYEjUvpyvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@terrazzo/cli/node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.6.tgz", + "integrity": "sha512-mYa1+h2l6Zc0LvmwUh0oXKKYihnw/1WC73vTqw+IgtfEtv47A+rWzzcWwVDkW73+UDr0d/Ie/HRXoaOY22pQDw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@terrazzo/cli/node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.6.tgz", + "integrity": "sha512-e2ABskbNH3MRUBMjgxaMjYIw11DSwjLJxBII3UgpF6WClGLIh8A20kamc+FKH5vIaFVnYQInmcLYSUVpqMPLow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@terrazzo/cli/node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.6.tgz", + "integrity": "sha512-dJVc3ifhaRXxIEh1xowLohzFrlQXkJ66LepHm+CmSprTWgVrPa8Fx3OL57xwIqDEH9hufcKkDX2v65rS3NZyRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@terrazzo/cli/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.6.tgz", + "integrity": "sha512-Y0+JT8Mi1mmW08K6HieG315XNRu4L0rkfCpA364HtytjgiqYnMYRdFPcxRl+BQQqNXzecL2S9nii+RUpO93XIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@terrazzo/cli/node_modules/@terrazzo/parser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@terrazzo/parser/-/parser-2.0.0.tgz", + "integrity": "sha512-de2bX/Vt76rNKGJfHliCv6LsiZMi15gTNbxzjz9vX5bZYald05VQFsrJdL/DnVIBYp9Z6CDZpAaB2G6VkWG88A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@humanwhocodes/momoa": "^3.3.10", + "@terrazzo/json-schema-tools": "^0.2.0", + "@terrazzo/token-tools": "^2.0.0", + "@types/babel__code-frame": "^7.27.0", + "colorjs.io": "^0.6.1", + "fast-deep-equal": "^3.1.3", + "merge-anything": "^5.1.7", + "picocolors": "^1.1.1", + "scule": "^1.3.0" + }, + "peerDependencies": { + "yaml-to-momoa": "0.0.9" + }, + "peerDependenciesMeta": { + "yaml-to-momoa": { + "optional": true + } + } + }, + "node_modules/@terrazzo/cli/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@terrazzo/cli/node_modules/colorjs.io": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.6.1.tgz", + "integrity": "sha512-8lyR2wHzuIykCpqHKgluGsqQi5iDm3/a2IgP2GBZrasn2sBRkE4NOGsglZxWLs/jZQoNkmA/KM/8NV16rLUdBg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/color" + } + }, + "node_modules/@terrazzo/cli/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@terrazzo/cli/node_modules/rolldown": { + "version": "1.0.0-rc.6", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.6.tgz", + "integrity": "sha512-B8vFPV1ADyegoYfhg+E7RAucYKv0xdVlwYYsIJgfPNeiSxZGWNxts9RqhyGzC11ULK/VaeXyKezGCwpMiH8Ktw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.115.0", + "@rolldown/pluginutils": "1.0.0-rc.6" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.6", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.6", + "@rolldown/binding-darwin-x64": "1.0.0-rc.6", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.6", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.6", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.6", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.6", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.6", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.6", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.6", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.6", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.6", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.6" + } + }, + "node_modules/@terrazzo/cli/node_modules/vite": { + "version": "8.0.0-beta.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0-beta.16.tgz", + "integrity": "sha512-c0t7hYkxsjws89HH+BUFh/sL3BpPNhNsL9CJrTpMxBmwKQBRSa5OJ5w4o9O0bQVI/H/vx7UpUUIevvXa37NS/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.115.0", + "lightningcss": "^1.31.1", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rolldown": "1.0.0-rc.6", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.0.0-alpha.31", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/@terrazzo/cli/node_modules/yaml-to-momoa": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/yaml-to-momoa/-/yaml-to-momoa-0.0.9.tgz", + "integrity": "sha512-HUsUaZv0yPTfwMQIYfGwuhNwqJXnT4vgdsuC1XJmP6PiGuzK3JXm1Mllz23bkca4h2Xq/i3hA5Hsmat1EAq9QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@humanwhocodes/momoa": "^3.3.10", + "yaml": "^2.8.2" + } + }, + "node_modules/@terrazzo/json-schema-tools": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@terrazzo/json-schema-tools/-/json-schema-tools-0.2.0.tgz", + "integrity": "sha512-k/yPRIImOjUUYV+P9Joy13DXxiUqt4t1SiFaHX+igWsmpzG5x1BdYo3T/SJKPEy7vb9gLdklOUS/GfdQpH8bnw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@humanwhocodes/momoa": "^3.0.0", + "yaml-to-momoa": "0.0.8" + }, + "peerDependenciesMeta": { + "yaml-to-momoa": { + "optional": true + } + } + }, + "node_modules/@terrazzo/token-tools": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@terrazzo/token-tools/-/token-tools-2.0.0.tgz", + "integrity": "sha512-HM+UUe1ykXWBSwF3oDQSBfybqTPyaWmFY5XVt6KAIzaMmFtd7muAfer8eAkuWMCm/cXImPX6kTNW0l2wGzWhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@humanwhocodes/momoa": "^3.3.10", + "colorjs.io": "^0.6.1", + "wildcard-match": "^5.1.4" + } + }, + "node_modules/@terrazzo/token-tools/node_modules/colorjs.io": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.6.1.tgz", + "integrity": "sha512-8lyR2wHzuIykCpqHKgluGsqQi5iDm3/a2IgP2GBZrasn2sBRkE4NOGsglZxWLs/jZQoNkmA/KM/8NV16rLUdBg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/color" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2318,6 +3212,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/babel__code-frame": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__code-frame/-/babel__code-frame-7.27.0.tgz", + "integrity": "sha512-Dwlo+LrxDx/0SpfmJ/BKveHf7QXWvLBLc+x03l5sbzykj3oB9nHygCpSECF1a+s+QIxbghe+KHqC90vGtxLRAA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -3066,6 +3967,16 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cacheable": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.4.tgz", @@ -3877,6 +4788,146 @@ "node": ">=8" } }, + "node_modules/detect-package-manager": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/detect-package-manager/-/detect-package-manager-3.0.2.tgz", + "integrity": "sha512-8JFjJHutStYrfWwzfretQoyNGoZVW1Fsrp4JO9spa7h/fBfwgTMEIy4/LBzRDGsxwVPHU0q+T9YvwLDJoOApLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/detect-package-manager/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/detect-package-manager/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/detect-package-manager/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/detect-package-manager/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/detect-package-manager/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-package-manager/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-package-manager/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-package-manager/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-package-manager/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/diff": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", @@ -3909,6 +4960,13 @@ "node": ">=8" } }, + "node_modules/dtcg-examples": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dtcg-examples/-/dtcg-examples-1.0.3.tgz", + "integrity": "sha512-gKHpa3kLAYDP8g36sDtjG6oLQHrxfSYm5HW97QIwo4C6d0XnhU5H+N2q0j++uSMuCAxAu17JUG6Q6T5e2T6N0g==", + "dev": true, + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4212,6 +5270,28 @@ "node": ">=0.8.0" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -4226,6 +5306,16 @@ "node": ">=4" } }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -4236,6 +5326,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -5306,6 +6406,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/husky": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", @@ -5726,6 +6836,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -6647,6 +7770,22 @@ "dev": true, "license": "MIT" }, + "node_modules/merge-anything": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", + "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -6659,6 +7798,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6669,6 +7815,16 @@ "node": ">= 8" } }, + "node_modules/meriyah": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/meriyah/-/meriyah-7.1.0.tgz", + "integrity": "sha512-4K/lV+RFSrM8vy9H58FSd+wrxrXlPhYOK8AOaNQ7iFaHugYRx4tHIAaRYLMtXx/spMByZ4S080di6lXSTDI9eg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -6696,6 +7852,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/mime": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -6721,6 +7893,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -7045,6 +8227,22 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", @@ -8027,6 +9225,51 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" } }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -8983,6 +10226,13 @@ "cdocparser": "^0.13.0" } }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true, + "license": "MIT" + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -9243,6 +10493,13 @@ "dev": true, "license": "ISC" }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/slash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", @@ -9394,6 +10651,16 @@ "node": ">=0.10.0" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -10627,6 +11894,104 @@ } } }, + "node_modules/vite-node": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-5.3.0.tgz", + "integrity": "sha512-8f20COPYJujc3OKPX6OuyBy3ZIv2det4eRRU4GY1y2MjbeGSUmPjedxg1b72KnTagCofwvZ65ThzjxDW2AtQFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "es-module-lexer": "^2.0.0", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "vite": "^7.3.1" + }, + "bin": { + "vite-node": "dist/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://opencollective.com/antfu" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/vite-plugin-dts": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-4.5.4.tgz", @@ -10824,6 +12189,13 @@ "node": ">=8" } }, + "node_modules/wildcard-match": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/wildcard-match/-/wildcard-match-5.1.4.tgz", + "integrity": "sha512-wldeCaczs8XXq7hj+5d/F38JE2r7EXgb6WQDM84RVwxy81T/sxB5e9+uZLK9Q9oNz1mlvjut+QtvgaOQFPVq/g==", + "dev": true, + "license": "ISC" + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -10890,6 +12262,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", @@ -10921,6 +12309,53 @@ "igniteui-theming": "*" } }, + "packages/plugins": { + "name": "@igniteui-theming/plugins", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@terrazzo/parser": "^2.0.0", + "immutable": "^5.1.5", + "sass-embedded": "1.92.1" + } + }, + "packages/plugins/node_modules/@terrazzo/parser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@terrazzo/parser/-/parser-2.0.0.tgz", + "integrity": "sha512-de2bX/Vt76rNKGJfHliCv6LsiZMi15gTNbxzjz9vX5bZYald05VQFsrJdL/DnVIBYp9Z6CDZpAaB2G6VkWG88A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@humanwhocodes/momoa": "^3.3.10", + "@terrazzo/json-schema-tools": "^0.2.0", + "@terrazzo/token-tools": "^2.0.0", + "@types/babel__code-frame": "^7.27.0", + "colorjs.io": "^0.6.1", + "fast-deep-equal": "^3.1.3", + "merge-anything": "^5.1.7", + "picocolors": "^1.1.1", + "scule": "^1.3.0" + }, + "peerDependencies": { + "yaml-to-momoa": "0.0.9" + }, + "peerDependenciesMeta": { + "yaml-to-momoa": { + "optional": true + } + } + }, + "packages/plugins/node_modules/colorjs.io": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.6.1.tgz", + "integrity": "sha512-8lyR2wHzuIykCpqHKgluGsqQi5iDm3/a2IgP2GBZrasn2sBRkE4NOGsglZxWLs/jZQoNkmA/KM/8NV16rLUdBg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/color" + } + }, "packages/theming": { "name": "igniteui-theming", "version": "1.0.0", @@ -10935,6 +12370,9 @@ }, "devDependencies": { "@ast-grep/cli": "^0.42.0", + "@igniteui-theming/plugins": "*", + "@terrazzo/cli": "^2.0.0", + "@terrazzo/parser": "^2.0.0", "@types/postcss-safe-parser": "^5.0.4", "igniteui-sassdoc-theme": "^1.1.6", "lunr": "^2.3.9", @@ -10947,6 +12385,43 @@ "stylelint-config-standard-scss": "^17.0.0", "stylelint-scss": "^7.0.0" } + }, + "packages/theming/node_modules/@terrazzo/parser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@terrazzo/parser/-/parser-2.0.0.tgz", + "integrity": "sha512-de2bX/Vt76rNKGJfHliCv6LsiZMi15gTNbxzjz9vX5bZYald05VQFsrJdL/DnVIBYp9Z6CDZpAaB2G6VkWG88A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@humanwhocodes/momoa": "^3.3.10", + "@terrazzo/json-schema-tools": "^0.2.0", + "@terrazzo/token-tools": "^2.0.0", + "@types/babel__code-frame": "^7.27.0", + "colorjs.io": "^0.6.1", + "fast-deep-equal": "^3.1.3", + "merge-anything": "^5.1.7", + "picocolors": "^1.1.1", + "scule": "^1.3.0" + }, + "peerDependencies": { + "yaml-to-momoa": "0.0.9" + }, + "peerDependenciesMeta": { + "yaml-to-momoa": { + "optional": true + } + } + }, + "packages/theming/node_modules/colorjs.io": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.6.1.tgz", + "integrity": "sha512-8lyR2wHzuIykCpqHKgluGsqQi5iDm3/a2IgP2GBZrasn2sBRkE4NOGsglZxWLs/jZQoNkmA/KM/8NV16rLUdBg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/color" + } } } } diff --git a/package.json b/package.json index 02a75bae..f8a1be34 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,11 @@ "type": "module", "workspaces": [ "packages/theming", - "packages/mcp" + "packages/mcp", + "packages/plugins" ], "scripts": { - "build": "npm run build -w packages/theming", + "build": "npm run build -w packages/theming && npm run build -w packages/plugins", "clean": "npm run clean --workspaces --if-present", "test": "vitest run", "lint": "npm run lint:biome && npm run lint --workspaces --if-present", diff --git a/packages/plugins/index.ts b/packages/plugins/index.ts new file mode 100644 index 00000000..405c33e8 --- /dev/null +++ b/packages/plugins/index.ts @@ -0,0 +1 @@ +export * from "./src/terrazzo/index.js"; diff --git a/packages/plugins/package.json b/packages/plugins/package.json new file mode 100644 index 00000000..aa42f7a0 --- /dev/null +++ b/packages/plugins/package.json @@ -0,0 +1,52 @@ +{ + "name": "@igniteui-theming/plugins", + "version": "1.0.0", + "private": true, + "description": "Plugins for Ignite UI Theming", + "type": "module", + "main": "dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/IgniteUI/igniteui-theming.git", + "directory": "packages/plugins" + }, + "keywords": [ + "Ignite", + "UI", + "theming", + "dtcg", + "design token", + "terrazzo", + "transform" + ], + "author": "Infragistics", + "license": "MIT", + "bugs": { + "url": "https://github.com/IgniteUI/igniteui-theming/issues" + }, + "homepage": "https://github.com/IgniteUI/igniteui-theming#readme", + "exports": { + ".": { + "default": "./dist/index.js" + } + }, + "scripts": { + "test": "vitest run", + "build": "vite build", + "clean": "shx rm -rf dist/" + }, + "lint-staged": { + "*.{js,ts,cjs,mjs,jsx,tsx}": [ + "biome check --fix --no-errors-on-unmatched" + ] + }, + "devDependencies": { + "@terrazzo/parser": "^2.0.0", + "immutable": "^5.1.5", + "sass-embedded": "1.92.1" + } +} diff --git a/packages/plugins/src/index.ts b/packages/plugins/src/index.ts new file mode 100644 index 00000000..ebe0c68c --- /dev/null +++ b/packages/plugins/src/index.ts @@ -0,0 +1,2 @@ +export type { SassSchemaPluginConfig } from "./terrazzo/index.js"; +export { sassSchemaPlugin } from "./terrazzo/index.js"; diff --git a/packages/plugins/src/terrazzo/index.ts b/packages/plugins/src/terrazzo/index.ts new file mode 100644 index 00000000..6f266990 --- /dev/null +++ b/packages/plugins/src/terrazzo/index.ts @@ -0,0 +1,4 @@ +import sassSchemaPlugin from "./sass-schema/index.js"; + +export type { SassSchemaPluginConfig } from "./sass-schema/index.js"; +export { sassSchemaPlugin }; diff --git a/packages/plugins/src/terrazzo/sass-schema/builder.ts b/packages/plugins/src/terrazzo/sass-schema/builder.ts new file mode 100644 index 00000000..ed11cfa5 --- /dev/null +++ b/packages/plugins/src/terrazzo/sass-schema/builder.ts @@ -0,0 +1,219 @@ +/** + * Transforms individual DTCG leaf tokens into Sass leaf format with mode expansion. + * + * Handles: + * - $type: "reference" with color → { color: { ref, hex, alpha? } } + * - $type: "reference" with dimension → { dimension: { ref, value } } + * - $type: "color" (literal) → { color: { hex, alpha? } } + * - $type: "dimension" (literal) → { dimension: { value } } + * + * Mode expansion: + * - Root $value → default mode (light) + * - $extensions.modes.dark.$value → dark mode + * - If no dark override, duplicate the default mode value + */ + +import type { + DTCGColorToken, + DTCGColorValue, + DTCGDimensionToken, + DTCGDimensionValue, + DTCGLeafToken, + DTCGReferenceValue, + SassColorLeaf, + SassDimensionLeaf, + SassModeContent, + SassModeLeaf, +} from "./types.js"; + +interface BuildLeafOptions { + /** Number of leading ref segments to strip. Default: 1 */ + stripRefSegments?: number; + /** Mode names to emit (at least one). First mode is the default. Default: ['light', 'dark'] */ + modes?: [string, ...string[]]; +} + +/** + * Parse a DTCG ref string into a string array. + * + * Example: + * parseRef("{primitive-colors.secondary.secondary-500}", 1) + * → ['secondary', 'secondary-500'] + * + * parseRef("{primitive-colors.grays.grays-100}", 1) + * → ['grays', 'grays-100'] + * + * @param ref - The ref string, e.g., "{primitive-colors.secondary.secondary-500}" + * @param stripSegments - Number of leading segments to strip after removing braces. Default: 1 + * @returns Array of path segments, e.g., ['secondary', 'secondary-500'] + */ +export function parseRef(ref: string, stripSegments = 1): string[] { + // Strip curly braces + const inner = ref.replace(/^\{|\}$/g, ""); + + // Split on dots + const segments = inner.split("."); + + // Strip leading segments + return segments.slice(stripSegments); +} + +/** + * Build a Sass mode leaf from a DTCG leaf token. + */ +export function buildSassLeaf( + token: DTCGLeafToken, + options: BuildLeafOptions = {}, +): SassModeLeaf { + const { stripRefSegments = 1, modes = ["light", "dark"] } = options; + const [defaultMode] = modes; + + // Build the default mode value from root $value + const defaultValue = buildModeValue( + token.$type, + token.$value, + stripRefSegments, + ); + + // Build each mode: default mode gets the root value, + // other modes get their override from $extensions.modes or fall back to the default. + const result: SassModeLeaf = {}; + + for (const mode of modes) { + if (mode === defaultMode) { + result[mode] = defaultValue; + } else { + const modeOverride = token.$extensions?.modes?.[mode]; + + result[mode] = modeOverride + ? buildModeValueFromOverride( + token.$type, + modeOverride.$value, + stripRefSegments, + ) + : defaultValue; + } + } + + return result; +} + +/** + * Build a single mode's value from the root $type and $value. + */ +function buildModeValue( + type: string, + value: DTCGLeafToken["$value"], + stripRefSegments: number, +): SassModeContent { + if (type === "reference") { + return buildFromReference(value as DTCGReferenceValue, stripRefSegments); + } + + if (type === "color") { + return { color: buildColorLeaf(value as DTCGColorValue) }; + } + + if (type === "dimension") { + return { dimension: buildDimensionLeaf(value as DTCGDimensionValue) }; + } + + // Fallback for unknown types - return empty + return {}; +} + +/** + * Build a single mode's value from a mode override entry. + * Mode overrides for reference tokens have the same shape as the root $value + * (with ref + type-keyed resolved value). For literal tokens in modes, + * the value is wrapped in a type-keyed object. + */ +function buildModeValueFromOverride( + rootType: string, + value: unknown, + stripRefSegments: number, +): SassModeContent { + if (value == null || typeof value !== "object") return {}; + + // If the override has a 'ref' field, it's a reference value + if ("ref" in value) { + return buildFromReference( + value as unknown as DTCGReferenceValue, + stripRefSegments, + ); + } + + // For non-reference root types, the mode value is wrapped: + // { color: { $type: "color", $value: {...} } } or bare value + if (rootType === "color" || "color" in value) { + const colorToken = (value as { color?: DTCGColorToken }).color; + + if (colorToken) { + return { color: buildColorLeaf(colorToken.$value) }; + } + + // Bare color value + return { color: buildColorLeaf(value as unknown as DTCGColorValue) }; + } + + if (rootType === "dimension" || "dimension" in value) { + const dimToken = (value as { dimension?: DTCGDimensionToken }).dimension; + + if (dimToken) { + return { dimension: buildDimensionLeaf(dimToken.$value) }; + } + + return { + dimension: buildDimensionLeaf(value as unknown as DTCGDimensionValue), + }; + } + + return {}; +} + +/** + * Build mode value from a reference-type value. + */ +function buildFromReference( + value: DTCGReferenceValue, + stripRefSegments: number, +): SassModeContent { + const ref = parseRef(value.ref, stripRefSegments); + + if (value.color) { + const leaf = buildColorLeaf(value.color.$value); + + leaf.ref = ref; + return { color: leaf }; + } + + if (value.dimension) { + const leaf = buildDimensionLeaf(value.dimension.$value); + + leaf.ref = ref; + return { dimension: leaf }; + } + + return {}; +} + +/** + * Build a SassColorLeaf from a DTCG color value. + * Alpha is included only when !== 1. + */ +function buildColorLeaf(value: DTCGColorValue): SassColorLeaf { + const leaf: SassColorLeaf = { hex: value.hex }; + + if (value.alpha !== 1) { + leaf.alpha = value.alpha; + } + + return leaf; +} + +/** + * Build a SassDimensionLeaf from a DTCG dimension value. + */ +function buildDimensionLeaf(value: DTCGDimensionValue): SassDimensionLeaf { + return { value: `${value.value}${value.unit}` }; +} diff --git a/packages/plugins/src/terrazzo/sass-schema/guards.ts b/packages/plugins/src/terrazzo/sass-schema/guards.ts new file mode 100644 index 00000000..23d90ffd --- /dev/null +++ b/packages/plugins/src/terrazzo/sass-schema/guards.ts @@ -0,0 +1,90 @@ +/** + * Runtime type guards for distinguishing SassModeLeaf nodes from SassTree + * branches during serialization and tree construction. + */ + +import type { SassModeLeaf, SassValueKey } from "./types.js"; + +/** + * Runtime-available keys from SassValueMap. + * Must stay in sync with the SassValueMap interface in types.ts. + */ +export const SASS_VALUE_KEYS: readonly SassValueKey[] = [ + "color", + "dimension", + "border", +]; + +/** + * Leaf-shape validators keyed by SassValueKey. + * + * Each validator checks whether a candidate value matches the expected leaf + * structure for that type. This prevents tree branches whose key happens to + * collide with a SassValueKey (e.g., a group named "color" or "border") + * from being misidentified as mode-leaf content. + */ +const LEAF_VALIDATORS: Record boolean> = { + /** SassColorLeaf always has `hex`. */ + color: (v) => typeof v === "object" && v !== null && "hex" in v, + + /** SassDimensionLeaf always has `value` (string). */ + dimension: (v) => + typeof v === "object" && + v !== null && + "value" in v && + typeof (v as Record).value === "string", + + /** SassBorderLeaf has at least one of style (string), width (has value), or color (has hex). */ + border: (v) => { + if (typeof v !== "object" || v === null) return false; + + const b = v as Record; + + if ("style" in b && typeof b.style === "string") return true; + + if ( + "width" in b && + typeof b.width === "object" && + b.width !== null && + "value" in b.width + ) { + return true; + } + + if ( + "color" in b && + typeof b.color === "object" && + b.color !== null && + "hex" in b.color + ) { + return true; + } + + return false; + }, +}; + +/** + * Check if a value is a SassModeLeaf (mode-keyed object with value-type content). + * + * Two-level check: the first entry's value must contain a recognized + * SassValueKey AND the value under that key must match the corresponding + * leaf shape (e.g., a color leaf must have `hex`). + */ +export function isSassModeLeaf(value: unknown): value is SassModeLeaf { + if (typeof value !== "object" || value === null) return false; + + const entries = Object.values(value as Record); + + if (entries.length === 0) return false; + + const first = entries[0]; + + if (typeof first !== "object" || first === null) return false; + + const content = first as Record; + + return SASS_VALUE_KEYS.some( + (key) => key in content && LEAF_VALIDATORS[key](content[key]), + ); +} diff --git a/packages/plugins/src/terrazzo/sass-schema/index.ts b/packages/plugins/src/terrazzo/sass-schema/index.ts new file mode 100644 index 00000000..371484e3 --- /dev/null +++ b/packages/plugins/src/terrazzo/sass-schema/index.ts @@ -0,0 +1,170 @@ +/** + * Terrazzo plugin that transforms DTCG JSON tokens into Sass map files. + * + * Pipeline: + * tokens (flat Terrazzo dict) + * → group by component (first ID segment) + * → rebuild nested tree from remaining ID segments + * → transform each leaf to Sass mode leaf format + * → serialize tree to Sass map string + * → outputFile() per component + */ + +import type { Plugin } from "@terrazzo/parser"; +import { buildSassLeaf } from "./builder.js"; +import { isSassModeLeaf } from "./guards.js"; +import { serializeToSass } from "./serializer.js"; +import type { + DTCGLeafToken, + SassModeLeaf, + SassSchemaPluginConfig, + SassTree, +} from "./types.js"; + +export type { SassSchemaPluginConfig }; + +const DEFAULT_CONFIG: Required = { + filePrefix: "_", + stripRefSegments: 1, + modes: ["light", "dark"], + pretty: false, +}; + +/** + * Create a Terrazzo plugin that outputs Sass schema maps from DTCG tokens. + */ +export default function sassSchemaPlugin( + config: SassSchemaPluginConfig = {}, +): Plugin { + const cfg = { ...DEFAULT_CONFIG, ...config }; + + return { + name: "sass-schema-transformer", + async build({ tokens, outputFile }) { + // Step 1: Group tokens by component (first ID segment). + // Token IDs are dot-separated paths like "badge.info.background". + // We group by the first segment ("badge") and build a nested tree from all remaining segments. + const groups = groupTokensByComponent(tokens); + + // Step 2: For each component group, transform leaves and serialize + for (const [component, tokenEntries] of Object.entries(groups)) { + const sassTree = buildSassTree(tokenEntries, cfg); + + const sassContent = serializeToSass(sassTree, component, cfg.pretty); + const fileName = `${cfg.filePrefix}${component}.scss`; + + outputFile(fileName, sassContent); + } + }, + }; +} + +/** + * A token entry with its remaining path segments and original value. + */ +interface TokenEntry { + /** Remaining path segments after stripping the component prefix. */ + path: string[]; + /** The original DTCG leaf token value. */ + value: DTCGLeafToken; +} + +/** + * Group flat tokens by their component key (first ID segment). + * + * For a token ID like "badge.info.background": + * - component = "badge" + * - remaining path = ["info", "background"] + * + * For a token ID like "button.flat.background.idle": + * - component = "button" + * - remaining path = ["flat", "background", "idle"] + */ +function groupTokensByComponent( + tokens: Record, +): Record { + const groups: Record = {}; + + for (const token of Object.values(tokens)) { + const segments = token.id.split("."); + + if (segments.length < 2) { + continue; + } + + const [component, ...rest] = segments; + + if (!groups[component]) { + groups[component] = []; + } + + groups[component].push({ + path: rest, + value: token.originalValue as DTCGLeafToken, + }); + } + + return groups; +} + +/** + * Build a SassTree from a flat list of token entries. + * + * Each entry has a path (array of keys) and a DTCG leaf value. + * We reconstruct the nested tree structure and transform each leaf. + */ +function buildSassTree( + entries: TokenEntry[], + config: Required, +): SassTree { + const tree: SassTree = {}; + + for (const entry of entries) { + const sassLeaf = buildSassLeaf(entry.value, { + stripRefSegments: config.stripRefSegments, + modes: config.modes, + }); + + setNestedValue(tree, entry.path, sassLeaf); + } + + return tree; +} + +/** + * Set a value at a nested path in an object tree. + */ +function setNestedValue( + obj: SassTree, + path: string[], + value: SassModeLeaf, +): void { + let current = obj; + + for (let i = 0; i < path.length - 1; i++) { + const key = path[i]; + + if ( + !current[key] || + typeof current[key] !== "object" || + isSassModeLeaf(current[key]) + ) { + current[key] = {}; + } + + current = current[key] as SassTree; + } + + const lastKey = path[path.length - 1]; + current[lastKey] = value; +} + +export { buildSassLeaf, parseRef } from "./builder.js"; +export { serializeToSass } from "./serializer.js"; +export type { + DTCGLeafToken, + SassColorLeaf, + SassDimensionLeaf, + SassModeLeaf, + SassTree, +} from "./types.js"; diff --git a/packages/plugins/src/terrazzo/sass-schema/serializer.ts b/packages/plugins/src/terrazzo/sass-schema/serializer.ts new file mode 100644 index 00000000..6a64c210 --- /dev/null +++ b/packages/plugins/src/terrazzo/sass-schema/serializer.ts @@ -0,0 +1,182 @@ +/** + * Serializes a Sass tree (JS object) into a Sass variable declaration. + * + * Uses the Sass JavaScript API to build typed `SassMap` / `SassList` / + * `SassString` / `SassNumber` objects, then converts them to a string with + * either the API's natural compact form or an optional multi-line pretty form. + * + * Default compact output: + * ```scss + * $button-contained: (background: (idle: (light: (color: (ref: (secondary, secondary-500), hex: #df1b74)), dark: (color: (ref: (secondary, secondary-500), hex: #df1b74))))); + * ``` + * + * With `pretty: true`: + * ```scss + * $button-contained: ( + * background: ( + * idle: ( + * light: ( + * color: ( + * ref: (secondary, secondary-500), + * hex: #df1b74, + * ), + * ), + * ), + * ), + * ); + * ``` + */ + +import { OrderedMap } from "immutable"; +import type { Value } from "sass-embedded"; +import { SassList, SassMap, SassNumber, SassString } from "sass-embedded"; + +import { isSassModeLeaf, SASS_VALUE_KEYS } from "./guards.js"; +import type { + SassBorderLeaf, + SassColorLeaf, + SassDimensionLeaf, + SassModeContent, + SassModeLeaf, + SassTree, + SassValueKey, +} from "./types.js"; + +/** Bare (unquoted) Sass string — used for identifiers, hex values, dimension strings. */ +const unquoted = (s: string): SassString => + new SassString(s, { quotes: false }); + +/** Parenthesized comma-separated ref path, e.g. `(secondary, 500, ...)`. */ +const refList = (refs: string[]): SassList => { + return new SassList([...refs.map(unquoted)], { + separator: ",", + brackets: false, + }); +}; + +/** A single string-key / Sass-value pair ready for use in `OrderedMap`. */ +const pair = (key: string, value: Value): [Value, Value] => [ + unquoted(key), + value, +]; + +/** Build a `SassMap` from an ordered list of string-keyed pairs. */ +const buildMap = (pairs: [Value, Value][]): SassMap => + new SassMap(OrderedMap(pairs)); + +function colorLeafToSassMap(leaf: SassColorLeaf): SassMap { + return buildMap([ + ...(leaf.ref !== undefined + ? [pair("ref", unquoted(`(${refList(leaf.ref)})`))] + : []), + ...(leaf.alpha !== undefined + ? [pair("alpha", new SassNumber(leaf.alpha))] + : []), + pair("hex", unquoted(leaf.hex)), + ]); +} + +function dimensionLeafToSassMap(leaf: SassDimensionLeaf): SassMap { + return buildMap([ + ...(leaf.ref !== undefined + ? [pair("ref", unquoted(`(${refList(leaf.ref)})`))] + : []), + pair("value", unquoted(leaf.value)), + ]); +} + +function borderLeafToSassMap(leaf: SassBorderLeaf): SassMap { + return buildMap([ + ...(leaf.ref !== undefined + ? [pair("ref", unquoted(`(${refList(leaf.ref)})`))] + : []), + ...(leaf.color !== undefined + ? [pair("color", colorLeafToSassMap(leaf.color))] + : []), + ...(leaf.width !== undefined + ? [pair("width", dimensionLeafToSassMap(leaf.width))] + : []), + ...(leaf.style !== undefined ? [pair("style", unquoted(leaf.style))] : []), + ]); +} + +/** Map of value-type key to the function that converts its leaf into a SassMap. */ +const valueSerializers: Partial< + Record SassMap> +> = { + color: (leaf) => colorLeafToSassMap(leaf as SassColorLeaf), + dimension: (leaf) => dimensionLeafToSassMap(leaf as SassDimensionLeaf), + border: (leaf) => borderLeafToSassMap(leaf as SassBorderLeaf), +}; + +function modeContentToSassMap(content: SassModeContent): SassMap { + for (const key of SASS_VALUE_KEYS) { + const leaf = content[key]; + const serializer = valueSerializers[key]; + + if (leaf && serializer) { + return buildMap([pair(key, serializer(leaf))]); + } + } + + return buildMap([]); +} + +function modeLeafToSassMap(leaf: SassModeLeaf): SassMap { + return buildMap( + Object.entries(leaf).map(([mode, content]) => + pair(mode, modeContentToSassMap(content)), + ), + ); +} + +export function treeToSassMap(tree: SassTree): SassMap { + return buildMap( + Object.entries(tree).map(([key, value]) => { + if (isSassModeLeaf(value)) { + return pair(key, modeLeafToSassMap(value)); + } + + return pair(key, treeToSassMap(value as SassTree)); + }), + ); +} + +/** + * Recursively format a Sass value as indented multi-line text. + * `SassMap` → multi-line with trailing commas; everything else → `.toString()`. + */ +function prettyPrint(value: Value, depth = 0): string { + if (value instanceof SassMap) { + const pad = String("").padStart(depth * 2); + const inner = String("").padStart((depth + 1) * 2); + const lines: string[] = []; + + for (const [k, v] of value.contents.entries()) { + lines.push(`${inner}${k.toString()}: ${prettyPrint(v, depth + 1)},\n`); + } + + return `(\n${lines.join("")}${pad})`; + } + + return value.toString(); +} + +/** + * Serialize a Sass tree to a complete Sass variable declaration. + * + * @param tree - The nested Sass tree to serialize. + * @param variableName - The Sass variable name (without `$`), e.g. `"button-contained"`. + * @param pretty - When `true`, produce indented multi-line output. Default: `false` (compact). + * @returns Complete Sass string with variable declaration. + */ +export function serializeToSass( + tree: SassTree, + variableName: string, + pretty = false, +): string { + const map = treeToSassMap(tree); + const body = pretty ? prettyPrint(map) : map.toString(); + + return `$${variableName}: ${body};\n`; +} diff --git a/packages/plugins/src/terrazzo/sass-schema/types.ts b/packages/plugins/src/terrazzo/sass-schema/types.ts new file mode 100644 index 00000000..df651e31 --- /dev/null +++ b/packages/plugins/src/terrazzo/sass-schema/types.ts @@ -0,0 +1,376 @@ +/** + * Types for the DTCG tokens format (2025.10) and the Sass output structure + * used by the sass-schema plugin. + * + * Architecture: + * DTCGValueMap — single source of truth: maps each $type key to its raw $value shape. + * DTCGTypedToken — generic {$type, $value} wrapper derived from the map. + * Individual aliases — DTCGColorToken, DTCGBooleanToken, etc. (= DTCGTypedToken). + * DTCGReferenceValue — Terrazzo-specific resolved alias (ref + typed token, derived). + * DTCGModeEntry — mode override inside $extensions.modes (derived). + * DTCGLeafToken — discriminated union on $type (derived from map + "reference"). + * + * Adding a new DTCG type only requires: + * 1. Define a value interface (if composite). + * 2. Add one entry to DTCGValueMap. + * Everything else auto-derives. + */ + +/** A map of well-known font weight numeric values to their string name aliases. */ +const fontWeights = { + 100: ["thin", "hairline"], + 200: ["extra-light", "ultra-light"], + 300: ["light"], + 400: ["normal", "regular", "book"], + 500: ["medium"], + 600: ["semi-bold", "demi-bold"], + 700: ["bold"], + 800: ["extra-bold", "ultra-bold"], + 900: ["black", "heavy"], + 950: ["extra-black", "ultra-black"], +} as const; + +/** Well-known numeric font weight values (100, 200, ..., 950). */ +type FontWeightNumeric = keyof typeof fontWeights; + +/** Pre-defined font weight string aliases (e.g., 'thin', 'bold', 'extra-black'). */ +type FontWeightAlias = (typeof fontWeights)[FontWeightNumeric][number]; + +/** A DTCG reference string pointing to another token's value, e.g. "{colors.primary.500}". */ +type DTCGReference = string; + +/** + * DTCG color value (spec §8.1). + * Uses the CSS Color Level 4 color-space model. + * The `hex` convenience field is populated by Terrazzo's resolver. + */ +export interface DTCGColorValue { + colorSpace: string; + components: number[]; + alpha: number; + hex: string; +} + +/** + * DTCG dimension value (spec §8.2). + * Also reused for duration sub-values (unit: "ms" | "s") and typography fontSize. + */ +export interface DTCGDimensionValue { + value: number; + unit: string; +} + +/** + * DTCG stroke-style object value (spec §9.3.2). + * The simpler string form ("solid", "dashed", …) uses DTCGStrokeStyleValue directly. + */ +export interface DTCGStrokeStyleObjectValue { + dashArray: DTCGDimensionValue[]; + lineCap: "round" | "butt" | "square"; +} + +/** + * DTCG stroke-style value (spec §9.3). + * Either one of the pre-defined CSS line-style keywords or an object form. + */ +export type DTCGStrokeStyleValue = + | "solid" + | "dashed" + | "dotted" + | "double" + | "groove" + | "ridge" + | "outset" + | "inset" + | DTCGStrokeStyleObjectValue; + +/** + * DTCG border composite value (spec §9.4). + */ +export interface DTCGBorderValue { + color: DTCGColorValue; + width: DTCGDimensionValue; + style: DTCGStrokeStyleValue; +} + +/** + * DTCG transition composite value (spec §9.5). + */ +export interface DTCGTransitionValue { + duration: DTCGDimensionValue; + delay: DTCGDimensionValue; + timingFunction: [number, number, number, number]; +} + +/** + * DTCG shadow object (spec §9.6). + * A shadow token's $value can be a single instance or an array of these. + */ +export interface DTCGShadowValue { + color: DTCGColorValue; + offsetX: DTCGDimensionValue; + offsetY: DTCGDimensionValue; + blur: DTCGDimensionValue; + spread: DTCGDimensionValue; + inset?: boolean; +} + +/** + * DTCG gradient stop (spec §9.7). + * A gradient token's $value is an array of these. + */ +export interface DTCGGradientStop { + color: DTCGColorValue; + /** Position along the gradient axis in the range [0, 1]. */ + position: number; +} + +/** + * DTCG typography composite value (spec §9.8). + * Sub-values are raw values (not wrapped in {$type,$value} tokens) as per the spec. + */ +export interface DTCGTypographyValue { + /** Font family string or ordered array (most-preferred first). */ + fontFamily: string | string[]; + /** Font size as a dimension value. */ + fontSize: DTCGDimensionValue; + /** Font weight: any number in [1, 1000] or a pre-defined alias. */ + fontWeight: number | FontWeightAlias; + /** Horizontal character spacing as a dimension value. */ + letterSpacing?: DTCGDimensionValue; + /** Line height as a unitless multiplier of fontSize. */ + lineHeight?: number; +} + +/** + * Maps every DTCG $type string to its raw $value shape. + * + * This is the central registry. All derived types (token wrappers, references, + * mode entries, leaf tokens) are auto-generated from this interface. + * + * Spec §8 scalar types: boolean, string, number, color, dimension, fontFamily, fontWeight, duration, cubicBezier + * Spec §9 composite types: strokeStyle, border, transition, shadow, gradient, typography + * + * Note: "boolean" and "string" are not formally standardised in the 2025.10 spec + * (they appear in §8.8 "Additional types") but are widely implemented by tools. + */ +export interface DTCGValueMap { + boolean: boolean; + string: string; + number: number; + color: DTCGColorValue; + dimension: DTCGDimensionValue; + fontFamily: string | string[]; + fontWeight: number | FontWeightAlias; + duration: DTCGDimensionValue; + cubicBezier: [number, number, number, number]; + strokeStyle: DTCGStrokeStyleValue; + border: DTCGBorderValue; + transition: DTCGTransitionValue; + shadow: DTCGShadowValue | DTCGShadowValue[]; + gradient: DTCGGradientStop[]; + typography: DTCGTypographyValue; +} + +/** All known DTCG $type strings. */ +export type DTCGTokenType = keyof DTCGValueMap; + +/** + * A DTCG typed token: `{$type: K, $value: DTCGValueMap[K]}`. + * + * Distributive conditional — when K is a union, produces a proper discriminated + * union where each member has a concrete $type and the corresponding $value. + */ +export type DTCGTypedToken = + K extends DTCGTokenType ? { $type: K; $value: DTCGValueMap[K] } : never; + +// ─── Convenience Token Type Aliases ──────────────────────────────────────── + +/** @see DTCGValueMap.boolean */ +export type DTCGBooleanToken = DTCGTypedToken<"boolean">; + +/** @see DTCGValueMap.string */ +export type DTCGStringToken = DTCGTypedToken<"string">; + +/** @see DTCGValueMap.number */ +export type DTCGNumberToken = DTCGTypedToken<"number">; + +/** @see DTCGValueMap.color */ +export type DTCGColorToken = DTCGTypedToken<"color">; + +/** @see DTCGValueMap.dimension */ +export type DTCGDimensionToken = DTCGTypedToken<"dimension">; + +/** @see DTCGValueMap.fontFamily */ +export type DTCGFontFamilyToken = DTCGTypedToken<"fontFamily">; + +/** @see DTCGValueMap.fontWeight */ +export type DTCGFontWeightToken = DTCGTypedToken<"fontWeight">; + +/** @see DTCGValueMap.duration */ +export type DTCGDurationToken = DTCGTypedToken<"duration">; + +/** @see DTCGValueMap.cubicBezier */ +export type DTCGCubicBezierToken = DTCGTypedToken<"cubicBezier">; + +/** @see DTCGValueMap.strokeStyle */ +export type DTCGStrokeStyleToken = DTCGTypedToken<"strokeStyle">; + +/** @see DTCGValueMap.border */ +export type DTCGBorderToken = DTCGTypedToken<"border">; + +/** @see DTCGValueMap.transition */ +export type DTCGTransitionToken = DTCGTypedToken<"transition">; + +/** @see DTCGValueMap.shadow */ +export type DTCGShadowToken = DTCGTypedToken<"shadow">; + +/** @see DTCGValueMap.gradient */ +export type DTCGGradientToken = DTCGTypedToken<"gradient">; + +/** @see DTCGValueMap.typography */ +export type DTCGTypographyToken = DTCGTypedToken<"typography">; + +/** + * Terrazzo-resolved reference value (Terrazzo-specific, not part of the DTCG spec). + * + * In the DTCG spec, an alias token has `$value: "{token.path}"` (a plain string). + * Terrazzo resolves the reference at parse time and records it as `$type: "reference"` + * with this shape: + * - `ref`: the original curly-brace reference string + * - One optional key per DTCG type holding the resolved `{$type, $value}` token + * + * The optional resolved-token keys are auto-derived from DTCGValueMap, so no manual + * sync is needed when the map grows. + */ +export type DTCGReferenceValue = { ref: DTCGReference } & { + [K in DTCGTokenType]?: DTCGTypedToken; +}; + +/** Union of all raw DTCG $value shapes. */ +type DTCGAnyValue = DTCGValueMap[DTCGTokenType]; + +/** + * A single mode override entry inside `$extensions.modes`. + * + * $value is either: + * - A DTCGReferenceValue (Terrazzo resolved alias with `ref` + typed token) + * - Any raw DTCG value (for literal mode overrides, e.g. a bare DTCGColorValue) + */ +export interface DTCGModeEntry { + $value: DTCGReferenceValue | DTCGAnyValue; +} + +/** Extensions block on a DTCG token. */ +export interface DTCGExtensions { + modes?: Record; + [key: string]: unknown; +} + +/** + * Helper: produces one leaf-token variant for a given $type. + * Distributive — when K is a union, each member gets its own $value type. + */ +type DTCGLeafTokenOf = + K extends "reference" + ? { + $type: "reference"; + $value: DTCGReferenceValue; + $description?: string; + $extensions?: DTCGExtensions; + } + : K extends DTCGTokenType + ? { + $type: K; + $value: DTCGValueMap[K]; + $description?: string; + $extensions?: DTCGExtensions; + } + : never; + +/** + * A leaf token in the DTCG document. This is the `originalValue` as seen + * by the Terrazzo parser for each individual token. + * + * This is a proper discriminated union on `$type`: narrowing `$type` to "color" + * constrains `$value` to `DTCGColorValue`; narrowing to "reference" constrains + * `$value` to `DTCGReferenceValue`; and so on. + * + * "reference" is a Terrazzo-specific pseudo-type for resolved aliases. + */ +export type DTCGLeafToken = DTCGLeafTokenOf; + +/** + * A nested DTCG document. Groups contain either more groups or leaf tokens. + * A leaf token is identified by having a `$type` property. + */ +export type DTCGNode = DTCGLeafToken | DTCGGroup; +export type DTCGGroup = { [key: string]: DTCGNode }; + +/** A single color entry in the Sass output. */ +export interface SassColorLeaf { + ref?: string[]; + alpha?: number; + hex: string; +} + +/** A single dimension entry in the Sass output. */ +export interface SassDimensionLeaf { + ref?: string[]; + value: string; +} + +/** A single border entry in the Sass output. */ +export interface SassBorderLeaf { + ref?: string[]; + color?: SassColorLeaf; + width?: SassDimensionLeaf; + style?: string; +} + +/** + * Maps each supported Sass value-type key to its leaf shape. + * + * Extend this interface to add new value types to the output. + * Both `SassModeContent` and the runtime guard `SASS_VALUE_KEYS` + * (in serializer.ts / index.ts) derive from this map. + */ +export interface SassValueMap { + color: SassColorLeaf; + dimension: SassDimensionLeaf; + border: SassBorderLeaf; +} + +/** Known Sass value-type keys, for use in runtime guards. */ +export type SassValueKey = keyof SassValueMap; + +/** + * The content for a single mode — at most one value-type entry is present. + * Auto-derived from SassValueMap so it stays in sync. + */ +export type SassModeContent = { + [K in SassValueKey]?: SassValueMap[K]; +}; + +/** + * The mode-expanded leaf for a single token. + * Keys are mode names (e.g. "light", "dark"); values are the per-mode content. + */ +export type SassModeLeaf = Record; + +/** Recursive Sass tree structure. Leaves are SassModeLeaf, branches are nested maps. */ +export type SassTree = { [key: string]: SassTree | SassModeLeaf }; + +// ─── Plugin Config ────────────────────────────────────────────────────────── + +/** Configuration options for the sass-schema plugin. */ +export interface SassSchemaPluginConfig { + /** File prefix for Sass partials. Default: '_' */ + filePrefix?: string; + /** Number of leading ref path segments to strip (e.g., 1 strips 'primitive-colors'). Default: 1 */ + stripRefSegments?: number; + /** Mode names to emit (at least one). First mode is the default. Default: ['light', 'dark'] */ + modes?: [string, ...string[]]; + /** Produce indented multi-line Sass output instead of compact single-line. Default: false */ + pretty?: boolean; +} diff --git a/packages/plugins/tests/terrazzo/builder.test.ts b/packages/plugins/tests/terrazzo/builder.test.ts new file mode 100644 index 00000000..a223729f --- /dev/null +++ b/packages/plugins/tests/terrazzo/builder.test.ts @@ -0,0 +1,289 @@ +import { describe, expect, it } from "vitest"; +import { buildSassLeaf } from "../../src/terrazzo/sass-schema/builder.js"; +import type { DTCGLeafToken } from "../../src/terrazzo/sass-schema/types.js"; + +describe("buildSassLeaf", () => { + describe("reference color tokens", () => { + it("builds a simple reference color (no modes, alpha = 1)", () => { + const token: DTCGLeafToken = { + $type: "reference", + $value: { + ref: "{primitive-colors.secondary.500}", + color: { + $type: "color", + $value: { + colorSpace: "srgb", + components: [0.8745, 0.1059, 0.4549], + alpha: 1, + hex: "#df1b74", + }, + }, + }, + }; + + const result = buildSassLeaf(token); + + expect(result).toEqual({ + light: { + color: { + ref: ["secondary", "500"], + hex: "#df1b74", + }, + }, + dark: { + color: { + ref: ["secondary", "500"], + hex: "#df1b74", + }, + }, + }); + }); + + it("includes alpha when < 1", () => { + const token: DTCGLeafToken = { + $type: "reference", + $value: { + ref: "{primitive-colors.secondary.500}", + color: { + $type: "color", + $value: { + colorSpace: "srgb", + components: [0.8745, 0.1059, 0.4549], + alpha: 0.08, + hex: "#df1b7414", + }, + }, + }, + }; + + const result = buildSassLeaf(token); + + expect(result.light.color).toEqual({ + ref: ["secondary", "500"], + alpha: 0.08, + hex: "#df1b7414", + }); + }); + + it("includes alpha when 0", () => { + const token: DTCGLeafToken = { + $type: "reference", + $value: { + ref: "{primitive-colors.secondary.500}", + color: { + $type: "color", + $value: { + colorSpace: "srgb", + components: [0.8745, 0.1059, 0.4549], + alpha: 0, + hex: "#df1b7400", + }, + }, + }, + }; + + const result = buildSassLeaf(token); + + expect(result.light.color?.alpha).toBe(0); + }); + + it("handles dark mode override with different ref and hex", () => { + const token: DTCGLeafToken = { + $type: "reference", + $value: { + ref: "{primitive-colors.gray.100}", + color: { + $type: "color", + $value: { + colorSpace: "srgb", + components: [0.961, 0.961, 0.961], + alpha: 1, + hex: "#f5f5f5", + }, + }, + }, + $extensions: { + modes: { + dark: { + $value: { + ref: "{primitive-colors.gray.100}", + color: { + $type: "color", + $value: { + colorSpace: "srgb", + components: [0.259, 0.259, 0.259], + alpha: 1, + hex: "#424242", + }, + }, + }, + }, + }, + }, + }; + + const result = buildSassLeaf(token); + + expect(result.light.color).toEqual({ + ref: ["gray", "100"], + hex: "#f5f5f5", + }); + expect(result.dark.color).toEqual({ + ref: ["gray", "100"], + hex: "#424242", + }); + }); + + it("duplicates default when no dark mode override", () => { + const token: DTCGLeafToken = { + $type: "reference", + $value: { + ref: "{primitive-colors.secondary.500}", + color: { + $type: "color", + $value: { + colorSpace: "srgb", + components: [0.8745, 0.1059, 0.4549], + alpha: 1, + hex: "#df1b74", + }, + }, + }, + }; + + const result = buildSassLeaf(token); + + expect(result.light).toEqual(result.dark); + }); + + it("handles dark mode override with different ref path", () => { + const token: DTCGLeafToken = { + $type: "reference", + $value: { + ref: "{primitive-colors.secondary.800}", + color: { + $type: "color", + $value: { + colorSpace: "srgb", + components: [0.7137, 0, 0.3255], + alpha: 1, + hex: "#b60053", + }, + }, + }, + $extensions: { + modes: { + dark: { + $value: { + ref: "{primitive-colors.secondary.300}", + color: { + $type: "color", + $value: { + colorSpace: "srgb", + components: [0.8235, 0.3451, 0.5608], + alpha: 1, + hex: "#d2588f", + }, + }, + }, + }, + }, + }, + }; + + const result = buildSassLeaf(token); + + expect(result.light.color?.ref).toEqual(["secondary", "800"]); + expect(result.dark.color?.ref).toEqual(["secondary", "300"]); + }); + }); + + describe("literal color tokens", () => { + it("builds a literal color (no ref)", () => { + const token: DTCGLeafToken = { + $type: "color", + $value: { + colorSpace: "srgb", + components: [0, 0, 0], + alpha: 1, + hex: "#000000", + }, + }; + + const result = buildSassLeaf(token); + + expect(result.light.color).toEqual({ hex: "#000000" }); + expect(result.dark.color).toEqual({ hex: "#000000" }); + expect(result.light.color?.ref).toBeUndefined(); + }); + + it("builds a literal transparent color (alpha = 0)", () => { + const token: DTCGLeafToken = { + $type: "color", + $value: { + colorSpace: "srgb", + components: [1, 1, 1], + alpha: 0, + hex: "#ffffff00", + }, + }; + + const result = buildSassLeaf(token); + + expect(result.light.color).toEqual({ + alpha: 0, + hex: "#ffffff00", + }); + }); + }); + + describe("configurable ref stripping", () => { + it("strips 0 segments when configured", () => { + const token: DTCGLeafToken = { + $type: "reference", + $value: { + ref: "{primitive-colors.secondary.500}", + color: { + $type: "color", + $value: { + colorSpace: "srgb", + components: [0.8745, 0.1059, 0.4549], + alpha: 1, + hex: "#df1b74", + }, + }, + }, + }; + + const result = buildSassLeaf(token, { stripRefSegments: 0 }); + + expect(result.light.color?.ref).toEqual([ + "primitive-colors", + "secondary", + "500", + ]); + }); + + it("strips 2 segments when configured", () => { + const token: DTCGLeafToken = { + $type: "reference", + $value: { + ref: "{primitive-colors.secondary.500}", + color: { + $type: "color", + $value: { + colorSpace: "srgb", + components: [0.8745, 0.1059, 0.4549], + alpha: 1, + hex: "#df1b74", + }, + }, + }, + }; + + const result = buildSassLeaf(token, { stripRefSegments: 2 }); + + expect(result.light.color?.ref).toEqual(["500"]); + }); + }); +}); diff --git a/packages/plugins/tests/terrazzo/fixtures/components-button-contained.json b/packages/plugins/tests/terrazzo/fixtures/components-button-contained.json new file mode 100644 index 00000000..0ab86769 --- /dev/null +++ b/packages/plugins/tests/terrazzo/fixtures/components-button-contained.json @@ -0,0 +1,117 @@ +{ + "button": { + "contained": { + "background": { + "idle": { + "$type": "reference", + "$value": { + "ref": "{primitive-colors.secondary.500}", + "color": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [0.8745, 0.1059, 0.4549], + "alpha": 1, + "hex": "#df1b74" + } + } + } + }, + "hover": { + "$type": "reference", + "$value": { + "ref": "{primitive-colors.secondary.400}", + "color": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [0.851, 0.2078, 0.498], + "alpha": 1, + "hex": "#d9357f" + } + } + } + }, + "disabled": { + "$type": "reference", + "$value": { + "ref": "{primitive-colors.grays.100}", + "color": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [0.961, 0.961, 0.961], + "alpha": 1, + "hex": "#f5f5f5" + } + } + }, + "$extensions": { + "modes": { + "dark": { + "$value": { + "ref": "{primitive-colors.grays.100}", + "color": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [0.259, 0.259, 0.259], + "alpha": 1, + "hex": "#424242" + } + } + } + } + } + } + } + }, + "label": { + "foreground": { + "idle": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [0, 0, 0], + "alpha": 1, + "hex": "#000000" + } + }, + "disabled": { + "$type": "reference", + "$value": { + "ref": "{primitive-colors.grays.500}", + "color": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [0.62, 0.62, 0.62], + "alpha": 1, + "hex": "#9e9e9e" + } + } + }, + "$extensions": { + "modes": { + "dark": { + "$value": { + "ref": "{primitive-colors.grays.500}", + "color": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [0.741, 0.741, 0.741], + "alpha": 1, + "hex": "#bdbdbd" + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/packages/plugins/tests/terrazzo/fixtures/components-button-flat.json b/packages/plugins/tests/terrazzo/fixtures/components-button-flat.json new file mode 100644 index 00000000..e789d925 --- /dev/null +++ b/packages/plugins/tests/terrazzo/fixtures/components-button-flat.json @@ -0,0 +1,84 @@ +{ + "button": { + "flat": { + "background": { + "idle": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [1, 1, 1], + "alpha": 0, + "hex": "#ffffff00" + } + }, + "hover": { + "$type": "reference", + "$value": { + "ref": "{primitive-colors.secondary.500}", + "color": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [0.8745, 0.1059, 0.4549], + "alpha": 0.08, + "hex": "#df1b7414" + } + } + } + } + }, + "icon": { + "foreground": { + "idle": { + "$type": "reference", + "$value": { + "ref": "{primitive-colors.secondary.500}", + "color": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [0.8745, 0.1059, 0.4549], + "alpha": 1, + "hex": "#df1b74" + } + } + } + }, + "hover": { + "$type": "reference", + "$value": { + "ref": "{primitive-colors.secondary.800}", + "color": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [0.7137, 0, 0.3255], + "alpha": 1, + "hex": "#b60053" + } + } + }, + "$extensions": { + "modes": { + "dark": { + "$value": { + "ref": "{primitive-colors.secondary.300}", + "color": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [0.8235, 0.3451, 0.5608], + "alpha": 1, + "hex": "#d2588f" + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/packages/plugins/tests/terrazzo/guards.test.ts b/packages/plugins/tests/terrazzo/guards.test.ts new file mode 100644 index 00000000..1595b659 --- /dev/null +++ b/packages/plugins/tests/terrazzo/guards.test.ts @@ -0,0 +1,201 @@ +import { describe, expect, it } from "vitest"; +import { + isSassModeLeaf, + SASS_VALUE_KEYS, +} from "../../src/terrazzo/sass-schema/guards.js"; +import type { SassModeLeaf } from "../../src/terrazzo/sass-schema/types.js"; + +describe("SASS_VALUE_KEYS", () => { + it("contains all expected keys", () => { + expect(SASS_VALUE_KEYS).toContain("color"); + expect(SASS_VALUE_KEYS).toContain("dimension"); + expect(SASS_VALUE_KEYS).toContain("border"); + }); +}); + +describe("isSassModeLeaf — correctly identifies mode leaves", () => { + it("recognizes a color mode leaf", () => { + const leaf: SassModeLeaf = { + light: { color: { hex: "#ffffff" } }, + dark: { color: { hex: "#000000" } }, + }; + + expect(isSassModeLeaf(leaf)).toBe(true); + }); + + it("recognizes a color mode leaf with ref and alpha", () => { + const leaf: SassModeLeaf = { + light: { + color: { ref: ["primary", "500"], alpha: 0.5, hex: "#1976d280" }, + }, + dark: { color: { ref: ["primary", "300"], hex: "#64b5f6" } }, + }; + + expect(isSassModeLeaf(leaf)).toBe(true); + }); + + it("recognizes a dimension mode leaf", () => { + const leaf: SassModeLeaf = { + light: { dimension: { value: "1px" } }, + dark: { dimension: { value: "2px" } }, + }; + + expect(isSassModeLeaf(leaf)).toBe(true); + }); + + it("recognizes a dimension mode leaf with ref", () => { + const leaf: SassModeLeaf = { + light: { dimension: { ref: ["spacing", "sm"], value: "8px" } }, + dark: { dimension: { ref: ["spacing", "sm"], value: "8px" } }, + }; + + expect(isSassModeLeaf(leaf)).toBe(true); + }); + + it("recognizes a border mode leaf with all sub-values", () => { + const leaf: SassModeLeaf = { + light: { + border: { + color: { hex: "#e0e0e0" }, + width: { value: "1px" }, + style: "solid", + }, + }, + dark: { + border: { + color: { hex: "#424242" }, + width: { value: "1px" }, + style: "solid", + }, + }, + }; + + expect(isSassModeLeaf(leaf)).toBe(true); + }); + + it("recognizes a border mode leaf with only style", () => { + const leaf: SassModeLeaf = { + light: { border: { style: "dashed" } }, + dark: { border: { style: "dashed" } }, + }; + + expect(isSassModeLeaf(leaf)).toBe(true); + }); + + it("recognizes a border mode leaf with only width", () => { + const leaf: SassModeLeaf = { + light: { border: { width: { value: "2px" } } }, + dark: { border: { width: { value: "2px" } } }, + }; + + expect(isSassModeLeaf(leaf)).toBe(true); + }); + + it("recognizes a border mode leaf with only color", () => { + const leaf: SassModeLeaf = { + light: { border: { color: { hex: "#cccccc" } } }, + dark: { border: { color: { hex: "#333333" } } }, + }; + + expect(isSassModeLeaf(leaf)).toBe(true); + }); + + it("recognizes a single-mode leaf", () => { + const leaf: SassModeLeaf = { + light: { color: { hex: "#ffffff" } }, + }; + + expect(isSassModeLeaf(leaf)).toBe(true); + }); +}); + +describe("isSassModeLeaf — rejects tree branches and invalid values", () => { + it("rejects null", () => { + expect(isSassModeLeaf(null)).toBe(false); + }); + + it("rejects undefined", () => { + expect(isSassModeLeaf(undefined)).toBe(false); + }); + + it("rejects a string", () => { + expect(isSassModeLeaf("color")).toBe(false); + }); + + it("rejects a number", () => { + expect(isSassModeLeaf(42)).toBe(false); + }); + + it("rejects an empty object", () => { + expect(isSassModeLeaf({})).toBe(false); + }); + + it('rejects a tree branch whose child is named "color" but contains a mode leaf (not a color leaf)', () => { + // The "color" key matches SASS_VALUE_KEYS, but its value is a mode leaf, not a SassColorLeaf. + const treeBranch = { + color: { + light: { color: { hex: "#ffffff" } }, + dark: { color: { hex: "#000000" } }, + }, + }; + + expect(isSassModeLeaf(treeBranch)).toBe(false); + }); + + it('rejects a tree branch whose child is named "dimension" but contains a mode leaf', () => { + const treeBranch = { + dimension: { + light: { dimension: { value: "1px" } }, + dark: { dimension: { value: "2px" } }, + }, + }; + + expect(isSassModeLeaf(treeBranch)).toBe(false); + }); + + it('rejects a tree branch whose child is named "border" but contains a mode leaf', () => { + const treeBranch = { + border: { + light: { + border: { + color: { hex: "#e0e0e0" }, + style: "solid", + }, + }, + dark: { + border: { + color: { hex: "#424242" }, + style: "solid", + }, + }, + }, + }; + + expect(isSassModeLeaf(treeBranch)).toBe(false); + }); + + it("rejects a tree branch with nested groups", () => { + const treeBranch = { + background: { + idle: { + light: { color: { hex: "#ffffff" } }, + dark: { color: { hex: "#000000" } }, + }, + }, + }; + + expect(isSassModeLeaf(treeBranch)).toBe(false); + }); + + it("rejects when first entry value is a primitive", () => { + expect(isSassModeLeaf({ light: "not an object" })).toBe(false); + }); + + it("rejects when first entry value is null", () => { + expect(isSassModeLeaf({ light: null })).toBe(false); + }); + + it("rejects an object whose first entry has no recognized value-type keys", () => { + expect(isSassModeLeaf({ light: { unknown: "data" } })).toBe(false); + }); +}); diff --git a/packages/plugins/tests/terrazzo/parser.test.ts b/packages/plugins/tests/terrazzo/parser.test.ts new file mode 100644 index 00000000..f798afa4 --- /dev/null +++ b/packages/plugins/tests/terrazzo/parser.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { parseRef } from "../../src/terrazzo/sass-schema/builder.js"; + +describe("parseRef", () => { + it("strips 1 leading segment by default", () => { + expect(parseRef("{primitive-colors.secondary.500}")).toEqual([ + "secondary", + "500", + ]); + }); + + it("strips curly braces", () => { + expect(parseRef("{foo.bar.baz}", 0)).toEqual(["foo", "bar", "baz"]); + }); + + it("strips multiple leading segments", () => { + expect(parseRef("{primitive-colors.secondary.500}", 2)).toEqual(["500"]); + }); + + it("strips 0 segments when configured", () => { + expect(parseRef("{primitive-colors.secondary.500}", 0)).toEqual([ + "primitive-colors", + "secondary", + "500", + ]); + }); + + it("handles gray ref path", () => { + expect(parseRef("{primitive-colors.gray.100}")).toEqual(["gray", "100"]); + }); + + it("handles single-segment ref after stripping", () => { + expect(parseRef("{collection.token}", 1)).toEqual(["token"]); + }); + + it("handles ref with no braces gracefully", () => { + expect(parseRef("primitive-colors.secondary.500")).toEqual([ + "secondary", + "500", + ]); + }); + + it("returns empty array when all segments are stripped", () => { + expect(parseRef("{a.b}", 2)).toEqual([]); + }); +}); diff --git a/packages/plugins/tests/terrazzo/plugin.integration.test.ts b/packages/plugins/tests/terrazzo/plugin.integration.test.ts new file mode 100644 index 00000000..6572c001 --- /dev/null +++ b/packages/plugins/tests/terrazzo/plugin.integration.test.ts @@ -0,0 +1,605 @@ +import { describe, expect, it } from "vitest"; +import { + buildSassLeaf, + parseRef, +} from "../../src/terrazzo/sass-schema/builder.js"; +import { serializeToSass } from "../../src/terrazzo/sass-schema/serializer.js"; +import type { + DTCGLeafToken, + SassModeLeaf, + SassTree, +} from "../../src/terrazzo/sass-schema/types.js"; +// biome-ignore lint/correctness/useImportExtensions: .json is the correct extension for JSON file imports +import containedFixture from "./fixtures/components-button-contained.json"; +// biome-ignore lint/correctness/useImportExtensions: .json is the correct extension for JSON file imports +import flatFixture from "./fixtures/components-button-flat.json"; + +/** + * Check whether a node is a DTCG leaf token (has $type property). + */ +function isDTCGLeaf(node: unknown): node is DTCGLeafToken { + return typeof node === "object" && node !== null && "$type" in node; +} + +/** + * Flatten a nested DTCG document into dot-separated token entries. + * Simulates what Terrazzo does when parsing a JSON file. + */ +function flattenDTCG( + obj: Record, + prefix = "", +): { id: string; originalValue: DTCGLeafToken }[] { + const entries: { id: string; originalValue: DTCGLeafToken }[] = []; + + for (const [key, value] of Object.entries(obj)) { + const path = prefix ? `${prefix}.${key}` : key; + + if (isDTCGLeaf(value)) { + entries.push({ id: path, originalValue: value }); + } else if (typeof value === "object" && value !== null) { + entries.push(...flattenDTCG(value as Record, path)); + } + } + + return entries; +} + +/** + * Simulate the plugin pipeline: flatten → group by component (first segment) → build sass tree → serialize. + * Uses compact output by default; pass `pretty: true` to get indented output. + */ +function simulatePlugin( + fixture: Record, + pretty = false, +): Record { + const flatTokens = flattenDTCG(fixture); + + // Group by first segment (component name) + const groups: Record = {}; + + for (const token of flatTokens) { + const segments = token.id.split("."); + if (segments.length < 2) continue; + + const [component, ...rest] = segments; + if (!groups[component]) groups[component] = []; + + groups[component].push({ path: rest, value: token.originalValue }); + } + + // Build Sass output for each group + const outputs: Record = {}; + + for (const [component, entries] of Object.entries(groups)) { + const tree: SassTree = {}; + + for (const entry of entries) { + const sassLeaf = buildSassLeaf(entry.value, { stripRefSegments: 1 }); + let current = tree; + + for (let i = 0; i < entry.path.length - 1; i++) { + if (!current[entry.path[i]]) current[entry.path[i]] = {}; + current = current[entry.path[i]] as SassTree; + } + + current[entry.path[entry.path.length - 1]] = sassLeaf; + } + + outputs[component] = serializeToSass(tree, component, pretty); + } + + return outputs; +} + +describe("Plugin Integration", () => { + describe('contained button fixture (groups under "button")', () => { + it("produces a single $button variable", () => { + const sass = simulatePlugin(containedFixture).button; + + expect(sass).toBeDefined(); + expect(sass).toMatch(/^\$button: \(/); + expect(sass).toMatch(/\);\n$/); + }); + + it('preserves "contained" as a nested key', () => { + const sass = simulatePlugin(containedFixture).button; + expect(sass).toContain("contained: ("); + }); + + it("correctly transforms a reference color (contained.background.idle)", () => { + const sass = simulatePlugin(containedFixture).button; + + expect(sass).toContain("ref: (secondary, 500)"); + expect(sass).toContain("hex: #df1b74"); + }); + + it("correctly transforms a reference with dark mode override (contained.background.disabled)", () => { + const sass = simulatePlugin(containedFixture).button; + + // light mode: 100 → #f5f5f5 + expect(sass).toContain("hex: #f5f5f5"); + + // dark mode: 100 → #424242 + expect(sass).toContain("hex: #424242"); + }); + + it("correctly transforms a literal color (contained.label.foreground.idle)", () => { + const sass = simulatePlugin(containedFixture).button; + expect(sass).toContain("hex: #000000"); + }); + + it("does not include alpha when alpha is 1", () => { + const sass = simulatePlugin(containedFixture).button; + expect(sass).not.toContain("alpha:"); + }); + }); + + describe('flat button fixture (groups under "button")', () => { + it('produces a $button variable with "flat" nested key', () => { + const sass = simulatePlugin(flatFixture).button; + + expect(sass).toBeDefined(); + expect(sass).toMatch(/^\$button: \(/); + expect(sass).toContain("flat: ("); + }); + + it("includes alpha for opacity-merged tokens (flat.background.hover)", () => { + const sass = simulatePlugin(flatFixture).button; + + expect(sass).toContain("alpha: 0.08"); + expect(sass).toContain("hex: #df1b7414"); + }); + + it("includes alpha: 0 for transparent literal color (flat.background.idle)", () => { + const sass = simulatePlugin(flatFixture).button; + + expect(sass).toContain("alpha: 0"); + expect(sass).toContain("hex: #ffffff00"); + }); + + it("handles dark mode override on icon foreground", () => { + const sass = simulatePlugin(flatFixture).button; + + expect(sass).toContain("ref: (secondary, 800)"); + expect(sass).toContain("hex: #b60053"); + expect(sass).toContain("ref: (secondary, 300)"); + expect(sass).toContain("hex: #d2588f"); + }); + }); + + describe("parseRef integration", () => { + it("parses real ref strings from the fixture", () => { + expect(parseRef("{primitive-colors.secondary.500}")).toEqual([ + "secondary", + "500", + ]); + expect(parseRef("{primitive-colors.gray.100}")).toEqual(["gray", "100"]); + expect(parseRef("{primitive-colors.secondary.800}")).toEqual([ + "secondary", + "800", + ]); + }); + }); + + describe("full pipeline snapshot", () => { + it("produces expected Sass for a minimal button with contained variant (pretty)", () => { + const tree: SassTree = { + contained: { + background: { + idle: { + light: { + color: { ref: ["secondary", "500"], hex: "#df1b74" }, + }, + dark: { + color: { ref: ["secondary", "500"], hex: "#df1b74" }, + }, + } as SassModeLeaf, + disabled: { + light: { color: { ref: ["gray", "100"], hex: "#f5f5f5" } }, + dark: { color: { ref: ["gray", "100"], hex: "#424242" } }, + } as SassModeLeaf, + }, + label: { + foreground: { + idle: { + light: { color: { hex: "#000000" } }, + dark: { color: { hex: "#000000" } }, + } as SassModeLeaf, + }, + }, + }, + }; + + expect(serializeToSass(tree, "button", true)).toBe( + `$button: ( + contained: ( + background: ( + idle: ( + light: ( + color: ( + ref: (secondary, 500), + hex: #df1b74, + ), + ), + dark: ( + color: ( + ref: (secondary, 500), + hex: #df1b74, + ), + ), + ), + disabled: ( + light: ( + color: ( + ref: (gray, 100), + hex: #f5f5f5, + ), + ), + dark: ( + color: ( + ref: (gray, 100), + hex: #424242, + ), + ), + ), + ), + label: ( + foreground: ( + idle: ( + light: ( + color: ( + hex: #000000, + ), + ), + dark: ( + color: ( + hex: #000000, + ), + ), + ), + ), + ), + ), +); +`, + ); + }); + }); + + describe("border serialization integration", () => { + it("serializes a tree mixing color and border leaves", () => { + const tree: SassTree = { + contained: { + background: { + idle: { + light: { + color: { ref: ["secondary", "500"], hex: "#df1b74" }, + }, + dark: { + color: { ref: ["secondary", "500"], hex: "#df1b74" }, + }, + } as SassModeLeaf, + }, + outline: { + idle: { + light: { + border: { + color: { hex: "#e0e0e0" }, + width: { value: "1px" }, + style: "solid", + }, + }, + dark: { + border: { + color: { hex: "#424242" }, + width: { value: "1px" }, + style: "solid", + }, + }, + } as SassModeLeaf, + }, + }, + }; + const sass = serializeToSass(tree, "button"); + + // Color leaf is present + expect(sass).toContain("ref: (secondary, 500)"); + expect(sass).toContain("hex: #df1b74"); + + // Border leaf is present alongside color + expect(sass).toContain("border: ("); + expect(sass).toContain("style: solid"); + }); + + it("serializes different light/dark borders through the pipeline", () => { + const tree: SassTree = { + outline: { + idle: { + light: { + border: { + ref: ["borders", "default"], + color: { hex: "#e0e0e0" }, + width: { value: "1px" }, + style: "solid", + }, + }, + dark: { + border: { + ref: ["borders", "default"], + color: { hex: "#555555" }, + width: { value: "1px" }, + style: "solid", + }, + }, + } as SassModeLeaf, + }, + }; + const sass = serializeToSass(tree, "card"); + + expect(sass).toContain("hex: #e0e0e0"); + expect(sass).toContain("hex: #555555"); + expect(sass).toContain("ref: (borders, default)"); + }); + + it("produces expected Sass for a component with border tokens (pretty)", () => { + const tree: SassTree = { + outline: { + idle: { + light: { + border: { + color: { hex: "#e0e0e0" }, + width: { value: "1px" }, + style: "solid", + }, + }, + dark: { + border: { + color: { hex: "#424242" }, + width: { value: "1px" }, + style: "solid", + }, + }, + } as SassModeLeaf, + focus: { + light: { + border: { + color: { ref: ["primary", "500"], hex: "#1976d2" }, + width: { value: "2px" }, + style: "solid", + }, + }, + dark: { + border: { + color: { ref: ["primary", "300"], hex: "#64b5f6" }, + width: { value: "2px" }, + style: "solid", + }, + }, + } as SassModeLeaf, + }, + }; + + expect(serializeToSass(tree, "input", true)).toBe( + `$input: ( + outline: ( + idle: ( + light: ( + border: ( + color: ( + hex: #e0e0e0, + ), + width: ( + value: 1px, + ), + style: solid, + ), + ), + dark: ( + border: ( + color: ( + hex: #424242, + ), + width: ( + value: 1px, + ), + style: solid, + ), + ), + ), + focus: ( + light: ( + border: ( + color: ( + ref: (primary, 500), + hex: #1976d2, + ), + width: ( + value: 2px, + ), + style: solid, + ), + ), + dark: ( + border: ( + color: ( + ref: (primary, 300), + hex: #64b5f6, + ), + width: ( + value: 2px, + ), + style: solid, + ), + ), + ), + ), +); +`, + ); + }); + + it("handles a border leaf with only a color sub-value", () => { + const tree: SassTree = { + separator: { + idle: { + light: { border: { color: { hex: "#cccccc" } } }, + dark: { border: { color: { hex: "#444444" } } }, + } as SassModeLeaf, + }, + }; + const sass = serializeToSass(tree, "divider"); + + expect(sass).toContain("hex: #cccccc"); + expect(sass).toContain("hex: #444444"); + expect(sass).not.toContain("width:"); + expect(sass).not.toContain("style:"); + }); + + it("handles a border leaf with color alpha", () => { + const tree: SassTree = { + outline: { + idle: { + light: { + border: { + color: { alpha: 0.3, hex: "#00000033" }, + width: { value: "1px" }, + }, + }, + dark: { + border: { + color: { alpha: 0.3, hex: "#ffffff33" }, + width: { value: "1px" }, + }, + }, + } as SassModeLeaf, + }, + }; + const sass = serializeToSass(tree, "card"); + + expect(sass).toContain("alpha: 0.3"); + expect(sass).toContain("hex: #00000033"); + expect(sass).toContain("hex: #ffffff33"); + }); + }); + + describe("output validation — tree branches named after value keys", () => { + it("correctly nests outlined.border.color as branch > branch > leaf (not misidentified)", () => { + // The serializer must NOT misidentify the "border" branch as a mode leaf just because its child is named "color". + const tree: SassTree = { + outlined: { + border: { + color: { + light: { color: { hex: "#ffffff" } }, + dark: { color: { hex: "#000000" } }, + } as SassModeLeaf, + }, + }, + }; + + expect(serializeToSass(tree, "button", true)).toBe( + `$button: ( + outlined: ( + border: ( + color: ( + light: ( + color: ( + hex: #ffffff, + ), + ), + dark: ( + color: ( + hex: #000000, + ), + ), + ), + ), + ), +); +`, + ); + }); + + it("handles outlined.border.color with dark mode override", () => { + const tree: SassTree = { + outlined: { + border: { + color: { + light: { color: { ref: ["gray", "300"], hex: "#e0e0e0" } }, + dark: { color: { ref: ["gray", "700"], hex: "#616161" } }, + } as SassModeLeaf, + width: { + light: { dimension: { value: "1px" } }, + dark: { dimension: { value: "1px" } }, + } as SassModeLeaf, + style: { + light: { dimension: { value: "solid" } }, + dark: { dimension: { value: "solid" } }, + } as SassModeLeaf, + }, + }, + }; + const sass = serializeToSass(tree, "button"); + + // border.color branch serialized correctly (not collapsed) + expect(sass).toContain("ref: (gray, 300)"); + expect(sass).toContain("hex: #e0e0e0"); + expect(sass).toContain("ref: (gray, 700)"); + expect(sass).toContain("hex: #616161"); + + // border.width and border.style branches also present + expect(sass).toContain("value: 1px"); + expect(sass).toContain("value: solid"); + }); + + it("does not confuse a 'dimension' tree branch with a dimension mode leaf", () => { + const tree: SassTree = { + spacing: { + dimension: { + horizontal: { + light: { dimension: { value: "8px" } }, + dark: { dimension: { value: "8px" } }, + } as SassModeLeaf, + }, + }, + }; + const sass = serializeToSass(tree, "layout", true); + + // "dimension" is a tree branch, "horizontal" is the mode leaf + expect(sass).toContain("dimension: ("); + expect(sass).toContain("horizontal: ("); + expect(sass).toContain("value: 8px,"); + }); + + it("handles a mixed tree with both colliding branch names and real mode leaves", () => { + const tree: SassTree = { + outlined: { + border: { + color: { + light: { color: { hex: "#e0e0e0" } }, + dark: { color: { hex: "#424242" } }, + } as SassModeLeaf, + }, + background: { + idle: { + light: { color: { hex: "#ffffff" } }, + dark: { color: { hex: "#121212" } }, + } as SassModeLeaf, + }, + }, + }; + const sass = serializeToSass(tree, "button", true); + + // border.color is a branch > leaf (3 nesting levels before mode) + expect(sass).toContain("border: (\n color: (\n light:"); + + // background.idle is a branch > leaf (2 nesting levels before mode) + expect(sass).toContain("background: (\n idle: (\n light:"); + + // All hex values are present and correct + expect(sass).toContain("hex: #e0e0e0,"); + expect(sass).toContain("hex: #424242,"); + expect(sass).toContain("hex: #ffffff,"); + expect(sass).toContain("hex: #121212,"); + }); + }); +}); diff --git a/packages/plugins/tests/terrazzo/serializer.test.ts b/packages/plugins/tests/terrazzo/serializer.test.ts new file mode 100644 index 00000000..63f09106 --- /dev/null +++ b/packages/plugins/tests/terrazzo/serializer.test.ts @@ -0,0 +1,402 @@ +import { describe, expect, it } from "vitest"; +import { serializeToSass } from "../../src/terrazzo/sass-schema/serializer.js"; +import type { + SassBorderLeaf, + SassModeLeaf, + SassTree, +} from "../../src/terrazzo/sass-schema/types.js"; + +const refColorLeaf = (ref: string[], hex: string): SassModeLeaf => ({ + light: { color: { ref, hex } }, + dark: { color: { ref, hex } }, +}); + +const literalColorLeaf = (hex: string): SassModeLeaf => ({ + light: { color: { hex } }, + dark: { color: { hex } }, +}); + +const alphaColorLeaf = ( + ref: string[], + alpha: number, + hex: string, +): SassModeLeaf => ({ + light: { color: { ref, alpha, hex } }, + dark: { color: { ref, alpha, hex } }, +}); + +const borderLeaf = (border: SassBorderLeaf): SassModeLeaf => ({ + light: { border }, + dark: { border }, +}); + +describe("serializeToSass — compact (default)", () => { + it("produces a valid $variable declaration", () => { + const tree: SassTree = { + background: { + idle: refColorLeaf(["secondary", "500"], "#df1b74"), + }, + }; + const result = serializeToSass(tree, "button-contained"); + + expect(result).toMatch(/^\$button-contained: \(/); + expect(result).toMatch(/\);\n$/); + }); + + it("serializes a reference color leaf with parenthesized ref", () => { + const tree: SassTree = { + background: { + idle: refColorLeaf(["secondary", "500"], "#df1b74"), + }, + }; + const result = serializeToSass(tree, "button-contained"); + + expect(result).toContain("ref: (secondary, 500)"); + expect(result).toContain("hex: #df1b74"); + }); + + it("serializes a literal color leaf without ref", () => { + const tree: SassTree = { + label: { foreground: { idle: literalColorLeaf("#000000") } }, + }; + const result = serializeToSass(tree, "button-contained"); + + expect(result).toContain("hex: #000000"); + expect(result).not.toContain("ref:"); + }); + + it("serializes alpha when present", () => { + const tree: SassTree = { + background: { + hover: alphaColorLeaf(["secondary", "500"], 0.08, "#df1b7414"), + }, + }; + const result = serializeToSass(tree, "button-flat"); + + expect(result).toContain("alpha: 0.08"); + expect(result).toContain("ref: (secondary, 500)"); + expect(result).toContain("hex: #df1b7414"); + }); + + it("handles alpha: 0", () => { + const tree: SassTree = { + background: { + idle: { + light: { color: { alpha: 0, hex: "#ffffff00" } }, + dark: { color: { alpha: 0, hex: "#ffffff00" } }, + } as SassModeLeaf, + }, + }; + const result = serializeToSass(tree, "button-flat"); + + expect(result).toContain("alpha: 0"); + expect(result).toContain("hex: #ffffff00"); + }); + + it("serializes different light/dark values", () => { + const tree: SassTree = { + background: { + disabled: { + light: { color: { ref: ["grays", "grays-100"], hex: "#f5f5f5" } }, + dark: { color: { ref: ["grays", "grays-100"], hex: "#424242" } }, + } as SassModeLeaf, + }, + }; + const result = serializeToSass(tree, "button-contained"); + + expect(result).toContain("hex: #f5f5f5"); + expect(result).toContain("hex: #424242"); + }); +}); + +describe("serializeToSass — pretty", () => { + it("serializes a simple reference color leaf", () => { + const tree: SassTree = { + background: { + idle: refColorLeaf(["secondary", "500"], "#df1b74"), + }, + }; + + expect(serializeToSass(tree, "button-contained", true)).toBe( + `$button-contained: ( + background: ( + idle: ( + light: ( + color: ( + ref: (secondary, 500), + hex: #df1b74, + ), + ), + dark: ( + color: ( + ref: (secondary, 500), + hex: #df1b74, + ), + ), + ), + ), +); +`, + ); + }); + + it("serializes a deeply nested tree", () => { + const tree: SassTree = { + label: { foreground: { idle: literalColorLeaf("#000000") } }, + }; + + expect(serializeToSass(tree, "test", true)).toBe( + `$test: ( + label: ( + foreground: ( + idle: ( + light: ( + color: ( + hex: #000000, + ), + ), + dark: ( + color: ( + hex: #000000, + ), + ), + ), + ), + ), +); +`, + ); + }); + + it("serializes alpha when present", () => { + const result = serializeToSass( + { + background: { + hover: alphaColorLeaf(["secondary", "500"], 0.08, "#df1b7414"), + }, + }, + "button-flat", + true, + ); + + expect(result).toContain("ref: (secondary, 500),"); + expect(result).toContain("alpha: 0.08,"); + expect(result).toContain("hex: #df1b7414,"); + }); + + it("handles alpha: 0", () => { + const tree: SassTree = { + background: { + idle: { + light: { color: { alpha: 0, hex: "#ffffff00" } }, + dark: { color: { alpha: 0, hex: "#ffffff00" } }, + } as SassModeLeaf, + }, + }; + const result = serializeToSass(tree, "button-flat", true); + + expect(result).toContain("alpha: 0,"); + expect(result).toContain("hex: #ffffff00,"); + }); +}); + +describe("serializeToSass — border (compact)", () => { + it("serializes a full border with color, width, and style", () => { + const tree: SassTree = { + outline: { + idle: borderLeaf({ + color: { hex: "#333333" }, + width: { value: "1px" }, + style: "solid", + }), + }, + }; + const result = serializeToSass(tree, "card"); + + expect(result).toContain("border: ("); + expect(result).toContain("color: (hex: #333333)"); + expect(result).toContain("width: (value: 1px)"); + expect(result).toContain("style: solid"); + }); + + it("serializes a border with ref", () => { + const tree: SassTree = { + outline: { + idle: borderLeaf({ + ref: ["borders", "default"], + color: { hex: "#000000" }, + width: { value: "2px" }, + style: "dashed", + }), + }, + }; + const result = serializeToSass(tree, "card"); + + expect(result).toContain("ref: (borders, default)"); + expect(result).toContain("hex: #000000"); + expect(result).toContain("value: 2px"); + expect(result).toContain("style: dashed"); + }); + + it("serializes a border with only color", () => { + const tree: SassTree = { + divider: { + idle: borderLeaf({ color: { hex: "#cccccc" } }), + }, + }; + const result = serializeToSass(tree, "layout"); + + expect(result).toContain("border: (color: (hex: #cccccc))"); + expect(result).not.toContain("width:"); + expect(result).not.toContain("style:"); + }); + + it("serializes a border with only width", () => { + const tree: SassTree = { + outline: { + focus: borderLeaf({ width: { value: "3px" } }), + }, + }; + const result = serializeToSass(tree, "input"); + + expect(result).toContain("border: (width: (value: 3px))"); + expect(result).not.toContain("color:"); + expect(result).not.toContain("style:"); + }); + + it("serializes a border with color that has alpha", () => { + const tree: SassTree = { + outline: { + idle: borderLeaf({ + color: { alpha: 0.5, hex: "#00000080" }, + width: { value: "1px" }, + style: "solid", + }), + }, + }; + const result = serializeToSass(tree, "card"); + + expect(result).toContain("alpha: 0.5"); + expect(result).toContain("hex: #00000080"); + }); + + it("serializes a border with color ref", () => { + const tree: SassTree = { + outline: { + idle: borderLeaf({ + color: { ref: ["grays", "300"], hex: "#e0e0e0" }, + style: "solid", + }), + }, + }; + const result = serializeToSass(tree, "card"); + + expect(result).toContain("color: (ref: (grays, 300), hex: #e0e0e0)"); + expect(result).toContain("style: solid"); + }); + + it("serializes a border with width ref", () => { + const tree: SassTree = { + outline: { + idle: borderLeaf({ + width: { ref: ["spacing", "xs"], value: "1px" }, + }), + }, + }; + const result = serializeToSass(tree, "card"); + + expect(result).toContain( + "border: (width: (ref: (spacing, xs), value: 1px))", + ); + }); + + it("serializes different light/dark border values", () => { + const tree: SassTree = { + outline: { + idle: { + light: { + border: { + color: { hex: "#e0e0e0" }, + width: { value: "1px" }, + style: "solid", + }, + }, + dark: { + border: { + color: { hex: "#424242" }, + width: { value: "1px" }, + style: "solid", + }, + }, + } as SassModeLeaf, + }, + }; + const result = serializeToSass(tree, "card"); + + expect(result).toContain("hex: #e0e0e0"); + expect(result).toContain("hex: #424242"); + }); +}); + +describe("serializeToSass — border (pretty)", () => { + it("serializes a full border with indentation", () => { + const tree: SassTree = { + outline: { + idle: borderLeaf({ + color: { hex: "#333333" }, + width: { value: "1px" }, + style: "solid", + }), + }, + }; + + expect(serializeToSass(tree, "card", true)).toBe( + `$card: ( + outline: ( + idle: ( + light: ( + border: ( + color: ( + hex: #333333, + ), + width: ( + value: 1px, + ), + style: solid, + ), + ), + dark: ( + border: ( + color: ( + hex: #333333, + ), + width: ( + value: 1px, + ), + style: solid, + ), + ), + ), + ), +); +`, + ); + }); + + it("serializes a border with ref in pretty mode", () => { + const tree: SassTree = { + outline: { + idle: borderLeaf({ + ref: ["borders", "thin"], + color: { hex: "#cccccc" }, + style: "solid", + }), + }, + }; + const result = serializeToSass(tree, "card", true); + + expect(result).toContain("ref: (borders, thin),"); + expect(result).toContain("hex: #cccccc,"); + expect(result).toContain("style: solid,"); + }); +}); diff --git a/packages/plugins/tsconfig.json b/packages/plugins/tsconfig.json new file mode 100644 index 00000000..19af0ead --- /dev/null +++ b/packages/plugins/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "tests/**/*"] +} diff --git a/packages/plugins/vite.config.ts b/packages/plugins/vite.config.ts new file mode 100644 index 00000000..53d9d37d --- /dev/null +++ b/packages/plugins/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "vite"; +import dts from "vite-plugin-dts"; + +export default defineConfig({ + build: { + lib: { + entry: "src/index.ts", + formats: ["es"], + fileName: "index", + }, + rollupOptions: { + external: [ + "@bagrajs/core", + "unist-util-visit", + "sass-embedded", + "immutable", + ], + }, + }, + plugins: [dts({ rollupTypes: false })], +}); diff --git a/packages/plugins/vitest.config.ts b/packages/plugins/vitest.config.ts new file mode 100644 index 00000000..64682b98 --- /dev/null +++ b/packages/plugins/vitest.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["**/*.{test,spec}.ts"], + exclude: ["dist/**", "node_modules/**"], + environment: "node", + globals: false, + testTimeout: 10000, + coverage: { + provider: "v8", + include: ["tests/**/*.ts"], + exclude: [ + "dist", + "node_modules/**", + "**/*.test.ts", + "**/*.spec.ts", + "src/index.ts", + ], + }, + }, +}); diff --git a/packages/theming/package.json b/packages/theming/package.json index 71e4ec93..255ac1c9 100644 --- a/packages/theming/package.json +++ b/packages/theming/package.json @@ -17,6 +17,7 @@ "build:docs:en:production": "set NODE_ENV=production && npx sassdoc ./sass -d docs", "build:docs:en:staging": "set NODE_ENV=staging && npx sassdoc ./sass -d docs", "build:e2e": "sass --quiet ./tests/e2e/theme.scss ./tests/e2e/theme.css", + "build:tokens": "tz build", "clean": "npm run clean:json && npm run clean:tailwind && npm run clean:mcp && npm run clean:docs", "clean:json": "shx rm -rf dist/json", "clean:tailwind": "shx rm -rf dist/tailwind", @@ -100,6 +101,9 @@ "devDependencies": { "@ast-grep/cli": "^0.42.0", "@types/postcss-safe-parser": "^5.0.4", + "@igniteui-theming/plugins": "*", + "@terrazzo/parser": "^2.0.0", + "@terrazzo/cli": "^2.0.0", "igniteui-sassdoc-theme": "^1.1.6", "lunr": "^2.3.9", "postcss": "^8.4.35", diff --git a/packages/theming/terrazzo.config.ts b/packages/theming/terrazzo.config.ts new file mode 100644 index 00000000..8ffb2038 --- /dev/null +++ b/packages/theming/terrazzo.config.ts @@ -0,0 +1,33 @@ +import fs from "node:fs"; +import path from "node:path"; +import { sassSchemaPlugin } from "@igniteui-theming/plugins"; +import { defineConfig } from "@terrazzo/cli"; +import type { ConfigInit } from "@terrazzo/parser"; + +const TOKENS_DIR = "./tokens"; +const TOKENS_PATTERN = /^components-.*\.json$/; + +const tokenFiles = fs + .readdirSync(TOKENS_DIR) + .filter((file: string) => TOKENS_PATTERN.test(file)) + .map((file: string) => `./${path.join(TOKENS_DIR, file)}`); + +const config: ConfigInit = defineConfig({ + tokens: tokenFiles, + plugins: [ + sassSchemaPlugin({ + filePrefix: "_", + stripRefSegments: 1, + modes: ["light", "dark"], + pretty: true, + }), + ], + outDir: "./tokens/output/sass/schemas/components/", + lint: { + rules: { + "core/valid-color": "off", + }, + }, +}); + +export default config; diff --git a/packages/theming/tokens/components-avatar.json b/packages/theming/tokens/components-avatar.json new file mode 100644 index 00000000..ad829f75 --- /dev/null +++ b/packages/theming/tokens/components-avatar.json @@ -0,0 +1,86 @@ +{ + "avatar": { + "foreground": { + "$type": "reference", + "$value": { + "ref": "{primitive-colors.gray.800}", + "color": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [ + 0.259, + 0.259, + 0.259 + ], + "alpha": 1, + "hex": "#424242" + } + } + }, + "$extensions": { + "modes": { + "dark": { + "$value": { + "ref": "{primitive-colors.gray.800}", + "color": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [ + 0.961, + 0.961, + 0.961 + ], + "alpha": 1, + "hex": "#f5f5f5" + } + } + } + } + } + } + }, + "background": { + "$type": "reference", + "$value": { + "ref": "{primitive-colors.gray.400}", + "color": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [ + 0.741, + 0.741, + 0.741 + ], + "alpha": 1, + "hex": "#bdbdbd" + } + } + }, + "$extensions": { + "modes": { + "dark": { + "$value": { + "ref": "{primitive-colors.gray.200}", + "color": { + "$type": "color", + "$value": { + "colorSpace": "srgb", + "components": [ + 0.38, + 0.38, + 0.38 + ], + "alpha": 1, + "hex": "#616161" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/theming/tsconfig.json b/packages/theming/tsconfig.json index 5afd24e8..476fe78b 100644 --- a/packages/theming/tsconfig.json +++ b/packages/theming/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "../../tsconfig.base.json", - "include": ["tests/**/*", "vitest.config.ts"], + "include": ["tests/**/*", "terrazzo.config.ts", "vitest.config.ts"], "exclude": ["node_modules", "dist"] } diff --git a/vitest.config.ts b/vitest.config.ts index eb73860b..a2307cdf 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,10 +1,11 @@ -import {defineConfig} from 'vitest/config'; +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { projects: [ - 'packages/theming', - 'packages/mcp', + "packages/theming", + "packages/mcp", + "packages/plugins", ], }, });