diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..b51ec84 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,32 @@ +name: Bug Report +description: Report a bug or unexpected behavior +labels: [bug] +body: + - type: markdown + attributes: + value: Thanks for taking the time to fill out this bug report! + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of the bug + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Steps or code to reproduce the behavior + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What you expected to happen + - type: textarea + id: environment + attributes: + label: Environment + description: Node version, OS, browser (if applicable) + placeholder: Node 20, Ubuntu, Chrome 120 diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..4f60e4f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,26 @@ +name: Feature Request +description: Suggest an idea for this project +labels: [enhancement] +body: + - type: markdown + attributes: + value: Thanks for suggesting a feature! + - type: textarea + id: problem + attributes: + label: Problem + description: What problem does this feature solve? + validations: + required: true + - type: textarea + id: solution + attributes: + label: Proposed solution + description: How would you like this to work? + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: What alternatives have you considered? diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..68dbc95 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ +## Description + + + +## Type + +- [ ] Bug fix +- [ ] New feature +- [ ] Refactor +- [ ] Documentation +- [ ] CI/CD + +## Checklist + +- [ ] Tests pass (`npm test`) +- [ ] Lint passes (`npm run lint`) +- [ ] Typecheck passes (`npm run typecheck`) +- [ ] Build passes (`npm run build`) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3920394 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20, 22, 24, 26] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + cache-dependency-path: Build/package-lock.json + + - name: Install dependencies + working-directory: Build + run: npm ci + + - name: Type check + working-directory: Build + run: npm run typecheck + + - name: Lint + working-directory: Build + run: npm run lint + + - name: Run tests + working-directory: Build + run: npm test + + - name: Build + working-directory: Build + run: npm run build diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 8b9a74e..1f88d56 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -50,7 +50,7 @@ jobs: cp -r Build/dist/* gh-pages-root/playground/dist/ - name: Deploy all assets to gh-pages - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: gh-pages-root diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/Build/README.md b/Build/README.md index 10578ce..467c51d 100644 --- a/Build/README.md +++ b/Build/README.md @@ -1,5 +1,7 @@ # Sazami +[![CI](https://github.com/Nisoku/Sazami/actions/workflows/ci.yml/badge.svg)](https://github.com/Nisoku/Sazami/actions/workflows/ci.yml) +[![Deploy](https://github.com/Nisoku/Sazami/actions/workflows/pages.yml/badge.svg)](https://github.com/Nisoku/Sazami/actions/workflows/pages.yml) [![npm version](https://img.shields.io/npm/v/@nisoku/sazami.svg)](https://www.npmjs.com/package/@nisoku/sazami) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE) diff --git a/Build/eslint.config.ts b/Build/eslint.config.ts new file mode 100644 index 0000000..0c55fa4 --- /dev/null +++ b/Build/eslint.config.ts @@ -0,0 +1,31 @@ +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + ignores: ["dist/**", "node_modules/**", "coverage/**"], + }, + { + rules: { + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], + "@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": "allow-with-description" }], + }, + }, + { + files: ["**/*.js", "**/*.mjs", "**/*.cjs"], + rules: { + "no-undef": "off", + "@typescript-eslint/no-require-imports": "off", + }, + }, + { + files: ["**/tests/**", "**/*.test.ts", "**/*.spec.ts", "**/__mocks__/**"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-require-imports": "off", + "prefer-const": "off", + }, + }, +); diff --git a/Build/jest.config.js b/Build/jest.config.js index 8d9fb0c..57744c1 100644 --- a/Build/jest.config.js +++ b/Build/jest.config.js @@ -12,11 +12,24 @@ module.exports = { strict: true, esModuleInterop: true, skipLibCheck: true, - moduleResolution: "node", + moduleResolution: "node16", isolatedModules: true, }, }], }, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + ], + coverageDirectory: 'coverage', + coverageThreshold: { + global: { + branches: 70, + functions: 70, + lines: 70, + statements: 70 + } + }, moduleNameMapper: { "^.*icons/index.*$": "/tests/__mocks__/icons.js", }, diff --git a/Build/package-lock.json b/Build/package-lock.json index f8c0172..78ba38e 100644 --- a/Build/package-lock.json +++ b/Build/package-lock.json @@ -1,12 +1,12 @@ { "name": "@nisoku/sazami", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@nisoku/sazami", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "dependencies": { "@nisoku/sairin": "^0.1.2", @@ -14,15 +14,19 @@ "@nisoku/satori": "^0.1.3" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/jest": "^30.0.0", "@types/node": "^26.0.1", "css.escape": "^1.5.1", + "eslint": "^10.6.0", "jest": "^30.0.0", "jest-environment-jsdom": "^30.0.0", "jest-fixed-jsdom": "^0.0.11", + "jiti": "^2.7.0", "prettier": "^3.8.1", "ts-jest": "^29.4.0", "typescript": "^6.0.3", + "typescript-eslint": "^8.63.0", "vite": "^8.0.16", "vite-plugin-dts": "^5.0.2" } @@ -49,13 +53,13 @@ "license": "ISC" }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -64,9 +68,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -74,21 +78,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -105,14 +109,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -122,14 +126,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -139,9 +143,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -149,29 +153,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -191,9 +195,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -201,9 +205,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -211,9 +215,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -221,27 +225,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -490,33 +494,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -524,14 +528,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -693,6 +697,239 @@ "tslib": "^2.4.0" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1327,9 +1564,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1347,9 +1581,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1367,9 +1598,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1387,9 +1615,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1407,9 +1632,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1427,9 +1649,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1656,6 +1875,13 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1713,6 +1939,13 @@ "parse5": "^7.0.0" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.1.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", @@ -1754,6 +1987,288 @@ "dev": true, "license": "MIT" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", + "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/type-utils": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.63.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", + "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", + "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.63.0", + "@typescript-eslint/types": "^8.63.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", + "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", + "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", + "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", + "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", + "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.63.0", + "@typescript-eslint/tsconfig-utils": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", + "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", + "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.63.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -2116,6 +2631,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -2126,6 +2651,23 @@ "node": ">= 14" } }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2315,9 +2857,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.13", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz", - "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==", + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2338,9 +2880,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", "dev": true, "funding": [ { @@ -2358,10 +2900,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", "update-browserslist-db": "^1.2.3" }, "bin": { @@ -2422,9 +2964,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001784", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz", - "integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==", + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", "dev": true, "funding": [ { @@ -2726,6 +3268,13 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -2761,76 +3310,286 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, - "license": "MIT" + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", + "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, - "node_modules/electron-to-chromium": { - "version": "1.5.331", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", - "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "node_modules/eslint/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, "engines": { - "node": ">=0.12" + "node": ">=10" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "node_modules/eslint/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "is-arrayish": "^0.2.1" + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, "engines": { - "node": ">=8" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esprima": { @@ -2847,6 +3606,42 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "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": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", @@ -2854,6 +3649,16 @@ "dev": true, "license": "MIT" }, + "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/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -2920,6 +3725,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2927,6 +3739,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -2955,6 +3774,19 @@ } } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -2969,6 +3801,27 @@ "node": ">=8" } }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -3073,6 +3926,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3183,6 +4049,16 @@ "node": ">=0.10.0" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -3239,6 +4115,16 @@ "dev": true, "license": "MIT" }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3259,6 +4145,19 @@ "node": ">=6" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -4015,6 +4914,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4023,9 +4932,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", "dev": true, "license": "MIT", "dependencies": { @@ -4089,6 +4998,13 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -4096,6 +5012,20 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4109,6 +5039,16 @@ "node": ">=6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kolorist": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", @@ -4126,6 +5066,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -4647,11 +5601,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.37", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", - "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -4709,6 +5666,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4955,6 +5930,16 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "3.9.4", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz", @@ -5602,6 +6587,19 @@ "node": ">=18" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-jest": { "version": "29.4.11", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.11.tgz", @@ -5689,6 +6687,19 @@ "license": "0BSD", "optional": true }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -5726,6 +6737,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", + "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.63.0", + "@typescript-eslint/parser": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/ufo": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", @@ -5893,6 +6928,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -6113,6 +7158,16 @@ "node": ">= 8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", diff --git a/Build/package.json b/Build/package.json index 83db5ed..27ed8a2 100644 --- a/Build/package.json +++ b/Build/package.json @@ -1,6 +1,6 @@ { "name": "@nisoku/sazami", - "version": "0.1.0", + "version": "0.1.1", "description": "Sazami UI, the curvomorphic UI library", "license": "Apache-2.0", "repository": { @@ -30,9 +30,13 @@ "scripts": { "build": "vite build", "build:demo": "vite build && node ./scripts/build-demo.js", + "dev:demo": "node ./scripts/dev-demo.js", "test": "jest --no-cache", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", "typecheck": "tsc --noEmit", - "make-pretty": "prettier --write 'src/**/*.{ts,tsx,js,jsx,json,css,scss,md,html,cjs}'" + "make-pretty": "prettier --write 'src/**/*.{ts,tsx,js,jsx,json,css,scss,md,html,cjs}'", + "lint": "eslint ." }, "dependencies": { "@nisoku/sairin": "^0.1.2", @@ -40,15 +44,19 @@ "@nisoku/satori": "^0.1.3" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/jest": "^30.0.0", "@types/node": "^26.0.1", "css.escape": "^1.5.1", + "eslint": "^10.6.0", "jest": "^30.0.0", "jest-environment-jsdom": "^30.0.0", "jest-fixed-jsdom": "^0.0.11", + "jiti": "^2.7.0", "prettier": "^3.8.1", "ts-jest": "^29.4.0", "typescript": "^6.0.3", + "typescript-eslint": "^8.63.0", "vite": "^8.0.16", "vite-plugin-dts": "^5.0.2" } diff --git a/Build/scripts/build-demo.js b/Build/scripts/build-demo.js index 65d901a..ccaefb3 100644 --- a/Build/scripts/build-demo.js +++ b/Build/scripts/build-demo.js @@ -2,7 +2,7 @@ const fs = require('fs'); const path = require('path'); const srcDir = path.join(__dirname, '..', 'dist'); -const destDir = path.join(__dirname, '..', 'Demo', 'dist'); +const destDir = path.join(__dirname, '..', '..', 'Demo', 'dist'); try { // Remove existing Demo/dist diff --git a/Build/scripts/dev-demo.js b/Build/scripts/dev-demo.js new file mode 100644 index 0000000..bcbf15f --- /dev/null +++ b/Build/scripts/dev-demo.js @@ -0,0 +1,144 @@ +const fs = require('fs'); +const path = require('path'); +const http = require('http'); +const { spawn } = require('child_process'); + +const ROOT = path.resolve(__dirname, '..'); +const DIST = path.resolve(ROOT, 'dist'); +const DEMO = path.resolve(__dirname, '..', '..', 'Demo'); +const DEMO_DIST = path.resolve(DEMO, 'dist'); + +function copyDist() { + try { + if (fs.existsSync(DEMO_DIST)) { + fs.rmSync(DEMO_DIST, { recursive: true, force: true }); + } + fs.cpSync(DIST, DEMO_DIST, { recursive: true }); + console.log(`[${new Date().toLocaleTimeString()}] Copied dist → Demo/dist`); + } catch (err) { + console.error('Copy failed:', err.message); + } +} + +let buildProcess = null; +let watchTimer = null; +let watchReady = false; + +function startBuild() { + // Kill previous build if running + if (buildProcess) { + buildProcess.kill(); + buildProcess = null; + } + + buildProcess = spawn('npx', ['vite', 'build', '--watch'], { + cwd: ROOT, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + buildProcess.stdout.on('data', (data) => { + const text = data.toString(); + process.stdout.write(`[vite] ${text}`); + // When vite reports a completed build, trigger copy + if (text.includes('built in') || text.includes('modules transformed')) { + if (watchReady) { + // Debounce to wait for file writes to settle + clearTimeout(watchTimer); + watchTimer = setTimeout(() => { + // Wait for the output files to exist + setTimeout(copyDist, 200); + }, 500); + } + } + }); + + buildProcess.stderr.on('data', (data) => { + process.stderr.write(`[vite] ${data}`); + }); + + buildProcess.on('exit', (code) => { + console.log(`vite exited (code ${code})`); + buildProcess = null; + }); +} + +function startServer() { + const MIME = { + '.html': 'text/html', + '.css': 'text/css', + '.js': 'application/javascript', + '.mjs': 'application/javascript', + '.json': 'application/json', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + }; + const PORT = 5173; + const server = http.createServer((req, res) => { + let file = req.url === '/' ? '/index.html' : req.url.split('?')[0]; + const filePath = path.join(DEMO, file); + const ext = path.extname(filePath); + fs.readFile(filePath, (err, data) => { + if (err) { + res.writeHead(404); + res.end('Not found'); + return; + } + res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' }); + res.end(data); + }); + }); + server.listen(PORT, () => { + console.log(`Demo server at http://localhost:${PORT}`); + }); + server.unref(); +} + +async function main() { + console.log('Building for demo…'); + + // First do a full build synchronously + console.log('Running initial build…'); + const initial = spawn('npx', ['vite', 'build'], { + cwd: ROOT, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + initial.stdout.pipe(process.stdout); + initial.stderr.pipe(process.stderr); + + await new Promise((resolve, reject) => { + initial.on('exit', (code) => { + if (code === 0) resolve(); + else reject(new Error(`Initial build failed (code ${code})`)); + }); + }); + + // Copy the initial build + copyDist(); + watchReady = true; + + // Start a static dev server for Demo/ + startServer(); + + // Now start the watch mode build + console.log('\nWatching for changes…'); + startBuild(); + + // Graceful shutdown + process.on('SIGINT', () => { + console.log('\nShutting down…'); + if (buildProcess) buildProcess.kill(); + process.exit(0); + }); + + process.on('SIGTERM', () => { + if (buildProcess) buildProcess.kill(); + process.exit(0); + }); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/Build/src/config.ts b/Build/src/config.ts index 5b22e01..c5f213c 100644 --- a/Build/src/config.ts +++ b/Build/src/config.ts @@ -4,7 +4,7 @@ export interface SazamiConfig { satori: SatoriInstance | null; } -let currentConfig: SazamiConfig = { +const currentConfig: SazamiConfig = { satori: null, }; diff --git a/Build/src/errors.ts b/Build/src/errors.ts index d3cb7ea..5586e0a 100644 --- a/Build/src/errors.ts +++ b/Build/src/errors.ts @@ -1,18 +1,31 @@ -let logger: any = null; +type Logger = { + info: (msg: string, opts?: Record) => void; + warn: (msg: string, opts?: Record) => void; + error: (msg: string, opts?: Record) => void; +}; -function getLogger(scope: string) { +declare function require(id: string): unknown; + +let logger: Logger | null = null; + +function getLogger(scope: string): Logger { if (!logger) { try { - const satori = require("@nisoku/satori-log"); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const satori = require("@nisoku/satori-log") as { + createSatori: (opts: { logLevel: string; enableConsole: boolean }) => { + createLogger: (scope: string) => Logger; + }; + }; const s = satori.createSatori({ logLevel: "error", enableConsole: true }); logger = s.createLogger(scope); } catch { logger = { - info: (msg: string, opts?: any) => + info: (msg: string, opts?: unknown) => console.log(`[${scope}] ${msg}`, opts), - warn: (msg: string, opts?: any) => + warn: (msg: string, opts?: unknown) => console.warn(`[${scope}] ${msg}`, opts), - error: (msg: string, opts?: any) => + error: (msg: string, opts?: unknown) => console.error(`[${scope}] ${msg}`, opts), }; } diff --git a/Build/src/escape.ts b/Build/src/escape.ts index e6e779c..8d78ae2 100644 --- a/Build/src/escape.ts +++ b/Build/src/escape.ts @@ -55,5 +55,5 @@ export function escapeUrl(str: string): string { * Escapes characters that have special meaning in CSS. */ export function escapeCss(str: string): string { - return str.replace(/[!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/g, "\\$&"); + return str.replace(/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&"); } diff --git a/Build/src/icons/index.ts b/Build/src/icons/index.ts index e365501..3d47139 100644 --- a/Build/src/icons/index.ts +++ b/Build/src/icons/index.ts @@ -1,4 +1,4 @@ -// @ts-expect-error +// @ts-expect-error - Vite glob import type const iconModules = import.meta.glob("./*.svg", { query: "?raw", import: "default", diff --git a/Build/src/index.ts b/Build/src/index.ts index c6b0cac..0dac713 100644 --- a/Build/src/index.ts +++ b/Build/src/index.ts @@ -21,7 +21,10 @@ export { ICON_SVGS } from "./icons/index"; export { escapeHtml, unescapeHtml, escapeUrl, escapeCss } from "./escape"; +export { ReactiveContext } from "./runtime/reactive-context"; + import { parseSakko } from "@nisoku/sakko"; +import type { AtcodeDeclaration } from "@nisoku/sakko"; import { transformAST, VNode, @@ -32,9 +35,14 @@ import { generateThemeCSS } from "./config/generator"; import { enableCurvomorphism } from "./curvomorphism/index"; import { registerComponents } from "./primitives/registry"; import { parseModifiers as pmParseModifiers } from "./primitives/modifier-map"; +import { ReactiveContext } from "./runtime/reactive-context"; let themeInjected = false; +export function isThemeInjected(): boolean { + return themeInjected; +} + export function injectThemeCSS(customTokens?: Record): void { if (typeof document === "undefined") return; @@ -52,11 +60,39 @@ export function injectThemeCSS(customTokens?: Record): void { themeInjected = true; } +function processDeclarations( + declarations: AtcodeDeclaration[], + context: ReactiveContext, +): void { + for (const decl of declarations) { + if (decl.type === "state") { + for (const { name, value } of decl.declarations) { + context.addState(name, value); + } + } else if (decl.type === "derived") { + for (const { name, expr } of decl.declarations) { + context.addDerived(name, expr); + } + } else if (decl.type === "effect") { + context.addEffect(decl.body); + } + } +} + export function compileSakko( source: string, target: HTMLElement, - options?: { tokens?: Record }, + options?: { tokens?: Record; replace?: boolean }, ): void { + if (options?.replace !== false) { + target.querySelectorAll("*").forEach((el) => { + const d = (el as Element & { __sazamiIfDisposer?: () => void }) + .__sazamiIfDisposer; + if (d) d(); + }); + target.innerHTML = ""; + } + if (typeof customElements !== "undefined") { registerComponents(); } @@ -72,14 +108,20 @@ export function compileSakko( const ast = parseSakko(wrapped); + // Process reactive declarations + const context = new ReactiveContext(ast.name); + processDeclarations(ast.declarations, context); + // Always render the root element as a component wrapper. + // If the root name isn't a registered Sazami component, wrap in a div. + const tag = getTagName(ast.name); const rootVNode: VNode = { - type: getTagName(ast.name), + type: tag.startsWith("saz-") && !customElements.get(tag) ? "div" : tag, props: ast.modifiers ? pmParseModifiers(ast.modifiers) : {}, children: [], }; for (const child of ast.children) { - const result = transformAST(child); + const result = transformAST(child, context); if (Array.isArray(result)) { rootVNode.children.push(...result); } else { @@ -91,13 +133,14 @@ export function compileSakko( if (typeof window !== "undefined") { // Disconnect any observer from a previous compileSakko call on this target // so re-renders (e.g. playground live preview) don't leave stale observers. - const prev = (target as any).__sazamiRO as ResizeObserver | undefined; + const prev = (target as HTMLElement & { __sazamiRO?: ResizeObserver }) + .__sazamiRO; if (prev) prev.disconnect(); // Dispose any previous curvomorphism listeners - const prevDisposers = (target as any).__sazamiCurvoDisposers as - | Array<() => void> - | undefined; + const prevDisposers = ( + target as HTMLElement & { __sazamiCurvoDisposers?: Array<() => void> } + ).__sazamiCurvoDisposers; if (prevDisposers) { prevDisposers.forEach((d) => { d(); @@ -141,8 +184,10 @@ export function compileSakko( }); }); }); - (target as any).__sazamiRO = ro; - (target as any).__sazamiCurvoDisposers = disposers; + (target as HTMLElement & { __sazamiRO: ResizeObserver }).__sazamiRO = ro; + ( + target as HTMLElement & { __sazamiCurvoDisposers: Array<() => void> } + ).__sazamiCurvoDisposers = disposers; ro.observe(target); } } diff --git a/Build/src/primitives/avatar.ts b/Build/src/primitives/avatar.ts index 1192f6c..57c1707 100644 --- a/Build/src/primitives/avatar.ts +++ b/Build/src/primitives/avatar.ts @@ -52,6 +52,7 @@ const avatarConfig = { @component(avatarConfig) export class SazamiAvatar extends SazamiComponent { private _srcSignal: Readable | null = null; + private _src: string | undefined; private _imgElement: HTMLImageElement | null = null; private _initialsElement: HTMLElement | null = null; private _srcDisposer: (() => void) | null = null; @@ -84,7 +85,7 @@ export class SazamiAvatar extends SazamiComponent { private _getCurrentSrc(): string { if (this._srcSignal) return this._srcSignal.get(); - if ((this as any)._src) return (this as any)._src; + if (this._src) return this._src; return this.getAttribute("src") || ""; } @@ -93,15 +94,15 @@ export class SazamiAvatar extends SazamiComponent { } set src(value: string | Readable) { - const wasImageMode = this._isImageMode; + const _wasImageMode = this._isImageMode; const hasReadable = this._isReadableStr(value); if (hasReadable) { this._srcSignal = value; - (this as any)._src = undefined; + this._src = undefined; } else { this._srcSignal = null; - (this as any)._src = value; + this._src = value; } const nowImageMode = this._isImageModeNow(); @@ -116,7 +117,7 @@ export class SazamiAvatar extends SazamiComponent { this._disposeSrcBinding(); this._setupSrcBinding(); if (!this._srcSignal && this._imgElement) { - this._imgElement.src = (this as any)._src || ""; + this._imgElement.src = this._src || ""; this._imgElement.alt = this.getAttribute("alt") || ""; } } else if (!nowImageMode && this._initialsElement) { @@ -129,7 +130,7 @@ export class SazamiAvatar extends SazamiComponent { } get src(): string | Readable { - return this._srcSignal || (this as any)._src || ""; + return this._srcSignal || this._src || ""; } private _setupSrcBinding() { @@ -263,7 +264,7 @@ export class SazamiAvatar extends SazamiComponent { ) { super.attributeChangedCallback(name, oldValue, newValue); if (name === "src" && oldValue !== newValue) { - (this as any)._src = newValue || ""; + this._src = newValue || ""; if (!this._srcSignal) { const wasImageMode = this._isImageMode; const nowImageMode = !!this._getCurrentSrc(); diff --git a/Build/src/primitives/base.ts b/Build/src/primitives/base.ts index b98af20..a7e0b0e 100644 --- a/Build/src/primitives/base.ts +++ b/Build/src/primitives/base.ts @@ -1,9 +1,4 @@ -import { - propertyError, - eventError, - bindingError, - renderError, -} from "../errors"; +import { eventError, bindingError, renderError } from "../errors"; import { type Signal, type Readable, effect } from "@nisoku/sairin"; import { bindText, @@ -53,9 +48,7 @@ export interface PropertyConfigBoolean { } export type AnyPropertyConfig = - | PropertyConfig - | PropertyConfigNumber - | PropertyConfigBoolean; + PropertyConfig | PropertyConfigNumber | PropertyConfigBoolean; export interface EventConfig { name: string; @@ -65,7 +58,7 @@ export interface EventConfig { export type BindingType = "attribute" | "property" | "input"; // Get property config safely -type GetPropConfig = +type _GetPropConfig = C["properties"] extends Record ? P extends keyof C["properties"] ? C["properties"][P] @@ -73,14 +66,16 @@ type GetPropConfig = : never; // Map property name -> its actual TS type -type PropType = - GetPropConfig extends AnyPropertyConfig - ? GetPropConfig["type"] extends "boolean" - ? boolean - : GetPropConfig["type"] extends "number" - ? number - : string - : string; +type PropType< + C extends SazamiComponentConfig, + P extends string, +> = P extends keyof C["properties"] + ? C["properties"][P] extends { type: "boolean" } + ? boolean + : C["properties"][P] extends { type: "number" } + ? number + : string + : unknown; // Map event's detail object -> inferred detail type type EventDetail< @@ -94,7 +89,7 @@ type EventDetail< export type InferProps = C["properties"] extends Record ? { [P in keyof C["properties"]]: PropType } - : {}; + : Record; // All events with their detail types export type InferEvents = @@ -103,10 +98,10 @@ export type InferEvents = [E in keyof C["events"]]: C["events"][E] extends EventConfig ? C["events"][E]["detail"] extends Record ? EventDetail - : {} - : {}; + : Record + : Record; } - : {}; + : Record; // Decorator stores metadata on prototype for base class to consume export function component(config: C) { @@ -123,7 +118,7 @@ export function component(config: C) { let _nextComponentId = 0; export class SazamiComponent< - C extends SazamiComponentConfig = any, + C extends SazamiComponentConfig = SazamiComponentConfig, > extends HTMLElement { // Declare sazamiConfig, set by decorator on prototype declare sazamiConfig: C; @@ -167,9 +162,8 @@ export class SazamiComponent< // Static observedAttributes derived from properties with reflect: true static get observedAttributes(): string[] { - const cfg = (this.prototype as any).sazamiConfig as - | SazamiComponentConfig - | undefined; + const cfg = (this.prototype as { sazamiConfig?: SazamiComponentConfig }) + .sazamiConfig; if (!cfg) return []; // If explicitly provided, use that @@ -188,7 +182,7 @@ export class SazamiComponent< } protected getStructuralRoot(): string | null { - const cfg = (this as any).sazamiConfig as SazamiComponentConfig | undefined; + const cfg = (this as { sazamiConfig?: SazamiComponentConfig }).sazamiConfig; if (cfg?.structuralRoots) { const mode = this.getRenderMode(); return cfg.structuralRoots[mode] ?? null; @@ -339,7 +333,7 @@ export class SazamiComponent< protected bind( selector: string, target: BindTarget, - readable: Readable, + readable: Readable, ): void { const element = selector === ":host" ? this : this.$(selector); if (!element) { @@ -376,7 +370,11 @@ export class SazamiComponent< if ("set" in readable) { dispose = bindSelectValue(element, readable as Signal); } else { - dispose = bindProperty(element, "value", readable); + dispose = bindProperty( + element, + "value", + readable as Readable, + ); } } else { bindingError( @@ -453,7 +451,7 @@ export class SazamiComponent< protected bindAttribute( selector: string, attr: string, - readable: Readable, + readable: Readable, ): (() => void) | void { return this.bind(selector, attr, readable); } @@ -468,7 +466,11 @@ export class SazamiComponent< bindingError(`Element not found: ${selector}`, {}); return; } - const dispose = bindProperty(element, prop as any, readable); + const dispose = bindProperty( + element, + prop as keyof HTMLElement, + readable as Readable, + ); this._cleanupFns.push(dispose); } @@ -568,11 +570,9 @@ export class SazamiComponent< } // Handler registry: addHandler returns an ID for later removal - // Using Function type to accept both EventListener and specific event handlers like KeyboardEvent - // eslint-disable-next-line @typescript-eslint/no-explicit-any protected addHandler( type: string, - handler: Function, + handler: EventListener, options?: { internal?: boolean; element?: EventTarget }, ): number { const id = ++this._handlerId; @@ -588,11 +588,9 @@ export class SazamiComponent< return id; } - // Remove handler by ID, function reference, or type+id/type+fn - // eslint-disable-next-line @typescript-eslint/no-explicit-any protected removeHandler( typeOrId: string | number, - idOrFn?: number | Function, + idOrFn?: number | EventListener, ) { // If only one arg and it's a number, remove by ID across all types if (typeof typeOrId === "number") { @@ -656,7 +654,7 @@ export class SazamiComponent< } // dispatch custom events - protected dispatch( + protected dispatch( name: string, detail?: T, options: { bubbles?: boolean; composed?: boolean } = {}, @@ -681,7 +679,7 @@ export class SazamiComponent< // Find the event config by key const eventKey = event as string; - const eventConfig = (events as any)[eventKey]; + const eventConfig = events[eventKey]; if (!eventConfig) { eventError(`Event "${String(event)}" not defined in metadata`, { @@ -693,7 +691,7 @@ export class SazamiComponent< this.dispatchEvent( new CustomEvent(eventConfig.name, { - detail: detail as any, + detail, bubbles: true, composed: true, }), diff --git a/Build/src/primitives/button.ts b/Build/src/primitives/button.ts index 7463c3d..d0343d4 100644 --- a/Build/src/primitives/button.ts +++ b/Build/src/primitives/button.ts @@ -130,9 +130,11 @@ export class SazamiButton extends SazamiComponent { } this.removeHandler("click", this._handleClick); - this.removeHandler("keydown", this._handleKeydown); + this.removeHandler("keydown", this._handleKeydown as EventListener); this.addHandler("click", this._handleClick, { internal: true }); - this.addHandler("keydown", this._handleKeydown, { internal: true }); + this.addHandler("keydown", this._handleKeydown as EventListener, { + internal: true, + }); } private _handleClick = () => { diff --git a/Build/src/primitives/checkbox.ts b/Build/src/primitives/checkbox.ts index 6229b7d..de89150 100644 --- a/Build/src/primitives/checkbox.ts +++ b/Build/src/primitives/checkbox.ts @@ -72,7 +72,9 @@ const checkboxConfig = { @component(checkboxConfig) export class SazamiCheckbox extends SazamiComponent { private _checkedSignal: Readable | null = null; + private _checked: boolean | undefined; private _disabledSignal: Readable | null = null; + private _disabled: boolean | undefined; private _isReadableBool(value: unknown): value is Readable { return isSignal(value) || value instanceof Derived; @@ -89,11 +91,11 @@ export class SazamiCheckbox extends SazamiComponent { } get checked(): boolean | Readable { - return this._checkedSignal || (this as any)._checked || false; + return this._checkedSignal || this._checked || false; } private _setChecked(value: boolean) { - (this as any)._checked = value; + this._checked = value; if (value) { this.setAttribute("checked", ""); } else { @@ -113,11 +115,11 @@ export class SazamiCheckbox extends SazamiComponent { } get disabled(): boolean | Readable { - return this._disabledSignal || (this as any)._disabled || false; + return this._disabledSignal || this._disabled || false; } private _setDisabled(value: boolean) { - (this as any)._disabled = value; + this._disabled = value; if (value) { this.setAttribute("disabled", ""); } else { @@ -128,7 +130,7 @@ export class SazamiCheckbox extends SazamiComponent { private _getIsDisabled(): boolean { if (this._disabledSignal) return this._disabledSignal.get(); - if ((this as any)._disabled !== undefined) return !!(this as any)._disabled; + if (this._disabled !== undefined) return !!this._disabled; return this.hasAttribute("disabled"); } @@ -149,9 +151,11 @@ export class SazamiCheckbox extends SazamiComponent { this._updateAria(); this.removeHandler("click", this._handleClick); - this.removeHandler("keydown", this._handleKeydown); + this.removeHandler("keydown", this._handleKeydown as EventListener); this.addHandler("click", this._handleClick, { internal: true }); - this.addHandler("keydown", this._handleKeydown, { internal: true }); + this.addHandler("keydown", this._handleKeydown as EventListener, { + internal: true, + }); } private _handleClick = () => { @@ -166,7 +170,7 @@ export class SazamiCheckbox extends SazamiComponent { this._updateAria(); } } else { - const newValue = !((this as any)._checked || false); + const newValue = !(this._checked || false); this._setChecked(newValue); this._updateAria(newValue); this.dispatchEventTyped("change", { checked: newValue }); @@ -186,7 +190,7 @@ export class SazamiCheckbox extends SazamiComponent { ? checked : this._checkedSignal ? this._checkedSignal.get() - : !!(this as any)._checked; + : !!this._checked; const isDisabled = this._getIsDisabled(); this.setAttribute("aria-checked", isChecked ? "true" : "false"); if (isDisabled) { diff --git a/Build/src/primitives/chip.ts b/Build/src/primitives/chip.ts index 91bd5e3..7f95394 100644 --- a/Build/src/primitives/chip.ts +++ b/Build/src/primitives/chip.ts @@ -1,5 +1,5 @@ import { SazamiComponent, component } from "./base"; -import { STATE_DISABLED, INTERACTIVE_HOVER, VARIANT_BG_RULES } from "./shared"; +import { STATE_DISABLED, INTERACTIVE_HOVER } from "./shared"; import { ICON_SVGS } from "../icons/index"; import { escapeHtml } from "../escape"; import { Signal } from "@nisoku/sairin"; @@ -139,7 +139,9 @@ export class SazamiChip extends SazamiComponent { } this.addHandler("click", this._handleClick, { internal: true }); - this.addHandler("keydown", this._handleKeydown, { internal: true }); + this.addHandler("keydown", this._handleKeydown as EventListener, { + internal: true, + }); if (this.disabledSignal) { this.bindDisabled(":host", this.disabledSignal); diff --git a/Build/src/primitives/coverart.ts b/Build/src/primitives/coverart.ts index d693c28..d9cda4c 100644 --- a/Build/src/primitives/coverart.ts +++ b/Build/src/primitives/coverart.ts @@ -43,6 +43,7 @@ export class SazamiCoverart extends SazamiComponent { private _srcSignal: Readable | null = null; private _imgElement: HTMLImageElement | null = null; private _pendingSrc: string | null = null; + private _src: string | undefined; private _srcEffectDispose: (() => void) | null = null; private _isReadableStr(value: unknown): value is Readable { @@ -69,7 +70,7 @@ export class SazamiCoverart extends SazamiComponent { this._srcEffectDispose = null; } this._pendingSrc = value; - (this as any)._src = value; + this._src = value; if (!this._imgElement) { this.render(); } else { @@ -79,7 +80,7 @@ export class SazamiCoverart extends SazamiComponent { } get src(): string | Readable { - return this._srcSignal || (this as any)._src || ""; + return this._srcSignal || this._src || ""; } private _updateSrc(value: string) { @@ -105,7 +106,7 @@ export class SazamiCoverart extends SazamiComponent { const currentSrc = this._srcSignal ? this._srcSignal.get() : this._pendingSrc || - (this as any)._src || + this._src || this.getAttribute("src") || this.textContent?.trim() || ""; diff --git a/Build/src/primitives/divider.ts b/Build/src/primitives/divider.ts index 2abb96e..9c5bc71 100644 --- a/Build/src/primitives/divider.ts +++ b/Build/src/primitives/divider.ts @@ -1,5 +1,4 @@ import { SazamiComponent, component } from "./base"; -import { VARIANT_BG_RULES } from "./shared"; const STYLES = ` :host { diff --git a/Build/src/primitives/generic.ts b/Build/src/primitives/generic.ts index 3575c3c..57a1bdd 100644 --- a/Build/src/primitives/generic.ts +++ b/Build/src/primitives/generic.ts @@ -12,9 +12,9 @@ ${GAP_RULES} :host([justify="space-between"]) { justify-content: space-between; } `; -export function createGenericClass( - config?: C, -): { new (): SazamiComponent } { +export function createGenericClass< + C extends SazamiComponentConfig = SazamiComponentConfig, +>(config?: C): { new (): SazamiComponent } { class Generic extends SazamiComponent { render() { this.mount(STYLES, ``); @@ -22,7 +22,7 @@ export function createGenericClass( } if (config) { - component(config)(Generic as any); + component(config)(Generic as { new (): SazamiComponent }); } return Generic as { new (): SazamiComponent }; diff --git a/Build/src/primitives/icon-button.ts b/Build/src/primitives/icon-button.ts index e65c9bd..22f2da6 100644 --- a/Build/src/primitives/icon-button.ts +++ b/Build/src/primitives/icon-button.ts @@ -6,7 +6,7 @@ import { VARIANT_TEXT_RULES, } from "./shared"; import { ICON_SVGS } from "../icons/index"; -import { Signal, Derived, isSignal, type Readable } from "@nisoku/sairin"; +import { Derived, isSignal, type Readable } from "@nisoku/sairin"; const STYLES = ` :host { @@ -79,6 +79,7 @@ export class SazamiIconButton extends SazamiComponent { private _handlersAdded = false; private _autoAriaLabel = false; + private _disabled: boolean | undefined; private _disabledSignal: Readable | null = null; private _isReadableBool(value: unknown): value is Readable { @@ -96,11 +97,11 @@ export class SazamiIconButton extends SazamiComponent { } get disabled(): boolean | Readable { - return this._disabledSignal || (this as any)._disabled || false; + return this._disabledSignal || this._disabled || false; } private _setDisabled(value: boolean) { - (this as any)._disabled = value; + this._disabled = value; if (value) { this.setAttribute("disabled", ""); } else { @@ -110,7 +111,7 @@ export class SazamiIconButton extends SazamiComponent { private _getIsDisabled(): boolean { if (this._disabledSignal) return this._disabledSignal.get(); - if ((this as any)._disabled !== undefined) return !!(this as any)._disabled; + if (this._disabled !== undefined) return !!this._disabled; return this.hasAttribute("disabled"); } @@ -143,7 +144,9 @@ export class SazamiIconButton extends SazamiComponent { if (!this._handlersAdded) { this._handlersAdded = true; this.addHandler("click", this._handleClick, { internal: true }); - this.addHandler("keydown", this._handleKeydown, { internal: true }); + this.addHandler("keydown", this._handleKeydown as EventListener, { + internal: true, + }); } } diff --git a/Build/src/primitives/icon.ts b/Build/src/primitives/icon.ts index f3ae5fa..3338356 100644 --- a/Build/src/primitives/icon.ts +++ b/Build/src/primitives/icon.ts @@ -2,13 +2,7 @@ import { SazamiComponent, component } from "./base"; import { VARIANT_TEXT_RULES } from "./shared"; import { ICON_SVGS } from "../icons/index"; import { escapeHtml } from "../escape"; -import { - Signal, - Derived, - isSignal, - effect, - type Readable, -} from "@nisoku/sairin"; +import { Derived, isSignal, effect, type Readable } from "@nisoku/sairin"; const STYLES = ` :host { @@ -51,6 +45,7 @@ export class SazamiIcon extends SazamiComponent { private _iconSignal: Readable | null = null; private _iconElement: HTMLElement | null = null; + private _icon: string | undefined; private _iconEffectDispose: (() => void) | null = null; private _isReadableStr(value: unknown): value is Readable { @@ -69,13 +64,13 @@ export class SazamiIcon extends SazamiComponent { this._iconEffectDispose(); this._iconEffectDispose = null; } - (this as any)._icon = value; + this._icon = value; this._updateIcon(value); } } get icon(): string | Readable { - return this._iconSignal || (this as any)._icon || ""; + return this._iconSignal || this._icon || ""; } private _updateIcon(iconName: string) { @@ -154,7 +149,7 @@ export class SazamiIcon extends SazamiComponent { render() { const iconName = this._iconSignal ? this._iconSignal.get() - : (this as any)._icon || + : this._icon || this.getAttribute("icon") || this.textContent?.trim() || ""; diff --git a/Build/src/primitives/image.ts b/Build/src/primitives/image.ts index 06d81aa..1c712e8 100644 --- a/Build/src/primitives/image.ts +++ b/Build/src/primitives/image.ts @@ -1,5 +1,5 @@ import { SazamiComponent, component } from "./base"; -import { SHAPE_RULES, SIZE_RULES } from "./shared"; +import { SHAPE_RULES } from "./shared"; import { escapeHtml } from "../escape"; import { Derived, isSignal, type Readable } from "@nisoku/sairin"; import { bindProperty } from "@nisoku/sairin"; @@ -41,6 +41,7 @@ export class SazamiImage extends SazamiComponent { private _srcSignal: Readable | null = null; private _imgElement: HTMLImageElement | null = null; private _pendingSrc: string | null = null; + private _src: string | undefined; private _srcDispose: (() => void) | null = null; private _isReadableStr(value: unknown): value is Readable { @@ -58,13 +59,13 @@ export class SazamiImage extends SazamiComponent { } else { this._srcSignal = null; this._pendingSrc = value; - (this as any)._src = value; + this._src = value; this._updateSrc(value); } } get src(): string | Readable { - return this._srcSignal || (this as any)._src || ""; + return this._srcSignal || this._src || ""; } private _updateSrc(value: string) { @@ -108,8 +109,7 @@ export class SazamiImage extends SazamiComponent { if (this._srcSignal) return this._srcSignal.get(); if (this._pendingSrc !== undefined && this._pendingSrc !== null) return this._pendingSrc; - if ((this as any)._src !== undefined && (this as any)._src !== null) - return (this as any)._src; + if (this._src !== undefined && this._src !== null) return this._src; return this.getAttribute("src") || ""; } diff --git a/Build/src/primitives/input.ts b/Build/src/primitives/input.ts index 43813c1..784f419 100644 --- a/Build/src/primitives/input.ts +++ b/Build/src/primitives/input.ts @@ -8,7 +8,6 @@ import { effect, type Readable, } from "@nisoku/sairin"; -import { bindInputValue } from "@nisoku/sairin"; const STYLES = ` :host { display: block; } @@ -64,6 +63,7 @@ export class SazamiInput extends SazamiComponent { private _input: HTMLInputElement | null = null; private _valueEffectDisposer: (() => void) | null = null; private _inputHandler: ((e: Event) => void) | null = null; + private _value: string = ""; private _isReadableStr(value: unknown): value is Readable { return isSignal(value) || value instanceof Derived; @@ -97,7 +97,7 @@ export class SazamiInput extends SazamiComponent { } else { this._disposeValueBindings(); this._valueSignal = null; - (this as any)._value = valueOrSignal; + this._value = valueOrSignal; if (this._input && this._input.value !== valueOrSignal) { this._input.value = valueOrSignal || ""; } @@ -117,7 +117,7 @@ export class SazamiInput extends SazamiComponent { get value(): string | Readable { if (this._valueSignal) return this._valueSignal.get(); - if ((this as any)._value) return (this as any)._value; + if (this._value) return this._value; if (this._input) return this._input.value; return this.getAttribute("value") || ""; } @@ -129,7 +129,7 @@ export class SazamiInput extends SazamiComponent { const type = this.getAttribute("type") || "text"; const initialValue = this._valueSignal ? this._valueSignal.get() - : this.getAttribute("value") || (this as any)._value || ""; + : this.getAttribute("value") || this._value || ""; this.mount( STYLES, @@ -156,7 +156,7 @@ export class SazamiInput extends SazamiComponent { if (isSignal(this._valueSignal)) { (this._valueSignal as Signal).set(target.value); } - (this.dispatchEventTyped as any)("input", { value: target.value }); + this.dispatchEventTyped("input", { value: target.value }); }; this._input.addEventListener("input", this._inputHandler); this.onCleanup(() => { @@ -169,8 +169,8 @@ export class SazamiInput extends SazamiComponent { "input", (e: Event) => { const target = e.target as HTMLInputElement; - (this as any)._value = target.value; - (this.dispatchEventTyped as any)("input", { value: target.value }); + this._value = target.value; + this.dispatchEventTyped("input", { value: target.value }); }, { internal: true, element: this._input }, ); diff --git a/Build/src/primitives/modifier-map.ts b/Build/src/primitives/modifier-map.ts index 4902b6f..21c7c53 100644 --- a/Build/src/primitives/modifier-map.ts +++ b/Build/src/primitives/modifier-map.ts @@ -1,6 +1,6 @@ import type { Modifier } from "@nisoku/sakko"; -export const MODIFIER_MAP: Record> = { +export const MODIFIER_MAP: Record> = { accent: { variant: "accent" }, primary: { variant: "primary" }, secondary: { variant: "secondary" }, @@ -45,16 +45,59 @@ export const MODIFIER_MAP: Record> = { heading: { heading: true }, open: { open: true }, + + // Layout / display flags + absolute: { position: "absolute" }, + fixed: { position: "fixed" }, + relative: { position: "relative" }, + sticky: { position: "sticky" }, + "inline-block": { display: "inline-block" }, + "inline-flex": { display: "inline-flex" }, + block: { display: "block" }, + flex: { display: "flex" }, + hidden: { display: "none" }, }; -export function parseModifiers(modifiers: Modifier[]): Record { - const props: Record = {}; +const CSS_STYLE_KEYS = new Set([ + "position", + "top", + "right", + "bottom", + "left", + "inset", + "z-index", + "display", + "overflow", + "float", + "margin", + "padding", + "width", + "height", + "transform", + "transition", + "opacity", + "flex", + "order", + "align-self", + "justify-self", +]); + +export function parseModifiers(modifiers: Modifier[]): Record { + const props: Record = {}; modifiers.forEach((mod) => { if (mod.type === "flag") { const mapping = MODIFIER_MAP[mod.value]; if (mapping) { - Object.assign(props, mapping); + for (const [k, v] of Object.entries(mapping)) { + if (CSS_STYLE_KEYS.has(k)) { + const style = (props.__style || {}) as Record; + style[k] = v as string; + props.__style = style; + } else { + props[k] = v; + } + } } else { throw new Error( `Unknown modifier "${mod.value}". ` + @@ -62,12 +105,38 @@ export function parseModifiers(modifiers: Modifier[]): Record { ); } } else if (mod.type === "pair") { - props[mod.key] = mod.value; - } else { - throw new Error( - `Unknown modifier type "${mod.type}". ` + - `Expected "flag" or "pair". Modifier: ${JSON.stringify(mod)}`, - ); + if (CSS_STYLE_KEYS.has(mod.key)) { + const style = (props.__style || {}) as Record; + style[mod.key] = mod.value; + props.__style = style; + } else { + props[mod.key] = mod.value; + } + } else if (mod.type === "event") { + if (!props.__events) props.__events = []; + (props.__events as Modifier[]).push(mod); + } else if (mod.type === "atcode") { + if (mod.name === "bind") { + props.__bind = mod.body; + } else if (mod.name === "style") { + props.__style = props.__style || {}; + try { + const parsed = JSON.parse(mod.body); + if (typeof parsed === "object" && parsed !== null) { + Object.assign(props.__style as Record, parsed); + } + } catch { + // Treat as raw CSS string + props.__rawStyle = mod.body; + } + } else if (mod.name === "if") { + props.__if = mod.body; + } else if (mod.name === "class") { + const existing = (props.class || "") as string; + props.class = existing ? `${existing} ${mod.body}` : mod.body; + } else if (mod.name === "each") { + props.__each = mod.body; + } } }); diff --git a/Build/src/primitives/progress.ts b/Build/src/primitives/progress.ts index 88c7ccc..4a29490 100644 --- a/Build/src/primitives/progress.ts +++ b/Build/src/primitives/progress.ts @@ -1,11 +1,5 @@ import { SazamiComponent, component } from "./base"; -import { - Signal, - Derived, - isSignal, - effect, - type Readable, -} from "@nisoku/sairin"; +import { Derived, isSignal, effect, type Readable } from "@nisoku/sairin"; const STYLES = ` :host { display: block; width: 100%; } @@ -63,6 +57,7 @@ export class SazamiProgress extends SazamiComponent { private _barElement: HTMLElement | null = null; private _rangeMin: number = 0; private _rangeMax: number = 100; + private _value: number | undefined; private _valueBindingCleanup: (() => void) | null = null; private _isReadableNum(value: unknown): value is Readable { @@ -81,13 +76,13 @@ export class SazamiProgress extends SazamiComponent { this._valueBindingCleanup(); this._valueBindingCleanup = null; } - (this as any)._value = valueOrSignal; + this._value = valueOrSignal; this._updateBarWidth(valueOrSignal); } } get value(): number | Readable { - return this._valueSignal || (this as any)._value || 0; + return this._valueSignal || this._value || 0; } private _setupValueBinding() { @@ -146,7 +141,7 @@ export class SazamiProgress extends SazamiComponent { render() { const rawValue = this._valueSignal ? this._valueSignal.get() - : ((this as any)._value ?? Number(this.getAttribute("value") || "50")); + : (this._value ?? Number(this.getAttribute("value") || "50")); const rawMax = Number(this.getAttribute("max") || "100"); const rawMin = Number(this.getAttribute("min") || "0"); const value = Number.isFinite(rawValue) ? rawValue : 50; diff --git a/Build/src/primitives/radio.ts b/Build/src/primitives/radio.ts index 2abf82f..c66ad38 100644 --- a/Build/src/primitives/radio.ts +++ b/Build/src/primitives/radio.ts @@ -69,6 +69,8 @@ export class SazamiRadio extends SazamiComponent { private _checkedSignal: Readable | null = null; private _checkedBindingDispose: (() => void) | null = null; private _disabledSignal: Readable | null = null; + private _checked: boolean | undefined; + private _disabled: boolean | undefined; private _disabledBindingDispose: (() => void) | null = null; private _isReadableBool(value: unknown): value is Readable { @@ -93,11 +95,11 @@ export class SazamiRadio extends SazamiComponent { } get checked(): boolean | Readable { - return this._checkedSignal || (this as any)._checked || false; + return this._checkedSignal || this._checked || false; } private _setChecked(value: boolean) { - (this as any)._checked = value; + this._checked = value; if (value) { this.setAttribute("checked", ""); } else { @@ -123,11 +125,11 @@ export class SazamiRadio extends SazamiComponent { } get disabled(): boolean | Readable { - return this._disabledSignal || (this as any)._disabled || false; + return this._disabledSignal || this._disabled || false; } private _setDisabled(value: boolean) { - (this as any)._disabled = value; + this._disabled = value; if (value) { this.setAttribute("disabled", ""); } else { @@ -137,14 +139,14 @@ export class SazamiRadio extends SazamiComponent { private _getIsDisabled(): boolean { if (this._disabledSignal) return this._disabledSignal.get(); - if ((this as any)._disabled !== undefined) return !!(this as any)._disabled; + if (this._disabled !== undefined) return !!this._disabled; return this.hasAttribute("disabled"); } private _getIsChecked(): boolean { if (this._checkedSignal) return this._checkedSignal.get(); if (this.hasAttribute("checked")) return true; - return !!(this as any)._checked; + return !!this._checked; } render() { @@ -164,7 +166,9 @@ export class SazamiRadio extends SazamiComponent { if (!this._handlersInstalled) { this._handlersInstalled = true; this.addHandler("click", this._handleClick, { internal: true }); - this.addHandler("keydown", this._handleKeydown, { internal: true }); + this.addHandler("keydown", this._handleKeydown as EventListener, { + internal: true, + }); } } @@ -196,11 +200,11 @@ export class SazamiRadio extends SazamiComponent { .querySelectorAll(`saz-radio[name="${escapedName}"]`) .forEach((el) => { if (el === this) return; - const siblingSignal = (el as any)._checkedSignal; + const siblingSignal = (el as unknown as SazamiRadio)._checkedSignal; if (siblingSignal && "set" in siblingSignal) { (siblingSignal as Signal).set(false); } else { - (el as any).checked = false; + (el as unknown as SazamiRadio).checked = false; } }); } diff --git a/Build/src/primitives/select.ts b/Build/src/primitives/select.ts index 10700bf..92bbf76 100644 --- a/Build/src/primitives/select.ts +++ b/Build/src/primitives/select.ts @@ -122,6 +122,8 @@ export class SazamiSelect extends SazamiComponent { private _valueBindingInitialized = false; private _disabledSignal: Readable | null = null; private _disabledEffectDisposer: (() => void) | null = null; + private _value: string = ""; + private _disabled: boolean | undefined; private _handleDocumentClick = (e: Event) => { if (!this.contains(e.target as Node)) { this.open = false; @@ -146,19 +148,19 @@ export class SazamiSelect extends SazamiComponent { this._valueEffectDisposer(); this._valueEffectDisposer = null; } - (this as any)._value = valueOrSignal; + this._value = valueOrSignal; this._updateDisplay(); this._updateSelectedState(); } } get value(): string | Readable { - return this._valueSignal || (this as any)._value || ""; + return this._valueSignal || this._value || ""; } private _getValue(): string { if (this._valueSignal) return this._valueSignal.get(); - return (this as any)._value || this.getAttribute("value") || ""; + return this._value || this.getAttribute("value") || ""; } private _setupValueBinding() { @@ -166,12 +168,11 @@ export class SazamiSelect extends SazamiComponent { this._valueEffectDisposer(); } const sig = this._valueSignal as Readable; - const self = this; const dispose = effect(() => { const val = sig.get(); - (self as any)._value = val; - self._updateDisplay(); - self._updateSelectedState(); + this._value = val; + this._updateDisplay(); + this._updateSelectedState(); }); this._valueEffectDisposer = dispose; this._valueBindingInitialized = true; @@ -203,11 +204,11 @@ export class SazamiSelect extends SazamiComponent { } get disabled(): boolean | Readable { - return this._disabledSignal || (this as any)._disabled || false; + return this._disabledSignal || this._disabled || false; } private _setDisabled(value: boolean) { - (this as any)._disabled = value; + this._disabled = value; if (value) { this.setAttribute("disabled", ""); } else { @@ -218,7 +219,7 @@ export class SazamiSelect extends SazamiComponent { private _getIsDisabled(): boolean { if (this._disabledSignal) return this._disabledSignal.get(); - if ((this as any)._disabled !== undefined) return !!(this as any)._disabled; + if (this._disabled !== undefined) return !!this._disabled; return this.hasAttribute("disabled"); } @@ -251,7 +252,7 @@ export class SazamiSelect extends SazamiComponent { ${ICON_SVGS["chevron-down"] || ""} `, ); @@ -291,7 +292,7 @@ export class SazamiSelect extends SazamiComponent { this._navigateOption(e.key === "ArrowDown" ? 1 : -1); } }; - this.addHandler("keydown", handleKeydown, { + this.addHandler("keydown", handleKeydown as EventListener, { internal: true, element: trigger as HTMLElement, }); @@ -304,17 +305,17 @@ export class SazamiSelect extends SazamiComponent { if ("set" in this._valueSignal) { (this._valueSignal as Signal).set(newValue); } else { - (this as any)._value = newValue; + this._value = newValue; this._updateDisplay(); this._updateSelectedState(); } } else { - (this as any)._value = newValue; + this._value = newValue; this._updateDisplay(); this._updateSelectedState(); } this.open = false; - (this.dispatchEventTyped as any)("change", { value: newValue }); + this.dispatchEventTyped("change", { value: newValue }); } }; this.addHandler("click", handleDropdownClick, { @@ -342,16 +343,16 @@ export class SazamiSelect extends SazamiComponent { if ("set" in this._valueSignal) { (this._valueSignal as Signal).set(newValue); } else { - (this as any)._value = newValue; + this._value = newValue; this._updateDisplay(); this._updateSelectedState(); } } else { - (this as any)._value = newValue; + this._value = newValue; this._updateDisplay(); this._updateSelectedState(); } - (this.dispatchEventTyped as any)("change", { value: newValue }); + this.dispatchEventTyped("change", { value: newValue }); } private _updateSelectedState() { @@ -395,7 +396,7 @@ export class SazamiSelect extends SazamiComponent { if (oldVal === newVal) return; if (name === "open") { const trigger = this.$(".trigger"); - const dropdown = this.$(".dropdown"); + const _dropdown = this.$(".dropdown"); if (trigger) { trigger.setAttribute( "aria-expanded", diff --git a/Build/src/primitives/slider.ts b/Build/src/primitives/slider.ts index 4f43522..bd3c40d 100644 --- a/Build/src/primitives/slider.ts +++ b/Build/src/primitives/slider.ts @@ -113,6 +113,8 @@ export class SazamiSlider extends SazamiComponent { private _filledElement: HTMLElement | null = null; private _rangeMin: number = 0; private _rangeMax: number = 100; + private _value: number = 50; + private _disabled: boolean | undefined; private _isReadableNum(value: unknown): value is Readable { return isSignal(value) || value instanceof Derived; @@ -128,13 +130,13 @@ export class SazamiSlider extends SazamiComponent { this._setupValueBinding(); } else { this._valueSignal = null; - (this as any)._value = valueOrSignal; + this._value = valueOrSignal; this._updateSliderValue(valueOrSignal); } } get value(): number | Readable { - return this._valueSignal || (this as any)._value || 50; + return this._valueSignal || this._value || 50; } set disabled(value: boolean | Readable) { @@ -148,11 +150,11 @@ export class SazamiSlider extends SazamiComponent { } get disabled(): boolean | Readable { - return this._disabledSignal || (this as any)._disabled || false; + return this._disabledSignal || this._disabled || false; } private _setDisabled(value: boolean) { - (this as any)._disabled = value; + this._disabled = value; if (value) { this.setAttribute("disabled", ""); } else { @@ -162,7 +164,7 @@ export class SazamiSlider extends SazamiComponent { private _getIsDisabled(): boolean { if (this._disabledSignal) return this._disabledSignal.get(); - if ((this as any)._disabled !== undefined) return !!(this as any)._disabled; + if (this._disabled !== undefined) return !!this._disabled; return this.hasAttribute("disabled"); } @@ -216,7 +218,7 @@ export class SazamiSlider extends SazamiComponent { const currentValue = this._valueSignal ? this._valueSignal.get() - : (this as any)._value || 50; + : this._value || 50; let value = Number(currentValue); if (!Number.isFinite(value)) value = 50; if (value < min) value = min; @@ -269,9 +271,9 @@ export class SazamiSlider extends SazamiComponent { if (this._valueSignal && "set" in this._valueSignal) { (this._valueSignal as Signal).set(val); } else { - (this as any)._value = val; + this._value = val; } - (this.dispatchEventTyped as any)("input", { value: val }); + this.dispatchEventTyped("input", { value: val }); }, { internal: true, element: this._sliderElement }, ); @@ -311,16 +313,16 @@ export class SazamiSlider extends SazamiComponent { if (name === "step" && parsed <= 0) { parsed = 1; } - (this as any)[name] = parsed; + (this as unknown as Record)[name] = parsed; if (name === "value" || name === "min" || name === "max") { const min = this.min; const max = this.max; - const currentVal = ((this as any)._value as number) || 50; - if (currentVal < min) (this as any).value = min; - if (currentVal > max) (this as any).value = max; + const currentVal = this._value || 50; + if (currentVal < min) this.value = min; + if (currentVal > max) this.value = max; } } else if (name === "size") { - (this as any)[name] = newVal ?? ""; + (this as unknown as Record)[name] = newVal ?? ""; } super.attributeChangedCallback(name, oldVal, newVal); } diff --git a/Build/src/primitives/spinner.ts b/Build/src/primitives/spinner.ts index 456da86..c5d2544 100644 --- a/Build/src/primitives/spinner.ts +++ b/Build/src/primitives/spinner.ts @@ -1,6 +1,6 @@ import { SazamiComponent, component } from "./base"; import { ICON_SVGS } from "../icons/index"; -import { Signal, Derived, isSignal, type Readable } from "@nisoku/sairin"; +import { Derived, isSignal, type Readable } from "@nisoku/sairin"; import { bindText } from "@nisoku/sairin"; const STYLES = ` @@ -62,6 +62,7 @@ export class SazamiSpinner extends SazamiComponent { private _labelSignal: Readable | null = null; private _visibleSignal: Readable | null = null; private _labelElement: HTMLElement | null = null; + private _label: string = ""; private _isReadableStr(value: unknown): value is Readable { return isSignal(value) || value instanceof Derived; @@ -76,14 +77,14 @@ export class SazamiSpinner extends SazamiComponent { this._labelSignal = value; } else { this._labelSignal = null; - (this as any)._label = value; + this._label = value; this._updateLabel(value); } } get label(): string | Readable { if (this._labelSignal) return this._labelSignal; - return (this as any)._label || ""; + return this._label || ""; } set visible(value: boolean | Readable) { @@ -120,7 +121,7 @@ export class SazamiSpinner extends SazamiComponent { render() { const labelText = this._labelSignal ? this._labelSignal.get() - : ((this as any)._label ?? "Loading..."); + : (this._label ?? "Loading..."); if (!this.hasAttribute("role")) { this.setAttribute("role", "status"); diff --git a/Build/src/primitives/switch.ts b/Build/src/primitives/switch.ts index a70090d..acdb319 100644 --- a/Build/src/primitives/switch.ts +++ b/Build/src/primitives/switch.ts @@ -81,6 +81,8 @@ export class SazamiSwitch extends SazamiComponent { private _checkedBindingDispose: (() => void) | null = null; private _disabledSignal: Readable | null = null; private _disabledBindingDispose: (() => void) | null = null; + private _checked: boolean = false; + private _disabled: boolean | undefined; private _isReadableBool(value: unknown): value is Readable { return isSignal(value) || value instanceof Derived; @@ -104,11 +106,11 @@ export class SazamiSwitch extends SazamiComponent { } get checked(): boolean | Readable { - return this._checkedSignal || (this as any)._checked || false; + return this._checkedSignal || this._checked || false; } private _setChecked(value: boolean) { - (this as any)._checked = value; + this._checked = value; if (value) { this.setAttribute("checked", ""); } else { @@ -135,11 +137,11 @@ export class SazamiSwitch extends SazamiComponent { } get disabled(): boolean | Readable { - return this._disabledSignal || (this as any)._disabled || false; + return this._disabledSignal || this._disabled || false; } private _setDisabled(value: boolean) { - (this as any)._disabled = value; + this._disabled = value; if (value) { this.setAttribute("disabled", ""); } else { @@ -150,14 +152,14 @@ export class SazamiSwitch extends SazamiComponent { private _getIsDisabled(): boolean { if (this._disabledSignal) return this._disabledSignal.get(); - if ((this as any)._disabled !== undefined) return !!(this as any)._disabled; + if (this._disabled !== undefined) return !!this._disabled; return this.hasAttribute("disabled"); } private _getIsChecked(): boolean { if (this._checkedSignal) return this._checkedSignal.get(); if (this.hasAttribute("checked")) return true; - return !!(this as any)._checked; + return !!this._checked; } render() { @@ -175,7 +177,9 @@ export class SazamiSwitch extends SazamiComponent { this._updateAria(); this.addHandler("click", this._handleClick, { internal: true }); - this.addHandler("keydown", this._handleKeydown, { internal: true }); + this.addHandler("keydown", this._handleKeydown as EventListener, { + internal: true, + }); } private _updateAria() { diff --git a/Build/src/primitives/text.ts b/Build/src/primitives/text.ts index 72caa04..2bfcfa2 100644 --- a/Build/src/primitives/text.ts +++ b/Build/src/primitives/text.ts @@ -1,6 +1,6 @@ import { SazamiComponent, component } from "./base"; import { SIZE_RULES, TYPO_WEIGHT, TYPO_TONE } from "./shared"; -import { Signal, Derived, isSignal, type Readable } from "@nisoku/sairin"; +import { Derived, isSignal, type Readable } from "@nisoku/sairin"; import { bindText } from "@nisoku/sairin"; const STYLES = ` diff --git a/Build/src/primitives/toast.ts b/Build/src/primitives/toast.ts index ef62e36..ca8069e 100644 --- a/Build/src/primitives/toast.ts +++ b/Build/src/primitives/toast.ts @@ -165,8 +165,10 @@ export class SazamiToast extends SazamiComponent { } // Keyboard support: Escape to dismiss - this.removeHandler("keydown", this._handleKeydown); - this.addHandler("keydown", this._handleKeydown, { internal: true }); + this.removeHandler("keydown", this._handleKeydown as EventListener); + this.addHandler("keydown", this._handleKeydown as EventListener, { + internal: true, + }); if (!this.hasAttribute("visible")) { this.setAttribute("visible", ""); diff --git a/Build/src/primitives/toggle.ts b/Build/src/primitives/toggle.ts index a2888df..09d94c3 100644 --- a/Build/src/primitives/toggle.ts +++ b/Build/src/primitives/toggle.ts @@ -61,6 +61,8 @@ export class SazamiToggle extends SazamiComponent { private _checkedBindingDispose: (() => void) | null = null; private _disabledSignal: Readable | null = null; private _disabledBindingDispose: (() => void) | null = null; + private _checked: boolean = false; + private _disabled: boolean | undefined; private _isReadableBool(value: unknown): value is Readable { return isSignal(value) || value instanceof Derived; @@ -84,11 +86,11 @@ export class SazamiToggle extends SazamiComponent { } get checked(): boolean | Readable { - return this._checkedSignal || (this as any)._checked || false; + return this._checkedSignal || this._checked || false; } private _setChecked(value: boolean) { - (this as any)._checked = value; + this._checked = value; if (value) { this.setAttribute("checked", ""); } else { @@ -115,11 +117,11 @@ export class SazamiToggle extends SazamiComponent { } get disabled(): boolean | Readable { - return this._disabledSignal || (this as any)._disabled || false; + return this._disabledSignal || this._disabled || false; } private _setDisabled(value: boolean) { - (this as any)._disabled = value; + this._disabled = value; if (value) { this.setAttribute("disabled", ""); } else { @@ -130,7 +132,7 @@ export class SazamiToggle extends SazamiComponent { private _getIsDisabled(): boolean { if (this._disabledSignal) return this._disabledSignal.get(); - if ((this as any)._disabled !== undefined) return !!(this as any)._disabled; + if (this._disabled !== undefined) return !!this._disabled; return this.hasAttribute("disabled"); } @@ -147,14 +149,16 @@ export class SazamiToggle extends SazamiComponent { this._updateAria(); this.addHandler("click", this._handleClick, { internal: true }); - this.addHandler("keydown", this._handleKeydown, { internal: true }); + this.addHandler("keydown", this._handleKeydown as EventListener, { + internal: true, + }); } private _handleClick = () => { if (this._getIsDisabled()) return; const newValue = this._checkedSignal ? !this._checkedSignal.get() - : !((this as any)._checked || false); + : !(this._checked || false); if (this._checkedSignal) { if ("set" in this._checkedSignal) { (this._checkedSignal as Signal).set(newValue); @@ -177,7 +181,7 @@ export class SazamiToggle extends SazamiComponent { private _updateAria() { const isChecked = this._checkedSignal ? this._checkedSignal.get() - : !!(this as any)._checked; + : !!this._checked; const isDisabled = this._getIsDisabled(); this.setAttribute("aria-checked", isChecked ? "true" : "false"); if (isDisabled) { diff --git a/Build/src/runtime/reactive-context.ts b/Build/src/runtime/reactive-context.ts new file mode 100644 index 0000000..8e209de --- /dev/null +++ b/Build/src/runtime/reactive-context.ts @@ -0,0 +1,306 @@ +import { + signal, + derived, + effect, + path, + bindInputValue, + type Readable, + type Signal, +} from "@nisoku/sairin"; +import type { InterpolatedTextPart } from "@nisoku/sakko"; + +function escapeRegExp(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function getReferencedVars(code: string, knownNames: string[]): string[] { + return knownNames.filter((name) => { + if (!/^[A-Za-z_]\w*$/.test(name)) return false; + const re = new RegExp(`\\b${escapeRegExp(name)}\\b`); + return re.test(code); + }); +} + +function addGetCalls(code: string, varNames: string[]): string { + const strings: string[] = []; + let processed = code.replace(/'[^']*'|"[^"]*"/g, (m) => { + strings.push(m); + return `__STR${strings.length - 1}__`; + }); + + for (const name of varNames) { + if (!/^[A-Za-z_]\w*$/.test(name)) continue; + const escaped = escapeRegExp(name); + const regex = new RegExp( + `(? strings[+i]); +} + +function transformHandlerBody(code: string, varNames: string[]): string { + let result = code; + for (const name of varNames) { + if (!/^[A-Za-z_]\w*$/.test(name)) continue; + const escaped = escapeRegExp(name); + + result = result.replace( + new RegExp(`(? + `${name}.set(${name}.get() + ${addGetCalls(expr.trim(), varNames)})`, + ); + + result = result.replace( + new RegExp( + `(? + `${name}.set(${name}.get() - ${addGetCalls(expr.trim(), varNames)})`, + ); + + result = result.replace( + new RegExp( + `(? { + if (expr.includes(".set(")) return _; + return `${name}.set(${addGetCalls(expr.trim(), varNames)})`; + }, + ); + } + + return addGetCalls(result, varNames); +} + +function evaluateStatic(expr: string): unknown { + const trimmed = expr.trim(); + if (trimmed === "true") return true; + if (trimmed === "false") return false; + if (trimmed === "null") return null; + if (trimmed === "undefined") return undefined; + if ( + (trimmed.startsWith('"') && trimmed.endsWith('"')) || + (trimmed.startsWith("'") && trimmed.endsWith("'")) + ) { + try { + return JSON.parse(trimmed); + } catch { + return trimmed.slice(1, -1); + } + } + if (!isNaN(Number(trimmed)) && trimmed !== "") return Number(trimmed); + if (trimmed.startsWith("[") || trimmed.startsWith("{")) { + try { + return new Function(`return ${trimmed}`)(); + } catch { + return trimmed; + } + } + return trimmed; +} + +type SignalLike = ReturnType | ReturnType; + +export class ReactiveContext { + private signals = new Map>(); + private deriveds = new Map>(); + readonly rootName: string; + private interpCounter = 0; + + constructor(rootName: string) { + this.rootName = rootName; + } + + addState(name: string, initialValue: string): void { + const sig = signal( + path("component", this.rootName, name), + evaluateStatic(initialValue), + ); + this.signals.set(name, sig); + } + + addDerived(name: string, expr: string): void { + const allNames = this.getAllSignalNames(); + const refd = getReferencedVars(expr, allNames); + + if (refd.length === 0) { + const sig = derived(path("component", this.rootName, name), () => + evaluateStatic(expr), + ); + this.deriveds.set(name, sig); + return; + } + + const transformed = addGetCalls(expr, refd); + const fn = new Function(...refd, `return ${transformed}`); + const sig = derived(path("component", this.rootName, name), () => { + const signals = refd.map((n) => this.getSignal(n)!); + return fn(...signals); + }); + this.deriveds.set(name, sig); + } + + addEffect(body: string): void { + const allNames = this.getAllSignalNames(); + const refd = getReferencedVars(body, allNames); + + if (refd.length === 0) { + effect(() => { + new Function(body)(); + }); + return; + } + + const transformed = addGetCalls(body, refd); + const fn = new Function(...refd, transformed); + effect(() => { + const signals = refd.map((n) => this.getSignal(n)!); + fn(...signals); + }); + } + + getAllSignalNames(): string[] { + return [...this.signals.keys(), ...this.deriveds.keys()]; + } + + getSignal(name: string): SignalLike | undefined { + return this.signals.get(name) ?? this.deriveds.get(name); + } + + getSignals(): Map { + const all = new Map(); + for (const [k, v] of this.signals) all.set(k, v); + for (const [k, v] of this.deriveds) all.set(k, v); + return all; + } + + /** + * Register a read-only signal for `name` in this context. + * Used by @each to expose the iteration variable to template children. + */ + setSignalValue(name: string, initial: unknown): void { + if (this.signals.has(name) || this.deriveds.has(name)) return; + const sig = signal( + path("component", this.rootName, `__each_${name}`), + initial, + ); + this.signals.set(name, sig); + } + + /** Create a child context with all parent signals plus a new signal. */ + forkWithSignal(name: string, initial: unknown): ReactiveContext { + const child = new ReactiveContext(this.rootName); + child.interpCounter = this.interpCounter; + for (const [k, v] of this.signals) child.signals.set(k, v); + for (const [k, v] of this.deriveds) child.deriveds.set(k, v); + child.setSignalValue(name, initial); + return child; + } + + createInterpolated(parts: InterpolatedTextPart[]): Readable { + const allNames = this.getAllSignalNames(); + const allRefd = new Set(); + const id = this.interpCounter++; + + for (const part of parts) { + if (part.type === "expr") { + for (const name of getReferencedVars(part.value, allNames)) { + allRefd.add(name); + } + } + } + + const refd = [...allRefd]; + const exprFns = parts.map((p) => { + if (p.type === "text") return null; + const partRefd = getReferencedVars(p.value, allNames); + if (partRefd.length === 0) { + return () => String(evaluateStatic(p.value)); + } + const transformed = addGetCalls(p.value, partRefd); + const fn = new Function(...partRefd, `return String(${transformed})`); + return (signalMap: Record) => + fn(...partRefd.map((n) => signalMap[n])); + }); + + const interpPath = path("component", this.rootName, `__interp_${id}`); + + if (refd.length === 0) { + const staticVal = parts + .map((p) => + p.type === "text" ? p.value : String(evaluateStatic(p.value)), + ) + .join(""); + return derived(interpPath, () => staticVal); + } + + const d = derived(interpPath, () => { + const signalMap: Record = {}; + for (const name of refd) { + const sig = this.getSignal(name)!; + signalMap[name] = sig; + sig.get(); + } + return parts + .map((p, i) => { + const fn = exprFns[i]; + if (!fn) return (p as { value: string }).value; + return fn(signalMap); + }) + .join(""); + }); + + return d; + } + + createEventHandler(handlerBody: string): (e: Event) => void { + const allNames = this.getAllSignalNames(); + const refd = getReferencedVars(handlerBody, allNames); + const transformed = transformHandlerBody(handlerBody, refd); + + const fn = new Function(...refd, "e", transformed); + return (e: Event) => { + const signals = refd.map((n) => this.getSignal(n)!); + fn(...signals, e); + }; + } + + createBindHandler( + signalName: string, + elementType: string, + ): ((el: HTMLElement) => void) | null { + const sig = this.getSignal(signalName); + if (!sig) return null; + const tag = (el: HTMLElement) => el.tagName.toLowerCase(); + return (el: HTMLElement) => { + const t = tag(el); + if (t === "saz-checkbox" || t === "saz-switch" || t === "saz-toggle") { + (el as unknown as Record).checked = sig; + } else if ( + t === "saz-input" || t === "saz-select" || t === "saz-textarea" + ) { + (el as unknown as Record).value = sig; + } else { + bindInputValue(el as HTMLInputElement, sig as Signal); + } + }; + } +} diff --git a/Build/src/runtime/renderer.ts b/Build/src/runtime/renderer.ts index 075b72f..15d28f9 100644 --- a/Build/src/runtime/renderer.ts +++ b/Build/src/runtime/renderer.ts @@ -1,15 +1,84 @@ import { VNode } from "./transformer"; +import { bindText, type Readable } from "@nisoku/sairin"; -export function render(vnode: VNode | string, parent: HTMLElement): void { +const CSS_LENGTH_PROPS = new Set([ + "top", + "right", + "bottom", + "left", + "inset", + "margin", + "margin-top", + "margin-right", + "margin-bottom", + "margin-left", + "padding", + "padding-top", + "padding-right", + "padding-bottom", + "padding-left", + "width", + "height", + "min-width", + "min-height", + "max-width", + "max-height", + "font-size", + "gap", + "row-gap", + "column-gap", + "border-radius", + "border-width", + "border", + "transform-origin", + "perspective", +]); + +const NUMERIC_RE = /^-?\d+(\.\d+)?$/; + +function styleValue(key: string, val: string): string { + if (CSS_LENGTH_PROPS.has(key) && NUMERIC_RE.test(val)) { + return val + "px"; + } + return val; +} + +export function render( + vnode: VNode | string | Readable, + parent: HTMLElement, +): void { if (typeof vnode === "string") { parent.appendChild(document.createTextNode(vnode)); return; } + if (isReadable(vnode)) { + const textNode = document.createTextNode(""); + bindText(textNode, vnode); + parent.appendChild(textNode); + return; + } + const element = document.createElement(vnode.type); + // Apply inline styles before other props + if (vnode.props.__rawStyle) { + element.style.cssText = vnode.props.__rawStyle as string; + } + if (vnode.props.__style) { + for (const [key, val] of Object.entries(vnode.props.__style)) { + (element.style as unknown as Record)[key] = styleValue( + key, + String(val), + ); + } + } + Object.entries(vnode.props).forEach(([key, value]) => { - if (typeof value === "boolean" && value) { + if (key.startsWith("__")) return; + if (isReadable(value)) { + (element as unknown as Record)[key] = value; + } else if (typeof value === "boolean" && value) { element.setAttribute(key, ""); } else if (value !== undefined && value !== null && value !== false) { element.setAttribute(key, String(value)); @@ -18,11 +87,25 @@ export function render(vnode: VNode | string, parent: HTMLElement): void { vnode.children.forEach((child) => { if (Array.isArray(child)) { - (child as (VNode | string)[]).forEach((item) => render(item, element)); + (child as (VNode | string | Readable)[]).forEach((item) => + render(item, element), + ); } else { render(child, element); } }); parent.appendChild(element); + + if (vnode.afterRender) { + vnode.afterRender(element); + } +} + +function isReadable(v: unknown): v is Readable { + if (v === null || typeof v !== "object") return false; + const maybe = v as { get?: unknown; subscribe?: unknown }; + return ( + typeof maybe.get === "function" && typeof maybe.subscribe === "function" + ); } diff --git a/Build/src/runtime/transformer.ts b/Build/src/runtime/transformer.ts index 3d00873..847140d 100644 --- a/Build/src/runtime/transformer.ts +++ b/Build/src/runtime/transformer.ts @@ -1,11 +1,13 @@ -import type { ASTNode } from "@nisoku/sakko"; +import type { ASTNode, InterpolatedText } from "@nisoku/sakko"; import { parseModifiers } from "../primitives/modifier-map"; -import { unknownComponentError } from "../errors"; +import type { Readable } from "@nisoku/sairin"; +import { ReactiveContext } from "./reactive-context"; export type VNode = { type: string; - props: Record; - children: (VNode | string)[]; + props: Record; + children: (VNode | string | Readable)[]; + afterRender?: (el: HTMLElement) => void; }; const SAZAMI_REGISTRY: Record = { @@ -54,7 +56,6 @@ const SAZAMI_REGISTRY: Record = { export function getTag(name: string): string { const entry = SAZAMI_REGISTRY[name]; if (!entry) { - unknownComponentError(name); return `saz-${name}`; } return entry.tag; @@ -69,47 +70,227 @@ const CONTENT_SLOT_COMPONENTS = new Set([ "saz-label", ]); -function serializeValue( - value: - | string - | { - type: "interpolated"; - parts: Array<{ type: "text" | "expr"; value: string }>; - }, -): string { +function serializeValue(value: string | InterpolatedText): string { if (typeof value === "string") return value; return value.parts.map((p) => p.value).join(""); } -export function transformAST(node: ASTNode): VNode | VNode[] { +function hasReactiveExpr( + value: string | InterpolatedText, + context: ReactiveContext | undefined, +): boolean { + if (!context || typeof value === "string") return false; + const signalNames = context.getAllSignalNames(); + if (signalNames.length === 0) return false; + return value.parts.some( + (p) => + p.type === "expr" && + signalNames.some((name) => { + const re = new RegExp( + `\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, + ); + return re.test(p.value); + }), + ); +} + +function isReadable(v: unknown): v is Readable { + if (v === null || typeof v !== "object") return false; + const maybe = v as { get?: unknown; subscribe?: unknown }; + return ( + typeof maybe.get === "function" && typeof maybe.subscribe === "function" + ); +} + +function renderVNode( + vnode: VNode | string | Readable, + parent: HTMLElement, +): void { + if (typeof vnode === "string") { + parent.appendChild(document.createTextNode(vnode)); + return; + } + if (isReadable(vnode)) { + const textNode = document.createTextNode(""); + parent.appendChild(textNode); + const update = () => { textNode.textContent = vnode.get(); }; + update(); + vnode.subscribe(update); + return; + } + const el = document.createElement(vnode.type); + if (vnode.props.__rawStyle) { + el.style.cssText = vnode.props.__rawStyle as string; + } + if (vnode.props.__style) { + for (const [key, val] of Object.entries(vnode.props.__style)) { + (el.style as unknown as Record)[key] = String(val); + } + } + for (const [key, value] of Object.entries(vnode.props)) { + if (key.startsWith("__")) continue; + if (typeof value === "boolean" && value) { + el.setAttribute(key, ""); + } else if (value !== undefined && value !== null && value !== false) { + el.setAttribute(key, String(value)); + } + } + for (const child of vnode.children) { + if (Array.isArray(child)) { + (child as (VNode | string | Readable)[]).forEach((item) => + renderVNode(item, el), + ); + } else { + renderVNode(child, el); + } + } + parent.appendChild(el); + if (vnode.afterRender) { + vnode.afterRender(el); + } +} + +export function transformAST( + node: ASTNode, + context?: ReactiveContext, +): VNode | VNode[] { if (node.type === "inline") { const tag = getTag(node.name); const props = parseModifiers(node.modifiers); - const value = - typeof node.value === "string" ? node.value : serializeValue(node.value); - if (ICON_COMPONENTS.has(tag) && node.value && !props.icon) { - props.icon = - typeof node.value === "string" - ? node.value - : serializeValue(node.value); + const afterRenderFns: Array<(el: HTMLElement) => void> = []; + + const events = props.__events as + Array<{ event: string; handler: string }> | undefined; + delete props.__events; + + const bindSignal = props.__bind as string | undefined; + delete props.__bind; + + const ifSignal = props.__if as string | undefined; + delete props.__if; + + let value: string | Readable | undefined; + + if (node.value) { + if (typeof node.value === "string") { + value = node.value; + } else if (hasReactiveExpr(node.value, context)) { + value = context!.createInterpolated(node.value.parts); + } else { + value = serializeValue(node.value); + } + } + + if (events && context) { + for (const evt of events) { + const handler = context.createEventHandler(evt.handler); + afterRenderFns.push((el: HTMLElement) => { + el.addEventListener(evt.event, handler); + }); + } } - if (CONTENT_SLOT_COMPONENTS.has(tag) && node.value && !props.content) { - props.content = - typeof node.value === "string" - ? node.value - : serializeValue(node.value); + + if (bindSignal && context) { + const bindFn = context.createBindHandler(bindSignal, node.name); + if (bindFn) { + afterRenderFns.push(bindFn); + } } - return { + + if (ifSignal && context) { + const sig = context.getSignal(ifSignal); + if (sig) { + afterRenderFns.push((el: HTMLElement) => { + const update = () => { + el.style.display = sig.get() ? "" : "none"; + }; + update(); + (el as unknown as Record).__sazamiIfDisposer = + sig.subscribe(update); + }); + } + } + + if (ICON_COMPONENTS.has(tag) && value && !props.icon) { + props.icon = value; + } + if (CONTENT_SLOT_COMPONENTS.has(tag) && value && !props.content) { + props.content = value; + } + + const vnode: VNode = { type: tag, props, - children: CONTENT_SLOT_COMPONENTS.has(tag) ? [] : value ? [value] : [], + children: CONTENT_SLOT_COMPONENTS.has(tag) + ? [] + : value !== undefined && value !== "" + ? [value] + : [], }; + + if (afterRenderFns.length > 0) { + vnode.afterRender = (el: HTMLElement) => { + afterRenderFns.forEach((fn) => fn(el)); + }; + } + + return vnode; } if (node.type === "element") { - const children: (VNode | string)[] = []; + const props = parseModifiers(node.modifiers); + const eachStr = props.__each as string | undefined; + delete props.__each; + + if (eachStr) { + const match = eachStr.match(/^(\w+)\s+in\s+(\w+)$/); + if (!match) throw new Error(`Invalid @each syntax: "${eachStr}"`); + const [, itemVar, sourceName] = match; + + const tag = getTag(node.name); + const rawTemplate = node.children; + + return { + type: tag, + props, + children: [], + afterRender: (el) => { + const sourceSig = context?.getSignal(sourceName); + if (!sourceSig) return; + + let disposers: Array<() => void> = []; + + const renderList = () => { + disposers.forEach((d) => d()); + disposers = []; + el.innerHTML = ""; + + const items = sourceSig.get(); + if (!Array.isArray(items)) return; + + for (const item of items) { + const subCtx = context!.forkWithSignal(itemVar, item); + for (const child of rawTemplate) { + const result = transformAST(child, subCtx); + if (Array.isArray(result)) { + result.forEach((v) => renderVNode(v, el)); + } else { + renderVNode(result, el); + } + } + } + }; + + renderList(); + const unsub = sourceSig.subscribe(renderList); + disposers.push(unsub); + }, + }; + } + + const children: (VNode | string | Readable)[] = []; for (const child of node.children) { - const result = transformAST(child); + const result = transformAST(child, context); if (Array.isArray(result)) { children.push(...result); } else { @@ -126,7 +307,7 @@ export function transformAST(node: ASTNode): VNode | VNode[] { if (node.type === "list") { const items: VNode[] = []; for (const item of node.items) { - const result = transformAST(item); + const result = transformAST(item, context); if (Array.isArray(result)) { items.push(...result); } else { @@ -136,5 +317,5 @@ export function transformAST(node: ASTNode): VNode | VNode[] { return items; } - throw new Error(`Unknown node type: ${(node as any).type}`); + throw new Error(`Unknown node type: ${(node as { type: string }).type}`); } diff --git a/Build/tests/app-patterns.test.ts b/Build/tests/app-patterns.test.ts new file mode 100644 index 0000000..0003e37 --- /dev/null +++ b/Build/tests/app-patterns.test.ts @@ -0,0 +1,404 @@ +import { describe, test, expect, beforeAll, beforeEach } from "@jest/globals"; +import { compileSakko, injectThemeCSS } from "../src/index"; +import { __resetRegistryForTesting } from "@nisoku/sairin"; + +function defineOnce(name: string, cls: any) { + if (!customElements.get(name)) { + customElements.define(name, cls); + } +} + +beforeEach(() => { + document.head.innerHTML = ""; + document.body.innerHTML = ""; + __resetRegistryForTesting(); +}); + +beforeAll(() => { + const names: [string, string][] = [ + ["text","Text"],["badge","Badge"],["heading","Heading"],["label","Label"], + ["button","Button"],["input","Input"],["checkbox","Checkbox"], + ["card","Card"],["row","Row"],["column","Column"],["grid","Grid"], + ["stack","Stack"],["section","Section"],["icon","Icon"], + ["divider","Divider"],["spacer","Spacer"],["spinner","Spinner"], + ["switch","Switch"],["toggle","Toggle"],["select","Select"], + ["slider","Slider"],["radio","Radio"],["image","Image"], + ["avatar","Avatar"],["coverart","Coverart"],["progress","Progress"], + ["tag","Tag"],["chip","Chip"],["modal","Modal"],["toast","Toast"], + ["tabs","Tabs"], + ]; + for (const [k, cls] of names) { + defineOnce(`saz-${k}`, require(`../src/primitives/${k}`)[`Sazami${cls}`]); + } + defineOnce("saz-icon-button", require("../src/primitives/icon-button").SazamiIconButton); +}); + +describe("Real-world app patterns", () => { + test("Counter: increment/decrement/reset", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const text = container.querySelector("saz-text"); + expect(text!.shadowRoot!.textContent).toContain("Count: 0"); + + const buttons = container.querySelectorAll("saz-button"); + expect(buttons.length).toBe(3); + + // Increment + (buttons[0] as HTMLElement).click(); + await Promise.resolve(); + expect(text!.shadowRoot!.textContent).toContain("Count: 1"); + + // Increment again + (buttons[0] as HTMLElement).click(); + await Promise.resolve(); + expect(text!.shadowRoot!.textContent).toContain("Count: 2"); + + // Decrement + (buttons[1] as HTMLElement).click(); + await Promise.resolve(); + expect(text!.shadowRoot!.textContent).toContain("Count: 1"); + + // Reset + (buttons[2] as HTMLElement).click(); + await Promise.resolve(); + expect(text!.shadowRoot!.textContent).toContain("Count: 0"); + }); + + test("Toggle visibility with @if", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const text = container.querySelector("saz-text"); + expect(text).toBeTruthy(); + expect((text as HTMLElement).style.display).not.toBe("none"); + + const btn = container.querySelector("saz-button")!; + (btn as HTMLElement).click(); + await Promise.resolve(); + expect((text as HTMLElement).style.display).toBe("none"); + + (btn as HTMLElement).click(); + await Promise.resolve(); + expect((text as HTMLElement).style.display).not.toBe("none"); + }); + + test("@bind two-way on input", async () => { + const source = `
`; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const input = container.querySelector("saz-input")!; + const text = container.querySelector("saz-text")!; + expect(text!.shadowRoot!.textContent).toContain("Hello"); + + // Simulate input + const innerInput = (input as any).shadowRoot?.querySelector("input"); + if (innerInput) { + innerInput.value = "World"; + innerInput.dispatchEvent(new Event("input", { bubbles: true })); + await Promise.resolve(); + expect(text!.shadowRoot!.textContent).toContain("Hello World"); + } + }); + + test("@derived computed from multiple signals", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const texts = container.querySelectorAll("saz-text"); + expect(texts[0]!.shadowRoot!.textContent).toContain("5 + 3 = 8"); + expect(texts[1]!.shadowRoot!.textContent).toContain("5 * 3 = 15"); + }); + + test("@effect reacts to state changes", async () => { + const logSpy = jest.spyOn(console, "log").mockImplementation(() => {}); + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + expect(logSpy).toHaveBeenCalledWith("count changed to", 0); + logSpy.mockClear(); + + (container.querySelector("saz-button")! as HTMLElement).click(); + await Promise.resolve(); + expect(logSpy).toHaveBeenCalledWith("count changed to", 1); + + logSpy.mockRestore(); + }); + + test("Complex interpolation: expressions in strings", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const text = container.querySelector("saz-text")!; + expect(text!.shadowRoot!.textContent).toContain("Total: 30 dollars"); + }); + + test("Nested elements with modifiers", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const card = container.querySelector("saz-card"); + expect(card).toBeTruthy(); + expect(card!.getAttribute("variant")).toBe("accent"); + + const heading = container.querySelector("saz-heading"); + expect(heading!.shadowRoot!.textContent).toContain("Title"); + + const buttons = container.querySelectorAll("saz-button"); + expect(buttons.length).toBe(2); + expect(buttons[0].getAttribute("variant")).toBe("primary"); + }); + + test("Interpolation in @on:click handler body", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const button = container.querySelector("saz-button")!; + expect(button.textContent).toContain("Add 5"); + + (button as HTMLElement).click(); + await Promise.resolve(); + expect(button.textContent).toContain("Add 5"); // text shouldn't change, but count is now 5 + }); + + test("@class modifier on element", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + // @class is parsed as modifier but components use Shadow DOM so class on host may not apply internally + const text = container.querySelector("saz-text")!; + expect(text).toBeTruthy(); + }); + + test("Boolean state toggle", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + // Just verify it compiles and renders without error + expect(container.querySelector("saz-text")).toBeTruthy(); + expect(container.querySelector("saz-button")).toBeTruthy(); + }); + + test("Empty @state block", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + expect(() => compileSakko(source, container)).not.toThrow(); + await Promise.resolve(); + const text = container.querySelector("saz-text")!; + expect(text!.shadowRoot!.textContent).toContain("No state"); + }); + + test("Re-render replaces previous content", async () => { + const source = ``; + + const container = document.createElement("div"); + container.innerHTML = "
old content
"; + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + expect(container.textContent).not.toContain("old content"); + const text = container.querySelector("saz-text"); + expect(text).toBeTruthy(); + expect(text!.shadowRoot!.textContent).toContain("Version 2"); + }); + + test("Multiple @state blocks", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const text = container.querySelector("saz-text")!; + expect(text!.shadowRoot!.textContent).toContain("1 2 3"); + }); + + test("Deeply nested reactive: derived depends on derived", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const text = container.querySelector("saz-text")!; + expect(text!.shadowRoot!.textContent).toContain("2 squared = 4, cubed = 8"); + }); + + test("@class modifier adds CSS class", () => { + const source = `app { span(@class:highlight): "hello" }`; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + const span = container.querySelector("span"); + expect(span).toBeTruthy(); + expect(span!.getAttribute("class")).toBe("highlight"); + }); + + test("Unknown root name renders as div without warning", () => { + const source = `counter { text: "Hello" }`; + + const container = document.createElement("div"); + document.body.appendChild(container); + + // Should not throw despite "counter" not being registered + expect(() => compileSakko(source, container)).not.toThrow(); + const text = container.querySelector("saz-text"); + expect(text).toBeTruthy(); + expect(text!.shadowRoot!.textContent).toContain("Hello"); + }); + + test("@each iterates over signal array", async () => { + const source = `app { + @state { items = ["a", "b", "c"] } + div(@each item in items) { + text: "{item}" + } + }`; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const texts = container.querySelectorAll("saz-text"); + expect(texts.length).toBe(3); + expect(texts[0]!.shadowRoot!.textContent).toContain("a"); + expect(texts[1]!.shadowRoot!.textContent).toContain("b"); + expect(texts[2]!.shadowRoot!.textContent).toContain("c"); + }); + + test("@each updates when array signal changes", async () => { + const source = `app { + @state { items = ["x"] } + div(@each item in items) { + text: "{item}" + } + }`; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + let texts = container.querySelectorAll("saz-text"); + expect(texts.length).toBe(1); + expect(texts[0]!.shadowRoot!.textContent).toContain("x"); + }); + + test("@class with pair syntax adds CSS class attribute", () => { + const source = `app { span(class highlight): "hello" }`; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + const span = container.querySelector("span"); + expect(span).toBeTruthy(); + expect(span!.getAttribute("class")).toBe("highlight"); + }); +}); diff --git a/Build/tests/errors.test.ts b/Build/tests/errors.test.ts index f0039d2..61319f7 100644 --- a/Build/tests/errors.test.ts +++ b/Build/tests/errors.test.ts @@ -31,8 +31,10 @@ describe('Tokenizer - Error handling', () => { expect(() => tokenize('price: $5')).toThrow('Unexpected character: $'); }); - test('should throw on unexpected character !', () => { - expect(() => tokenize('!important')).toThrow('Unexpected character: !'); + test('should tokenize ! as a valid operator', () => { + const tokens = tokenize('!important'); + expect(tokens[0]).toMatchObject({ type: 'BANG', value: '!' }); + expect(tokens[1]).toMatchObject({ type: 'IDENT', value: 'important' }); }); test('should handle string with only whitespace content', () => { diff --git a/Build/tests/integration.test.ts b/Build/tests/integration.test.ts index ae16485..155fd4b 100644 --- a/Build/tests/integration.test.ts +++ b/Build/tests/integration.test.ts @@ -31,8 +31,10 @@ describe('Tokenizer - Error handling', () => { expect(() => tokenize('price: $5')).toThrow('Unexpected character: $'); }); - test('should throw on unexpected character !', () => { - expect(() => tokenize('!important')).toThrow('Unexpected character: !'); + test('should tokenize ! as a valid operator', () => { + const tokens = tokenize('!important'); + expect(tokens[0]).toMatchObject({ type: 'BANG', value: '!' }); + expect(tokens[1]).toMatchObject({ type: 'IDENT', value: 'important' }); }); test('should handle string with only whitespace content', () => { diff --git a/Build/tests/modifiers.test.ts b/Build/tests/modifiers.test.ts index decb70c..cd75caf 100644 --- a/Build/tests/modifiers.test.ts +++ b/Build/tests/modifiers.test.ts @@ -58,6 +58,72 @@ describe("Modifier Map", () => { }); }); +describe("Modifier Map CSS Style Pairs", () => { + test("converts position pair to __style", () => { + const modifiers: Modifier[] = [ + { type: "pair", key: "position", value: "absolute" }, + { type: "pair", key: "top", value: "10" }, + { type: "pair", key: "left", value: "20" }, + ]; + const result = parseModifiers(modifiers); + expect(result.__style).toEqual({ + position: "absolute", + top: "10", + left: "20", + }); + }); + + test("converts display pair to __style", () => { + const modifiers: Modifier[] = [ + { type: "pair", key: "display", value: "flex" }, + { type: "pair", key: "width", value: "100%" }, + ]; + const result = parseModifiers(modifiers); + expect(result.__style).toEqual({ + display: "flex", + width: "100%", + }); + }); + + test("merges CSS flags with __style pairs", () => { + const modifiers: Modifier[] = [ + { type: "flag", value: "absolute" }, + { type: "pair", key: "top", value: "10" }, + { type: "pair", key: "left", value: "20" }, + ]; + const result = parseModifiers(modifiers); + expect(result.__style).toEqual({ + position: "absolute", + top: "10", + left: "20", + }); + }); + + test("@style string becomes __rawStyle", () => { + const modifiers: Modifier[] = [ + { type: "atcode", name: "style", body: "color: red; background: blue" }, + ]; + const result = parseModifiers(modifiers); + expect(result.__rawStyle).toBe("color: red; background: blue"); + }); + + test("@style with JSON object merges into __style", () => { + const modifiers: Modifier[] = [ + { type: "atcode", name: "style", body: '{"color": "red", "background": "blue"}' }, + ]; + const result = parseModifiers(modifiers); + expect(result.__style).toEqual({ color: "red", background: "blue" }); + }); + + test("@if becomes __if", () => { + const modifiers: Modifier[] = [ + { type: "atcode", name: "if", body: "isVisible" }, + ]; + const result = parseModifiers(modifiers); + expect(result.__if).toBe("isVisible"); + }); +}); + describe("Modifier Map Constants", () => { test("has text variants", () => { expect(MODIFIER_MAP.accent).toEqual({ variant: "accent" }); @@ -100,4 +166,19 @@ describe("Modifier Map Constants", () => { expect(MODIFIER_MAP.dim.tone).toBe("dim"); expect(MODIFIER_MAP.dim.variant).toBe("dim"); }); + + test("has position modifier flags", () => { + expect(MODIFIER_MAP.absolute).toEqual({ position: "absolute" }); + expect(MODIFIER_MAP.fixed).toEqual({ position: "fixed" }); + expect(MODIFIER_MAP.relative).toEqual({ position: "relative" }); + expect(MODIFIER_MAP.sticky).toEqual({ position: "sticky" }); + }); + + test("has display modifier flags", () => { + expect(MODIFIER_MAP.block).toEqual({ display: "block" }); + expect(MODIFIER_MAP.flex).toEqual({ display: "flex" }); + expect(MODIFIER_MAP.hidden).toEqual({ display: "none" }); + expect(MODIFIER_MAP["inline-block"]).toEqual({ display: "inline-block" }); + expect(MODIFIER_MAP["inline-flex"]).toEqual({ display: "inline-flex" }); + }); }); diff --git a/Build/tests/pipeline.test.ts b/Build/tests/pipeline.test.ts index 9975c55..13b2624 100644 --- a/Build/tests/pipeline.test.ts +++ b/Build/tests/pipeline.test.ts @@ -10,7 +10,32 @@ describe("Full Pipeline - Advanced", () => { document.head.innerHTML = ""; }); - test("compiles the music player example from GUIDE.md", () => { + test("applies position modifier flags as inline styles", () => { + const container = document.createElement("div"); + compileSakko(``, container); + const btn = container.querySelector("saz-button") as HTMLElement; + expect(btn.style.position).toBe("absolute"); + expect(btn.style.top).toBe("10px"); + expect(btn.style.left).toBe("20px"); + }); + + test("applies display modifier flags as inline styles", () => { + const container = document.createElement("div"); + compileSakko(``, container); + const span = container.querySelector("span") as HTMLElement; + expect(span.style.display).toBe("none"); + }); + + test("applies @style string as inline CSS", () => { + const container = document.createElement("div"); + compileSakko(``, container); + const allDivs = container.querySelectorAll("div"); + expect(allDivs.length).toBe(2); + expect(allDivs[1].style.color).toBe("red"); + expect(allDivs[1].style.fontSize).toBe("20px"); + }); + + test("compiles the music player example", () => { const source = ` { } }); }); + +describe("Reactive Stuff", () => { + const templates = [ + // Counter + ``, + // Theme + ``, + // Tasks + ``, + ``, + // FAB + ``, + ]; + + templates.forEach((tmpl, i) => { + test(`reactive-lab template #${i + 1} parses and compiles`, () => { + const container = document.createElement("div"); + expect(() => compileSakko(tmpl, container)).not.toThrow(); + // at least the container should have been populated + expect(container.children.length).toBeGreaterThan(0); + }); + }); +}); diff --git a/Build/tests/reactive-pipeline.test.ts b/Build/tests/reactive-pipeline.test.ts new file mode 100644 index 0000000..82a4abf --- /dev/null +++ b/Build/tests/reactive-pipeline.test.ts @@ -0,0 +1,223 @@ +/** + * @jest-environment jest-fixed-jsdom + */ +import { describe, test, expect, beforeAll, beforeEach } from "@jest/globals"; +import { compileSakko } from "../src/index"; +import { __resetRegistryForTesting } from "@nisoku/sairin"; + +function defineOnce(name: string, cls: any) { + if (!customElements.get(name)) { + customElements.define(name, cls); + } +} + +beforeEach(() => { + document.head.innerHTML = ""; + document.body.innerHTML = ""; + __resetRegistryForTesting(); +}); + +beforeAll(() => { + defineOnce("saz-text", require("../src/primitives/text").SazamiText); + defineOnce("saz-badge", require("../src/primitives/badge").SazamiBadge); + defineOnce("saz-heading", require("../src/primitives/heading").SazamiHeading); + defineOnce("saz-label", require("../src/primitives/label").SazamiLabel); + defineOnce("saz-button", require("../src/primitives/button").SazamiButton); + defineOnce("saz-input", require("../src/primitives/input").SazamiInput); + defineOnce("saz-checkbox", require("../src/primitives/checkbox").SazamiCheckbox); + defineOnce("saz-card", require("../src/primitives/card").SazamiCard); + defineOnce("saz-row", require("../src/primitives/row").SazamiRow); + defineOnce("saz-column", require("../src/primitives/column").SazamiColumn); + defineOnce("saz-grid", require("../src/primitives/grid").SazamiGrid); + defineOnce("saz-stack", require("../src/primitives/stack").SazamiStack); + defineOnce("saz-section", require("../src/primitives/section").SazamiSection); + defineOnce("saz-icon", require("../src/primitives/icon").SazamiIcon); + defineOnce("saz-divider", require("../src/primitives/divider").SazamiDivider); + defineOnce("saz-spacer", require("../src/primitives/spacer").SazamiSpacer); + defineOnce("saz-spinner", require("../src/primitives/spinner").SazamiSpinner); +}); + +describe("Reactive Pipeline - @state/@derived/@effect", () => { + test("@state creates a signal and interpolated value updates reactively", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const text = container.querySelector("saz-text"); + expect(text).toBeTruthy(); + expect(text!.shadowRoot?.textContent).toContain("Hello World"); + }); + + test("@state with multiple variables", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const text = container.querySelector("saz-text"); + expect(text!.shadowRoot?.textContent).toContain("John Doe"); + }); + + test("@derived computes from state signals", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const text = container.querySelector("saz-text"); + expect(text!.shadowRoot?.textContent).toContain("Sum: 30"); + }); + + test("@effect runs and can be triggered by state changes", async () => { + const logSpy = jest.spyOn(console, "log").mockImplementation(() => {}); + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + expect(logSpy).toHaveBeenCalledWith("count is", 0); + + logSpy.mockRestore(); + }); + + test("@on:click fires event handler", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const button = container.querySelector("saz-button"); + expect(button).toBeTruthy(); + + button!.dispatchEvent(new MouseEvent("click", { bubbles: true })); + await Promise.resolve(); + + expect(button!.textContent).toContain("Count: 1"); + }); + + test("@on:click with increment operator", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const button = container.querySelector("saz-button"); + button!.dispatchEvent(new MouseEvent("click", { bubbles: true })); + await Promise.resolve(); + + expect(button!.textContent).toContain("1"); + + button!.dispatchEvent(new MouseEvent("click", { bubbles: true })); + await Promise.resolve(); + + expect(button!.textContent).toContain("2"); + }); + + test("@state with number values", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const text = container.querySelector("saz-text"); + expect(text!.shadowRoot?.textContent).toContain("42"); + }); + + test("@state with boolean values", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const text = container.querySelector("saz-text"); + expect(text!.shadowRoot?.textContent).toContain("true"); + }); + + test("interpolation with multiple expressions", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const text = container.querySelector("saz-text"); + expect(text!.shadowRoot?.textContent).toContain("Hello, World!"); + }); + + test("static text without expressions still works", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + await Promise.resolve(); + + const text = container.querySelector("saz-text"); + expect(text!.shadowRoot?.textContent).toContain("Just static text"); + }); + + test("no declarations still compiles", async () => { + const source = ``; + + const container = document.createElement("div"); + document.body.appendChild(container); + compileSakko(source, container); + + expect(container.querySelector("saz-text")).toBeTruthy(); + expect(container.querySelector("saz-button")).toBeTruthy(); + }); +}); diff --git a/Build/tests/renderer.test.ts b/Build/tests/renderer.test.ts index 67a6fe8..1b7cbd5 100644 --- a/Build/tests/renderer.test.ts +++ b/Build/tests/renderer.test.ts @@ -197,4 +197,45 @@ describe("Renderer", () => { expect(div.textContent).toBe("Before bold after"); expect(div.childNodes).toHaveLength(3); }); + + test("applies __style object as inline styles", () => { + const vnode: VNode = { + type: "div", + props: { __style: { position: "absolute", top: "10px", left: "20px" } }, + children: [], + }; + render(vnode, container); + const el = container.firstChild as HTMLElement; + expect(el.style.position).toBe("absolute"); + expect(el.style.top).toBe("10px"); + expect(el.style.left).toBe("20px"); + }); + + test("applies __rawStyle as cssText", () => { + const vnode: VNode = { + type: "div", + props: { __rawStyle: "color: red; font-size: 16px" }, + children: [], + }; + render(vnode, container); + const el = container.firstChild as HTMLElement; + expect(el.style.color).toBe("red"); + expect(el.style.fontSize).toBe("16px"); + }); + + test("applies both __style and __rawStyle", () => { + const vnode: VNode = { + type: "div", + props: { + __rawStyle: "background: blue", + __style: { color: "red", position: "absolute" }, + }, + children: [], + }; + render(vnode, container); + const el = container.firstChild as HTMLElement; + expect(el.style.position).toBe("absolute"); + expect(el.style.color).toBe("red"); + expect(el.style.background).toBe("blue"); + }); }); diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3d91baa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +## [0.1.1] - 2026-07-10 + +### Changed + +- Updated ESLint to flat config with strict type rules +- Fixed all type violations across the codebase +- Fixed `_disabled` field type in toggle/select/slider/switch primitives + +### Added + +- Added CI workflow +- Added CI lint step + +## [0.1.0] - 2026-06-09 + +Initial public release. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..887509d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,20 @@ +# Code of Conduct + +## Our Pledge + +We pledge to make participation in this project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +- Be respectful and inclusive +- Use welcoming language +- Accept constructive criticism gracefully +- Focus on what's best for the community + +## Enforcement + +Project maintainers are responsible for clarifying and enforcing standards. Instances of abusive behavior may be reported by contacting the project team. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..149a350 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributing to Sazami + +Thanks for your interest in contributing! + +## Getting Started + +1. Fork the repository +2. Clone your fork: `git clone https://github.com/your-username/Sazami.git` +3. Install dependencies: `cd Build && npm install` +4. Create a feature branch: `git checkout -b feat/your-feature` + +## Development + +```bash +cd Build +npm run dev # watch mode +npm test # run tests +npm run lint # check lint +npm run typecheck # type-check +``` + +## Pull Requests + +- Keep changes focused. One feature or fix per PR. +- Add tests for new functionality +- Ensure all checks pass (test, lint, typecheck, build) +- Update CHANGELOG.md if applicable + +## Code Style + +- ESLint and Prettier are configured. Run `npm run make-pretty` before committing. +- Follow the existing patterns in the codebase +- Avoid adding comments unless necessary for clarity + +## Reporting Issues + +Use the GitHub issue tracker with the appropriate template. diff --git a/Docs/docs/primitives.md b/Docs/docs/primitives.md index f815f67..259b780 100644 --- a/Docs/docs/primitives.md +++ b/Docs/docs/primitives.md @@ -477,7 +477,7 @@ Specialized image for album/media artwork. Fixed aspect ratio, square by default | ----------- | -------- | --------- | ------------- | | `src` | URL string | - | Image source | | `shape` | `square`, `round` | `square` | Border shape | -| `size` | `small`, `medium`, `large`, `xlarge` | `medium` | Dimensions (48–160px) | +| `size` | `small`, `medium`, `large`, `xlarge` | `medium` | Dimensions (48-160px) | ```sako coverart(round): "album.jpg" diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..768aaec --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,17 @@ +# Security Policy + +## Reporting a Vulnerability + +If you discover a security vulnerability, please report it privately by opening a security advisory at: + +https://github.com/Nisoku/Sazami/security/advisories + +Please do **not** report security vulnerabilities through public GitHub issues. + +## Response + +We will acknowledge receipt within 48 hours and provide an estimated timeline for a fix. + +## Disclosure + +We follow a 90-day disclosure window after a fix is released.