diff --git a/.github/workflows/ci.yml b/.github/workflows/checks.yml similarity index 68% rename from .github/workflows/ci.yml rename to .github/workflows/checks.yml index 463dae3..dc17ce9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/checks.yml @@ -1,4 +1,4 @@ -name: CI +name: Checks on: push: @@ -7,12 +7,12 @@ on: branches: [main, master] jobs: - test: + checks: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x, 20.x] + node-version: [20.x] steps: - name: Checkout code @@ -27,20 +27,9 @@ jobs: - name: Install dependencies run: npm ci - - name: Check formatting - run: npm run format:check - - - name: Run type check - run: npm run type-check - - - name: Run linter - run: npm run lint - - - name: Run tests - run: npm run test:ci - - - name: Build project - run: npm run build + - name: Run checks + shell: bash + run: ./scripts/checks.sh - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..e14bcb0 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,26 @@ +name: Pull Request + +on: + pull_request: + branches: [main, master] + +jobs: + pr-checks: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run checks + shell: bash + run: ./scripts/checks.sh diff --git a/avocado-config.json b/avocado-config.json deleted file mode 120000 index 5241138..0000000 --- a/avocado-config.json +++ /dev/null @@ -1 +0,0 @@ -/Users/spoff/repos/peridio/docs/src/schemas/avocado-config.json \ No newline at end of file diff --git a/copy-to-tmp.sh b/copy-to-tmp.sh deleted file mode 100644 index fcff834..0000000 --- a/copy-to-tmp.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -# Create tmp directory if it doesn't exist -mkdir -p tmp - -# Copy files to tmp folder -cp wasm-*.cjs tmp/ 2>/dev/null || echo "Warning: No wasm-*.cjs files found" -cp index.cjs.js tmp/ 2>/dev/null || echo "Warning: index.cjs.js not found" -cp style.css tmp/ 2>/dev/null || echo "Warning: style.css not found" - -echo "Files copied to tmp folder" diff --git a/package-lock.json b/package-lock.json index 5f96766..bd35599 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "@vitejs/plugin-react": "^4.0.0", + "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "eslint": "^8.0.0", "eslint-config-prettier": "^10.1.8", @@ -54,6 +55,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -387,6 +402,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -1095,6 +1120,63 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1328,6 +1410,17 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", @@ -2348,6 +2441,40 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -2847,6 +2974,35 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.5.tgz", + "integrity": "sha512-9SdXjNheSiE8bALAQCQQuT6fgQaoxJh7IRYrRGZ8/9nv8WhJeC1aXAwN8TbaOssGOukUvyvnkgD9+Yuykvl1aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.30", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -3462,6 +3618,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.215", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.215.tgz", @@ -3469,6 +3632,13 @@ "dev": true, "license": "ISC" }, + "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", @@ -4273,6 +4443,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -4428,6 +4615,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "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", @@ -4441,6 +4649,22 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -4675,6 +4899,13 @@ "node": ">=18" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/html-void-elements": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", @@ -4995,6 +5226,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", @@ -5256,6 +5497,60 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -5274,6 +5569,22 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jju": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", @@ -5538,6 +5849,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5721,6 +6060,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -6005,6 +6354,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6075,6 +6431,30 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -6867,6 +7247,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -6979,6 +7372,76 @@ "node": ">=0.6.19" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -7104,6 +7567,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -7205,6 +7682,37 @@ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", "license": "MIT" }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -8061,6 +8569,107 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 4728a1c..1d1b9b3 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "@vitejs/plugin-react": "^4.0.0", + "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "eslint": "^8.0.0", "eslint-config-prettier": "^10.1.8", diff --git a/scripts/checks.sh b/scripts/checks.sh new file mode 100755 index 0000000..5eefc99 --- /dev/null +++ b/scripts/checks.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +npm run format:check +npm run type-check +npm run lint +npm run test:ci +npm run build diff --git a/src/DeckardSchema.styles.css b/src/DeckardSchema.styles.css index 45759d2..0770a7d 100644 --- a/src/DeckardSchema.styles.css +++ b/src/DeckardSchema.styles.css @@ -8,6 +8,7 @@ --schema-bg: transparent; --schema-surface: transparent; --schema-surface-hover: rgba(99, 152, 255, 0.04); + --schema-modal-bg: #ffffff; --schema-border: #e8e8e8; --schema-border-strong: #cbd5e1; --schema-border-subtle: rgba(0, 0, 0, 0.04); @@ -80,6 +81,7 @@ --schema-code-text: #e6edf3; --schema-accent-soft: rgba(59, 130, 246, 0.15); --schema-accent-hover: rgba(148, 204, 235, 0.08); + --schema-modal-bg: #ffffff; } } @@ -134,6 +136,40 @@ margin: var(--schema-space-lg) 0 var(--schema-space-sm); } +/* ===== NO SEARCH RESULTS ===== */ + +.schema-container .no-search-results { + display: flex; + flex-direction: column; + align-items: center; + padding: var(--schema-space-xl) var(--schema-space-lg); + text-align: center; + border: 1px solid var(--schema-border); + border-radius: var(--schema-radius-md); + background: var(--schema-surface-hover); + margin: var(--schema-space-lg) 0; +} + +.schema-container .no-search-results-icon { + font-size: 2rem; + margin-bottom: var(--schema-space-md); + opacity: 0.6; +} + +.schema-container .no-search-results-message h3 { + margin: 0 0 var(--schema-space-sm) 0; + font-size: var(--schema-text-lg); + font-weight: 600; + color: var(--schema-text); +} + +.schema-container .no-search-results-message p { + margin: 0; + font-size: var(--schema-text-sm); + color: var(--schema-text-muted); + line-height: 1.5; +} + /* ===== PROPERTIES SECTION ===== */ .schema-container .properties-section { @@ -299,6 +335,149 @@ .schema-container .schema-search { display: none; } + + .schema-container .no-search-results { + display: none; + } +} + +/* ===== LINK BUTTON ANCHOR STYLES ===== */ + +.schema-container a.link-button, +.schema-container a.row-button.link-button { + text-decoration: none; + color: var(--schema-text-muted); + display: inline-flex; +} + +.schema-container a.link-button:hover, +.schema-container a.row-button.link-button:hover { + text-decoration: none; + color: var(--schema-text); +} + +/* Active route link button - red map pin */ +.schema-container a.link-button.active-route, +.schema-container a.row-button.link-button.active-route, +.schema-container .tooltip-trigger a.row-button.link-button.active-route { + color: var(--schema-danger); +} + +.schema-container a.link-button.active-route:hover, +.schema-container a.row-button.link-button.active-route:hover, +.schema-container .tooltip-trigger a.row-button.link-button.active-route:hover { + color: var(--schema-danger); +} + +.schema-container a.link-button:focus, +.schema-container a.row-button.link-button:focus { + outline: none; + box-shadow: 0 0 0 2px var(--schema-accent-soft); + border-radius: var(--schema-radius-sm); +} + +.schema-container a.link-button:visited, +.schema-container a.row-button.link-button:visited { + color: var(--schema-text-muted); +} + +/* ===== TOOLTIP SHIMMER EFFECT ===== */ + +@keyframes tooltip-wiggle { + 0%, + 100% { + transform: translateX(0); + } + 10% { + transform: translateX(-2px) rotate(-1deg); + } + 20% { + transform: translateX(2px) rotate(1deg); + } + 30% { + transform: translateX(-1px) rotate(-0.5deg); + } + 40% { + transform: translateX(1px) rotate(0.5deg); + } + 50% { + transform: translateX(0) rotate(0deg); + } + 60% { + transform: translateX(1px) rotate(0.3deg); + } + 70% { + transform: translateX(-1px) rotate(-0.3deg); + } + 80% { + transform: translateX(0.5px) rotate(0.2deg); + } + 90% { + transform: translateX(-0.5px) rotate(-0.1deg); + } +} + +@keyframes tooltip-glow { + 0%, + 100% { + opacity: 0; + } + 10% { + opacity: 0.3; + } + 20% { + opacity: 0.4; + } + 30% { + opacity: 0.35; + } + 40% { + opacity: 0.4; + } + 50% { + opacity: 0.25; + } + 60% { + opacity: 0.2; + } + 70% { + opacity: 0.15; + } + 80% { + opacity: 0.1; + } + 90% { + opacity: 0.05; + } +} + +.tooltip-trigger { + position: relative; +} + +.tooltips-shimmer .tooltip-trigger { + animation: tooltip-wiggle 1.2s ease-in-out infinite; +} + +.tooltips-shimmer .tooltip-trigger::before { + content: ''; + position: absolute; + top: -8px; + left: -8px; + right: -8px; + bottom: -8px; + background: radial-gradient( + ellipse at center, + rgba(0, 188, 212, 0.4) 0%, + rgba(0, 188, 212, 0.2) 30%, + rgba(0, 188, 212, 0.1) 50%, + transparent 70% + ); + border-radius: 50%; + pointer-events: none; + opacity: 0; + animation: tooltip-glow 1.2s ease-in-out infinite; + z-index: -1; } /* ===== ACCESSIBILITY ===== */ @@ -321,4 +500,9 @@ animation-duration: 0.01ms; transition-duration: 0.01ms; } + + /* Disable tooltip wiggle effect for users who prefer reduced motion */ + .tooltips-shimmer .tooltip-trigger { + animation: none; + } } diff --git a/src/DeckardSchema.styles.ts b/src/DeckardSchema.styles.ts index 8414677..f3b0ed1 100644 --- a/src/DeckardSchema.styles.ts +++ b/src/DeckardSchema.styles.ts @@ -9,6 +9,7 @@ export const deckardSchemaStyles = ` --schema-bg: transparent; --schema-surface: transparent; --schema-surface-hover: rgba(99, 152, 255, 0.04); + --schema-modal-bg: #ffffff; --schema-border: #e8e8e8; --schema-border-strong: #cbd5e1; --schema-border-subtle: rgba(0, 0, 0, 0.04); @@ -77,6 +78,7 @@ export const deckardSchemaStyles = ` --schema-code-bg: rgba(255, 255, 255, 0.06); --schema-accent-soft: rgba(59, 130, 246, 0.15); --schema-accent-hover: rgba(148, 204, 235, 0.08); + --schema-modal-bg: #ffffff; } } diff --git a/src/DeckardSchema.tsx b/src/DeckardSchema.tsx index c826ab1..6110611 100644 --- a/src/DeckardSchema.tsx +++ b/src/DeckardSchema.tsx @@ -19,7 +19,7 @@ import './property/ExamplesPanel.styles.css'; import './Rows.styles.css'; import './components/Settings.styles.css'; import './inputs/RadioGroup.styles.css'; -import { extractProperties, getSchemaType } from './utils.js'; +import { extractProperties, getSchemaType, resolveSchema } from './utils'; import Rows from './Rows'; import { Input } from './inputs'; import { Settings } from './components'; @@ -40,6 +40,7 @@ const DEFAULT_OPTIONS: DeckardOptions = { collapsible: true, autoExpand: false, theme: 'auto', + defaultExampleLanguage: 'yaml', }; // Load settings from localStorage @@ -48,11 +49,18 @@ const loadStoredSettings = (siteKey?: string): Partial => { if (typeof window === 'undefined') return {}; const key = siteKey || window.location.hostname; const storageKey = `deckard-settings-${key}`; + const stored = localStorage.getItem(storageKey); - return stored ? JSON.parse(stored) : {}; + const parsedSettings = stored ? JSON.parse(stored) : {}; + + // Ensure searchable is explicitly enabled by default if not set + if (parsedSettings.searchable === undefined) { + parsedSettings.searchable = true; + } + return parsedSettings; } catch (error) { console.warn('Failed to load settings from localStorage:', error); - return {}; + return { searchable: true }; } }; @@ -95,12 +103,38 @@ export const DeckardSchema: React.FC = ({ className, }) => { // Load settings from localStorage and merge with provided options - const storedSettings = loadStoredSettings(); - const [currentOptions, setCurrentOptions] = useState({ - ...DEFAULT_OPTIONS, - ...storedSettings, - ...options, // Props override localStorage settings + const storedSettings = loadStoredSettings( + typeof window !== 'undefined' ? window.location.hostname : 'default' + ); + + const [currentOptions, setCurrentOptions] = useState(() => { + const merged = { + ...DEFAULT_OPTIONS, + ...storedSettings, + ...options, // Props override localStorage settings + }; + + // Ensure searchable is always explicitly set + if (merged.searchable === undefined || merged.searchable === null) { + merged.searchable = true; + } + + return merged; }); + + // Update stored settings when currentOptions changes + useEffect(() => { + if (typeof window !== 'undefined') { + try { + const siteKey = window.location.hostname; + const storageKey = `deckard-settings-${siteKey}`; + localStorage.setItem(storageKey, JSON.stringify(currentOptions)); + } catch (error) { + console.warn('Failed to persist settings to localStorage:', error); + } + } + }, [currentOptions]); + const mergedOptions = currentOptions; // Extract specific values to avoid dependency issues @@ -134,10 +168,29 @@ export const DeckardSchema: React.FC = ({ const filteredProperties = useMemo(() => { if (!searchState.query) return properties; + const query = searchState.query.toLowerCase(); + return properties.filter(prop => { - const searchText = - `${prop.name} ${prop.schema.description || ''} ${getSchemaType(prop.schema, schema)}`.toLowerCase(); - return searchText.includes(searchState.query.toLowerCase()); + // Search field names + if (prop.name.toLowerCase().includes(query)) return true; + + // Search descriptions + if (prop.schema.description?.toLowerCase().includes(query)) return true; + + // Search schema type + if (getSchemaType(prop.schema, schema).toLowerCase().includes(query)) + return true; + + // Search examples + if (prop.schema.examples) { + for (const example of prop.schema.examples) { + const exampleText = + typeof example === 'string' ? example : JSON.stringify(example); + if (exampleText.toLowerCase().includes(query)) return true; + } + } + + return false; }); }, [properties, searchState.query, schema]); @@ -159,7 +212,9 @@ export const DeckardSchema: React.FC = ({ newStates[key] = { expanded: Boolean(autoExpand), hasDetails: true, // All fields are expandable now - matchesSearch: true, + matchesSearch: true, // Always true initially + isDirectMatch: false, + hasNestedMatches: false, }; // Recursively initialize nested properties @@ -177,24 +232,33 @@ export const DeckardSchema: React.FC = ({ // Initialize all properties (top-level and nested) initializePropertyStates(schema, [], 0, schema); - // Handle URL hash for auto-expansion + setPropertyStates(newStates); + }, [schema, autoExpand, searchState.query]); + + // Handle URL hash navigation - only on mount + useEffect(() => { const hash = typeof window !== 'undefined' ? window.location.hash : ''; if (hash) { const fieldKey = hash.substring(1).replace(/-/g, '.'); - // Expand all parent paths to make the target field visible - const pathParts = fieldKey.split('.'); - for (let i = 1; i <= pathParts.length; i++) { - const parentPath = pathParts.slice(0, i).join('.'); - if (newStates[parentPath]) { - newStates[parentPath].expanded = true; + // Update property states to expand path to target + setPropertyStates(prev => { + const newStates = { ...prev }; + + // Expand all parent paths to make the target field visible + const pathParts = fieldKey.split('.'); + for (let i = 1; i <= pathParts.length; i++) { + const parentPath = pathParts.slice(0, i).join('.'); + if (newStates[parentPath]) { + newStates[parentPath] = { + ...newStates[parentPath], + expanded: true, + }; + } } - } - // Expand the target field itself - if (newStates[fieldKey]) { - newStates[fieldKey].expanded = true; - } + return newStates; + }); // Set the focused property state for proper styling setFocusedProperty(fieldKey); @@ -206,31 +270,16 @@ export const DeckardSchema: React.FC = ({ fieldKey.replace(/\./g, '-') ); if (targetElement) { - // Focus the header container to match click behavior - const headerContainer = targetElement.querySelector( - '.property-header-container' - ) as HTMLElement; - if (headerContainer) { - headerContainer.focus(); - } else { - targetElement.setAttribute('tabindex', '-1'); - targetElement.focus(); - } - - // Manual scroll since browser's initial scroll failed - if (typeof targetElement.scrollIntoView === 'function') { - targetElement.scrollIntoView({ - behavior: 'smooth', - block: 'start', - }); - } + targetElement.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + targetElement.focus({ preventScroll: true }); } } }, 100); } - - setPropertyStates(newStates); - }, [schema, autoExpand]); + }, []); // Empty dependency array - only run on mount // Update search results useEffect(() => { @@ -254,19 +303,111 @@ export const DeckardSchema: React.FC = ({ (query: string) => { setSearchState(prev => ({ ...prev, query })); - // Auto-expand matching properties when searching + // When searching, collapse everything first, then expand matches if (query && collapsible) { const newStates = { ...propertyStates }; - filteredProperties.forEach(prop => { - const key = prop.path.join('.'); - if (newStates[key]?.hasDetails) { - newStates[key] = { ...newStates[key], expanded: true }; + const queryLower = query.toLowerCase(); + + // First, collapse everything + Object.keys(newStates).forEach(key => { + if (newStates[key]) { + newStates[key] = { + ...newStates[key], + expanded: false, + matchesSearch: false, + isDirectMatch: false, + hasNestedMatches: false, + }; + } + }); + + // Then find and mark direct matches + const directMatches = new Set(); + Object.keys(newStates).forEach(key => { + const pathSegments = key.split('.'); + + // Find the property in the schema to check for matches + let currentSchema = schema; + + for (let i = 0; i < pathSegments.length; i++) { + const segment = pathSegments[i]; + if (currentSchema.properties?.[segment]) { + currentSchema = currentSchema.properties[segment]; + if (i === pathSegments.length - 1) { + // Check if this property matches search + const matches = + segment.toLowerCase().includes(queryLower) || + currentSchema.description + ?.toLowerCase() + .includes(queryLower) || + getSchemaType(currentSchema, schema) + .toLowerCase() + .includes(queryLower) || + (currentSchema.examples && + currentSchema.examples.some(example => { + const exampleText = + typeof example === 'string' + ? example + : JSON.stringify(example); + return exampleText.toLowerCase().includes(queryLower); + })) || + false; + + if (matches) { + directMatches.add(key); + } + } + } + } + }); + + // Mark direct matches and propagate up parent chain + + directMatches.forEach(matchKey => { + // Mark the direct match + if (newStates[matchKey]) { + newStates[matchKey] = { + ...newStates[matchKey], + expanded: true, + matchesSearch: true, + isDirectMatch: true, + }; + } + + // Propagate up the parent chain + const pathSegments = matchKey.split('.'); + for (let i = pathSegments.length - 1; i > 0; i--) { + const parentKey = pathSegments.slice(0, i).join('.'); + if (newStates[parentKey]) { + newStates[parentKey] = { + ...newStates[parentKey], + expanded: true, + matchesSearch: true, + hasNestedMatches: true, + }; + } + } + }); + + setPropertyStates(newStates); + } else if (!query) { + // When clearing search, reset to default state + const newStates = { ...propertyStates }; + Object.keys(newStates).forEach(key => { + if (newStates[key]) { + newStates[key] = { + ...newStates[key], + expanded: Boolean(autoExpand), + matchesSearch: true, + isDirectMatch: false, + hasNestedMatches: false, + }; } }); setPropertyStates(newStates); } }, - [propertyStates, filteredProperties, collapsible] + [propertyStates, collapsible, schema, autoExpand] ); const expandAll = useCallback(() => { @@ -344,6 +485,9 @@ export const DeckardSchema: React.FC = ({ // Add nested properties if parent is expanded if (propertyStates[key]?.expanded && prop.schema) { + const allNestedProps: SchemaProperty[] = []; + + // First, collect regular nested properties const nested = extractProperties( prop.schema, prop.path, @@ -351,8 +495,31 @@ export const DeckardSchema: React.FC = ({ schema, [] ); - nested.sort((a, b) => a.name.localeCompare(b.name)); - nested.forEach(nestedProp => addProperty(nestedProp, currentDepth + 1)); + allNestedProps.push(...nested); + + // Then, collect allOf properties if they exist + if (prop.schema.allOf) { + prop.schema.allOf.forEach(allOfSchema => { + const resolvedSchema = resolveSchema(allOfSchema, schema); + if (resolvedSchema.properties || resolvedSchema.patternProperties) { + const allOfPath = [...prop.path, 'allof']; + const allOfProps = extractProperties( + resolvedSchema, + allOfPath, + prop.depth + 1, + schema, + [] + ); + allNestedProps.push(...allOfProps); + } + }); + } + + // Sort all nested properties together alphabetically + allNestedProps.sort((a, b) => a.name.localeCompare(b.name)); + allNestedProps.forEach(nestedProp => + addProperty(nestedProp, currentDepth + 1) + ); } }; @@ -483,14 +650,21 @@ export const DeckardSchema: React.FC = ({ return; } + // Tooltip shimmer effect on Ctrl key + if (e.key === 'Control' && !e.repeat) { + if (typeof document !== 'undefined') { + document.documentElement.classList.add('tooltips-shimmer'); + } + } + // Always prevent default for hjkl keys to stop page scrolling if (['h', 'j', 'k', 'l'].includes(e.key.toLowerCase())) { e.preventDefault(); e.stopPropagation(); } - // Search shortcut: / - if (e.key === '/') { + // Search shortcut: Shift + / + if (e.key === '?' && e.shiftKey) { e.preventDefault(); if (typeof document !== 'undefined') { const searchInput = document.querySelector( @@ -531,14 +705,28 @@ export const DeckardSchema: React.FC = ({ } }; + const handleKeyUp = (e: KeyboardEvent) => { + // Remove tooltip shimmer effect on Ctrl key release + if (e.key === 'Control') { + if (typeof document !== 'undefined') { + document.documentElement.classList.remove('tooltips-shimmer'); + } + } + }; + if (typeof document !== 'undefined') { document.addEventListener('keydown', handleKeyDown, { capture: true }); - return () => + document.addEventListener('keyup', handleKeyUp, { capture: true }); + return () => { document.removeEventListener('keydown', handleKeyDown, { capture: true, }); + document.removeEventListener('keyup', handleKeyUp, { + capture: true, + }); + }; } - }, [expandAll, collapseAll, clearSearch, handleNavigation]); + }, [clearSearch, expandAll, collapseAll, handleNavigation]); // Click away to clear focused property useEffect(() => { @@ -679,25 +867,17 @@ export const DeckardSchema: React.FC = ({ )} )} - {searchable && ( -
- handleSearch(e.target.value)} - tabIndex={1} - /> -
- )} {includePropertiesTitle && (

Properties

)} @@ -706,26 +886,64 @@ export const DeckardSchema: React.FC = ({ + + )} + + {searchable && ( +
+ handleSearch(e.target.value)} + tabIndex={1} />
)}
- + {filteredProperties.length === 0 && searchState.query ? ( +
+
🔍
+
+

No properties match your search

+

+ Try adjusting your search terms or clearing the search to + see all properties. +

+
+
+ ) : ( + + )}
{includeDefinitions && schema.definitions && (
diff --git a/src/NoAdditionalPropertiesRow.styles.css b/src/NoAdditionalPropertiesRow.styles.css index eef0400..cfaa21c 100644 --- a/src/NoAdditionalPropertiesRow.styles.css +++ b/src/NoAdditionalPropertiesRow.styles.css @@ -63,7 +63,9 @@ .schema-container .no-additional-properties:focus, .schema-container .no-additional-properties:focus-visible, .schema-container .no-additional-properties .row-header-container:focus, -.schema-container .no-additional-properties .row-header-container:focus-visible { +.schema-container + .no-additional-properties + .row-header-container:focus-visible { outline: none; background: transparent; box-shadow: none; diff --git a/src/Row.styles.css b/src/Row.styles.css index 501fb25..6345b97 100644 --- a/src/Row.styles.css +++ b/src/Row.styles.css @@ -79,7 +79,7 @@ } .schema-container .row-button:hover { - background: var(--schema-surface-hover); + background: rgba(59, 130, 246, 0.1); color: var(--schema-text); } diff --git a/src/Rows.tsx b/src/Rows.tsx index 5efbf9d..9720070 100644 --- a/src/Rows.tsx +++ b/src/Rows.tsx @@ -19,6 +19,8 @@ interface RowsProps { onFocusChange?: (propertyKey: string | null) => void; showNoAdditionalProperties?: boolean; className?: string; + options?: { defaultExampleLanguage?: 'json' | 'yaml' | 'toml' }; + searchQuery?: string; } const Rows: React.FC = ({ @@ -36,6 +38,8 @@ const Rows: React.FC = ({ onFocusChange, showNoAdditionalProperties = false, className = '', + options, + searchQuery, }) => { const rowsClasses = ['properties-rows', className].filter(Boolean).join(' '); @@ -63,6 +67,8 @@ const Rows: React.FC = ({ toggleProperty={toggleProperty} focusedProperty={focusedProperty} onFocusChange={onFocusChange} + options={options} + searchQuery={searchQuery} /> ); })} diff --git a/src/__tests__/DeckardSchema.test.tsx b/src/__tests__/DeckardSchema.test.tsx new file mode 100644 index 0000000..cfdacb4 --- /dev/null +++ b/src/__tests__/DeckardSchema.test.tsx @@ -0,0 +1,36 @@ +import { render, screen } from '@testing-library/react'; +import { DeckardSchema } from '../DeckardSchema'; +import { JsonSchema } from '../types'; + +describe('DeckardSchema', () => { + const mockSchema: JsonSchema = { + type: 'object', + properties: { + name: { + type: 'string', + description: 'A simple string property', + }, + age: { + type: 'number', + description: 'A number property', + }, + }, + required: ['name'], + }; + + test('renders without crashing', () => { + render(); + expect(screen.getByText('Properties')).toBeInTheDocument(); + }); + + test('renders schema properties', () => { + render(); + expect(screen.getByText('name')).toBeInTheDocument(); + expect(screen.getByText('age')).toBeInTheDocument(); + }); + + test('shows required indicator for required properties', () => { + render(); + expect(screen.getByText('required')).toBeInTheDocument(); + }); +}); diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts new file mode 100644 index 0000000..7b0828b --- /dev/null +++ b/src/__tests__/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/src/components/AllOfSelector.tsx b/src/components/AllOfSelector.tsx index 26f5d00..b813ded 100644 --- a/src/components/AllOfSelector.tsx +++ b/src/components/AllOfSelector.tsx @@ -15,6 +15,8 @@ interface AllOfSelectorProps { focusedProperty?: string | null; onFocusChange?: (propertyKey: string | null) => void; propertyPath?: string[]; + options?: { defaultExampleLanguage?: 'json' | 'yaml' | 'toml' }; + searchQuery?: string; } const AllOfSelector: React.FC = ({ @@ -27,6 +29,8 @@ const AllOfSelector: React.FC = ({ focusedProperty, onFocusChange, propertyPath = [], + options, + searchQuery, }) => { // Resolve all schemas and merge them const mergedSchema = useMemo(() => { @@ -119,6 +123,8 @@ const AllOfSelector: React.FC = ({ expanded: false, hasDetails: true, matchesSearch: true, + isDirectMatch: false, + hasNestedMatches: false, }; }); @@ -159,9 +165,11 @@ const AllOfSelector: React.FC = ({ includeExamples={true} examplesOnFocusOnly={false} rootSchema={rootSchema} - toggleProperty={handleAllOfToggle} + toggleProperty={toggleProperty} focusedProperty={focusedProperty} onFocusChange={onFocusChange} + options={options} + searchQuery={searchQuery} />
)} diff --git a/src/components/Badge.styles.css b/src/components/Badge.styles.css index 9dc8018..0e366b9 100644 --- a/src/components/Badge.styles.css +++ b/src/components/Badge.styles.css @@ -75,9 +75,9 @@ } .schema-container .badge-enum { - background: #f0fdf4; - color: #166534; - border: 1px solid #bbf7d0; + background: #f8fafc; + color: #475569; + border: 1px solid #cbd5e1; font-family: var(--schema-font-mono); font-weight: 500; } @@ -147,7 +147,6 @@ .schema-container .badge-type:hover { background: var(--schema-surface-hover); - color: var(--schema-text); } .schema-container .badge-pattern:hover { @@ -156,9 +155,9 @@ } .schema-container .badge-enum:hover { - background: #dcfce7; - border-color: #86efac; - color: #15803d; + background: #f1f5f9; + border-color: #94a3b8; + color: #334155; } .schema-container .badge-reference:hover { @@ -204,15 +203,15 @@ } .schema-container .badge-enum { - background: #f0fdf4; - color: #166534; - border-color: #bbf7d0; + background: #1e293b; + color: #cbd5e1; + border-color: #475569; } .schema-container .badge-enum:hover { - background: #dcfce7; - border-color: #86efac; - color: #15803d; + background: #334155; + border-color: #64748b; + color: #e2e8f0; } .schema-container .badge-reference { diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 6daba27..1bf4ecb 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -125,21 +125,32 @@ export const BadgeGroup: React.FC = ({ {label}: )} - {values.map((value, index) => ( - onValueClick?.(value)} - title={onValueClick ? 'Click to copy' : undefined} - aria-label={ - onValueClick ? `Copy value ${JSON.stringify(value)}` : undefined - } - > - {JSON.stringify(value)} - - ))} + {values.map((value, index) => { + const displayValue = + typeof value === 'string' + ? value + : value === null + ? 'null' + : value === undefined + ? 'undefined' + : typeof value === 'boolean' || typeof value === 'number' + ? String(value) + : JSON.stringify(value); + + return ( + onValueClick?.(value)} + title={onValueClick ? 'Click to copy' : undefined} + aria-label={onValueClick ? `Copy value ${displayValue}` : undefined} + > + {displayValue} + + ); + })} ); }; diff --git a/src/components/OneOfSelector.tsx b/src/components/OneOfSelector.tsx index a5ffb9c..e9beccb 100644 --- a/src/components/OneOfSelector.tsx +++ b/src/components/OneOfSelector.tsx @@ -14,6 +14,8 @@ interface OneOfSelectorProps { toggleProperty?: (key: string) => void; focusedProperty?: string | null; onFocusChange?: (propertyKey: string | null) => void; + options?: { defaultExampleLanguage?: 'json' | 'yaml' | 'toml' }; + searchQuery?: string; } const OneOfSelector: React.FC = ({ @@ -25,6 +27,8 @@ const OneOfSelector: React.FC = ({ toggleProperty, focusedProperty, onFocusChange, + options, + searchQuery, }) => { const [selectedIndex, setSelectedIndex] = useState(0); @@ -114,6 +118,8 @@ const OneOfSelector: React.FC = ({ expanded: false, hasDetails: true, matchesSearch: true, + isDirectMatch: false, + hasNestedMatches: false, }; }); return states; @@ -129,6 +135,8 @@ const OneOfSelector: React.FC = ({ expanded: !prev[propertyKey]?.expanded, hasDetails: true, matchesSearch: true, + isDirectMatch: false, + hasNestedMatches: false, }, })); // Still call the external toggle if provided @@ -139,7 +147,6 @@ const OneOfSelector: React.FC = ({ return (
- hehe
{optionDisplays.map((display, index) => (
)} diff --git a/src/components/Settings.styles.css b/src/components/Settings.styles.css index df0749d..e920fea 100644 --- a/src/components/Settings.styles.css +++ b/src/components/Settings.styles.css @@ -62,7 +62,7 @@ /* ===== SETTINGS MODAL ===== */ .schema-container .settings-modal { - background: var(--schema-surface); + background: var(--schema-modal-bg); border: 1px solid var(--schema-border); border-radius: var(--schema-radius-lg); width: 90%; @@ -221,7 +221,7 @@ height: 1.125rem; border: 2px solid var(--schema-border-strong); border-radius: 0.25rem; - background: var(--schema-surface); + background: var(--schema-modal-bg); transition: all var(--schema-transition); flex-shrink: 0; display: flex; @@ -296,7 +296,7 @@ .schema-container .settings-footer { padding: var(--schema-space-lg) var(--schema-space-xl); border-top: 1px solid var(--schema-border); - background: var(--schema-surface); + background: var(--schema-modal-bg); } .schema-container .settings-note { @@ -392,6 +392,67 @@ } } +/* ===== LANGUAGE SELECTION ===== */ + +.schema-container .settings-language-option { + display: flex; + align-items: center; + gap: var(--schema-space-sm); + margin-bottom: var(--schema-space-sm); + cursor: pointer; + padding: var(--schema-space-xs) var(--schema-space-sm); + border-radius: var(--schema-radius-sm); + transition: all var(--schema-transition); +} + +.schema-container .settings-language-option:hover { + background: var(--schema-surface-hover); +} + +.schema-container .settings-language-option input[type='radio'] { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.schema-container .settings-language-option .settings-checkbox { + border-radius: 50%; +} + +.schema-container + .settings-language-option + input[type='radio']:checked + + .settings-checkbox { + background: var(--schema-accent); + border-color: var(--schema-accent); +} + +.schema-container + .settings-language-option + input[type='radio']:checked + + .settings-checkbox::after { + content: ''; + width: 6px; + height: 6px; + background: var(--schema-text-inverse); + border-radius: 50%; + font-size: 0; +} + +.schema-container + .settings-language-option + input[type='radio']:focus + + .settings-checkbox { + box-shadow: 0 0 0 3px var(--schema-accent-soft); + border-color: var(--schema-accent); +} + +.schema-container .settings-language-option span { + font-size: var(--schema-text-sm); + font-weight: 500; + color: var(--schema-text); +} + /* ===== CUSTOM PROPERTIES FOR THEMING ===== */ .schema-container { diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index bbce647..2bb5dd4 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -2,6 +2,7 @@ import React, { useState, useCallback } from 'react'; import { FaCog, FaTimes } from 'react-icons/fa'; import { DeckardOptions } from '../types'; import { Button } from '../inputs'; +import { Tooltip } from './'; import './Settings.styles.css'; interface SettingsProps { @@ -28,7 +29,7 @@ const Settings: React.FC = ({ }, []); const handleOptionChange = useCallback( - (key: keyof DeckardOptions, value: boolean) => { + (key: keyof DeckardOptions, value: boolean | string) => { const newOptions = { [key]: value }; onChange(newOptions); @@ -36,10 +37,13 @@ const Settings: React.FC = ({ if (typeof window !== 'undefined') { try { const storageKey = `deckard-settings-${siteKey}`; + const existingSettings = JSON.parse( localStorage.getItem(storageKey) || '{}' ); + const updatedSettings = { ...existingSettings, [key]: value }; + localStorage.setItem(storageKey, JSON.stringify(updatedSettings)); } catch (error) { console.warn('Failed to save settings to localStorage:', error); @@ -60,16 +64,17 @@ const Settings: React.FC = ({ return ( <> - + + + {isOpen && (
@@ -147,7 +152,7 @@ const Settings: React.FC = ({
+ +
+

Examples

+
+
+
+
+
+ Default example language +
+
+ Choose the default format for code examples +
+
+ + + +
+
+
+
+
+
diff --git a/src/components/Tooltip.tsx b/src/components/Tooltip.tsx index 77bdc88..7eae94e 100644 --- a/src/components/Tooltip.tsx +++ b/src/components/Tooltip.tsx @@ -14,6 +14,7 @@ import { useTooltipGlobalManager } from './TooltipGlobalManager'; import './Tooltip.styles.css'; export interface TooltipProps { + title: string; content: React.ReactNode; children: React.ReactNode; placement?: Placement; @@ -31,6 +32,7 @@ export interface TooltipProps { } export const Tooltip: React.FC = ({ + title, content, children, placement = 'top', @@ -213,6 +215,7 @@ export const Tooltip: React.FC = ({ if ( refs.reference.current && refs.floating.current && + 'contains' in refs.reference.current && !refs.reference.current.contains(event.target as Node) && !refs.floating.current.contains(event.target as Node) ) { @@ -272,7 +275,7 @@ export const Tooltip: React.FC = ({ }, [context.middlewareData.arrow, context.placement]); const renderTooltip = () => { - if (!isVisible || disabled || !content) { + if (!isVisible || disabled || !title) { return null; } @@ -313,17 +316,26 @@ export const Tooltip: React.FC = ({ gap: '8px', }} > -
{content}
- {isPinned && ( - - )} +
+
+ {title} + {content && ( + <> +
+ {content} + + )} +
+
+
{showArrow &&
}
diff --git a/src/components/TooltipGlobalManager.tsx b/src/components/TooltipGlobalManager.tsx index 8c96799..c1fe925 100644 --- a/src/components/TooltipGlobalManager.tsx +++ b/src/components/TooltipGlobalManager.tsx @@ -1,10 +1,4 @@ -import React, { - createContext, - useContext, - useState, - useEffect, - useCallback, -} from 'react'; +import React, { createContext, useContext, useState, useCallback } from 'react'; interface TooltipGlobalManagerContextType { showAllTooltips: boolean; @@ -35,16 +29,11 @@ export const useTooltipGlobalManager = () => { export const TooltipGlobalManagerProvider: React.FC<{ children: React.ReactNode; }> = ({ children }) => { - const [showAllTooltips, setShowAllTooltips] = useState(false); + const [showAllTooltips] = useState(false); const [registeredTooltips] = useState< Map void; hide: () => void }> >(new Map()); - // Log when provider mounts - useEffect(() => { - // TooltipGlobalManagerProvider mounted and ready - }, []); - const registerTooltip = useCallback( (id: string, showTooltip: () => void, hideTooltip: () => void) => { registeredTooltips.set(id, { show: showTooltip, hide: hideTooltip }); @@ -59,43 +48,6 @@ export const TooltipGlobalManagerProvider: React.FC<{ [registeredTooltips] ); - // Handle Ctrl key press for all platforms - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - const isRevealKey = event.ctrlKey; - - // Key pressed: { key, ctrlKey, isRevealKey } - - if (isRevealKey && !showAllTooltips) { - setShowAllTooltips(true); - // Show all registered tooltips - registeredTooltips.forEach(({ show }) => { - show(); - }); - } - }; - - const handleKeyUp = (event: KeyboardEvent) => { - const wasRevealKey = !event.ctrlKey; - - if (wasRevealKey && showAllTooltips) { - setShowAllTooltips(false); - // Hide all registered tooltips (except pinned ones) - registeredTooltips.forEach(({ hide }) => { - hide(); - }); - } - }; - - document.addEventListener('keydown', handleKeyDown); - document.addEventListener('keyup', handleKeyUp); - - return () => { - document.removeEventListener('keydown', handleKeyDown); - document.removeEventListener('keyup', handleKeyUp); - }; - }, [showAllTooltips, registeredTooltips]); - const value: TooltipGlobalManagerContextType = { showAllTooltips, registerTooltip, diff --git a/src/inputs/RadioGroup.styles.css b/src/inputs/RadioGroup.styles.css index 00fd4b7..158114c 100644 --- a/src/inputs/RadioGroup.styles.css +++ b/src/inputs/RadioGroup.styles.css @@ -33,21 +33,21 @@ } .schema-container .radio-option.selected { - background: var(--schema-accent); - color: var(--schema-text-inverse); + background: #dbeafe; + color: #1e40af; } .schema-container .radio-option.selected:hover:not(.disabled) { - background: #2563eb; - color: var(--schema-text-inverse); + background: #bfdbfe; + color: #1e40af; } .schema-container .radio-option:active:not(.disabled) { - background: #dbeafe; + background: #f0f9ff; } .schema-container .radio-option.selected:active:not(.disabled) { - background: #1d4ed8; + background: #93c5fd; } .schema-container .radio-option.disabled { diff --git a/src/property/ExamplesPanel.styles.css b/src/property/ExamplesPanel.styles.css index 024250c..187909d 100644 --- a/src/property/ExamplesPanel.styles.css +++ b/src/property/ExamplesPanel.styles.css @@ -67,7 +67,7 @@ .schema-container .wrap-toggle-button:hover, .schema-container .copy-button:hover { - background: var(--schema-surface-hover); + background: rgba(59, 130, 246, 0.1); color: var(--schema-text); } @@ -130,7 +130,7 @@ } .schema-container .example-copy-button:hover { - background: var(--schema-surface-hover); + background: rgba(59, 130, 246, 0.1); color: var(--schema-text); } diff --git a/src/property/ExamplesPanel.tsx b/src/property/ExamplesPanel.tsx index 61c75cc..1d4e719 100644 --- a/src/property/ExamplesPanel.tsx +++ b/src/property/ExamplesPanel.tsx @@ -23,6 +23,7 @@ interface ExamplesPanelProps { rootSchema: JsonSchema; propertyPath: string[]; onCopy?: (text: string, element: HTMLElement) => void; + options?: { defaultExampleLanguage?: 'json' | 'yaml' | 'toml' }; } type Format = 'json' | 'yaml' | 'toml'; @@ -32,59 +33,28 @@ const ExamplesPanel: React.FC = ({ rootSchema, propertyPath, onCopy, + options, }) => { const [selectedFormat, setSelectedFormat] = useState(() => { + const defaultLanguage = options?.defaultExampleLanguage || 'yaml'; if (typeof window === 'undefined') { - return 'toml'; + return defaultLanguage; } try { return ( - (localStorage.getItem('deckard-examples-format') as Format) || 'toml' + (localStorage.getItem('deckard-examples-format') as Format) || + defaultLanguage ); } catch { - return 'toml'; + return defaultLanguage; } }); - // Sync format across all ExamplesPanel instances + // Reset to default when options change useEffect(() => { - if (typeof window === 'undefined') { - return; - } - - const handleStorageChange = (e: StorageEvent) => { - if (e.key === 'deckard-examples-format' && e.newValue) { - const newFormat = e.newValue as Format; - if (newFormat && ['json', 'yaml', 'toml'].includes(newFormat)) { - setSelectedFormat(newFormat); - } - } - }; - - // Listen for storage changes from other tabs/components - window.addEventListener('storage', handleStorageChange); - - // Listen for custom events from same page (since storage event doesn't fire for same page) - const handleFormatChange = (e: CustomEvent) => { - const newFormat = e.detail as Format; - if (newFormat && ['json', 'yaml', 'toml'].includes(newFormat)) { - setSelectedFormat(newFormat); - } - }; - - window.addEventListener( - 'deckard-format-change', - handleFormatChange as EventListener - ); - - return () => { - window.removeEventListener('storage', handleStorageChange); - window.removeEventListener( - 'deckard-format-change', - handleFormatChange as EventListener - ); - }; - }, []); + const defaultLanguage = options?.defaultExampleLanguage || 'yaml'; + setSelectedFormat(defaultLanguage); + }, [options?.defaultExampleLanguage]); const [highlighter, setHighlighter] = useState(null); const [highlighterError, setHighlighterError] = useState(false); const [lineWrap, setLineWrap] = useState(false); @@ -272,17 +242,8 @@ const ExamplesPanel: React.FC = ({ value={selectedFormat} onChange={format => { setSelectedFormat(format); - if (typeof window !== 'undefined') { - try { - localStorage.setItem('deckard-examples-format', format); - // Dispatch custom event to sync other panels on same page - window.dispatchEvent( - new CustomEvent('deckard-format-change', { detail: format }) - ); - } catch { - // Ignore localStorage errors - } - } + // Note: We don't save to localStorage here as this should only affect + // the current example, not the global default setting }} name="format-selector" size="md" @@ -292,7 +253,7 @@ const ExamplesPanel: React.FC = ({ size="xs" className={`wrap-toggle-button ${lineWrap ? 'active' : ''}`} onClick={() => setLineWrap(!lineWrap)} - title={lineWrap ? 'Disable line wrap' : 'Enable line wrap'} + title={lineWrap ? 'Disable line wrap.' : 'Enable line wrap.'} > {lineWrap ? ( @@ -327,7 +288,7 @@ const ExamplesPanel: React.FC = ({ }); } }} - title="Copy this example" + title="Copy this example." > diff --git a/src/property/PropertyDetails.tsx b/src/property/PropertyDetails.tsx index cbeb271..447ff38 100644 --- a/src/property/PropertyDetails.tsx +++ b/src/property/PropertyDetails.tsx @@ -7,7 +7,7 @@ import { JsonSchema, PropertyState, } from '../types'; -import { getConstraints, getUnsupportedFeatures } from '../utils.js'; +import { getConstraints, getUnsupportedFeatures } from '../utils'; import { Badge, BadgeGroup, @@ -26,6 +26,8 @@ interface PropertyDetailsProps { toggleProperty?: (key: string) => void; focusedProperty?: string | null; onFocusChange?: (propertyKey: string | null) => void; + options?: { defaultExampleLanguage?: 'json' | 'yaml' | 'toml' }; + searchQuery?: string; } const PropertyDetails: React.FC = ({ @@ -37,6 +39,8 @@ const PropertyDetails: React.FC = ({ toggleProperty, focusedProperty, onFocusChange, + options, + searchQuery, }) => { const constraints = getConstraints(property.schema); @@ -73,14 +77,14 @@ const PropertyDetails: React.FC = ({ @@ -125,6 +129,8 @@ const PropertyDetails: React.FC = ({ toggleProperty={toggleProperty} focusedProperty={focusedProperty} onFocusChange={onFocusChange} + options={options} + searchQuery={searchQuery} /> )} @@ -140,6 +146,8 @@ const PropertyDetails: React.FC = ({ focusedProperty={focusedProperty} onFocusChange={onFocusChange} propertyPath={property.path} + options={options} + searchQuery={searchQuery} /> )} @@ -248,7 +256,17 @@ const PropertyDetails: React.FC = ({ { - onCopy(JSON.stringify(value), document.createElement('div')); + const copyValue = + typeof value === 'string' + ? value + : value === null + ? 'null' + : value === undefined + ? 'undefined' + : typeof value === 'boolean' || typeof value === 'number' + ? String(value) + : JSON.stringify(value); + onCopy(copyValue, document.createElement('div')); }} badgeVariant="enum" badgeSize="xs" diff --git a/src/property/PropertyField.tsx b/src/property/PropertyField.tsx index a0d795a..2a3b72f 100644 --- a/src/property/PropertyField.tsx +++ b/src/property/PropertyField.tsx @@ -1,16 +1,47 @@ -import React, { useCallback, useState, useEffect } from 'react'; +import React, { useCallback, useState, useEffect, useMemo } from 'react'; import { FaChevronDown, FaChevronRight, FaLink, FaMapPin, + FaSearch, } from 'react-icons/fa'; import { SchemaProperty, PropertyState, JsonSchema } from '../types'; -import { getSchemaType, hasExamples } from '../utils.js'; +import { getSchemaType, hasExamples } from '../utils'; import ExamplesPanel from './ExamplesPanel'; import { Badge, Tooltip } from '../components'; import PropertyDetails from './PropertyDetails'; +const getTypeDescription = (type: string): string => { + switch (type.toLowerCase()) { + case 'string': + return 'Text data - can contain letters, numbers, and symbols.'; + case 'number': + return 'Numeric data - integers and decimal numbers.'; + case 'integer': + return 'Whole number data - no decimal places allowed.'; + case 'boolean': + return 'True or false value.'; + case 'array': + return 'List of items - can contain multiple values.'; + case 'object': + return 'Structured data with properties and values.'; + case 'null': + return 'Represents no value or empty data.'; + case 'oneof': + return 'Must match exactly one of the defined schemas.'; + case 'anyof': + return 'Must match at least one of the defined schemas.'; + case 'enum': + return 'Must be one of a specific set of predefined values.'; + default: + if (type.includes('|')) { + return 'Can be one of multiple data types.'; + } + return 'The expected data type for this property.'; + } +}; + interface PropertyFieldProps { property: SchemaProperty; propertyKey: string; @@ -26,6 +57,8 @@ interface PropertyFieldProps { _toggleProperty?: (key: string) => void; focusedProperty?: string | null; onFocusChange?: (propertyKey: string | null) => void; + options?: { defaultExampleLanguage?: 'json' | 'yaml' | 'toml' }; + searchQuery?: string; } const PropertyField: React.FC = ({ @@ -43,9 +76,14 @@ const PropertyField: React.FC = ({ _toggleProperty, focusedProperty, onFocusChange, + options, + searchQuery, }) => { const [isActiveRoute, setIsActiveRoute] = useState(false); + // Check if schema is valid + const hasValidSchema = property.schema != null; + // Check if current URL hash matches this property's link useEffect(() => { if (typeof window === 'undefined') { @@ -70,40 +108,49 @@ const PropertyField: React.FC = ({ const handleHeaderClick = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); - if (collapsible) { + if (collapsible && hasValidSchema) { onToggle(); } // Set this property as focused when clicking header onFocusChange?.(propertyKey); }, - [collapsible, onToggle, propertyKey, onFocusChange] + [collapsible, onToggle, propertyKey, onFocusChange, hasValidSchema] ); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - if (collapsible) { + if (collapsible && hasValidSchema) { onToggle(); } } }, - [collapsible, onToggle] + [collapsible, onToggle, hasValidSchema] ); const handleLinkClick = useCallback( (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - // Expand the row if it's collapsed (but don't collapse if expanded) - if (!state.expanded) { - onToggle(); + // Allow middle-click and cmd+click to navigate without expanding + if (e.button === 1 || e.ctrlKey || e.metaKey) { + return; // Let browser handle navigation } + // Set this property as focused when clicking link onFocusChange?.(propertyKey); - onCopyLink(propertyKey, e.currentTarget as HTMLElement); + // Expand the field if it's not already expanded + if (collapsible && hasValidSchema && !state.expanded) { + onToggle(); + } }, - [onCopyLink, propertyKey, state.expanded, onToggle, onFocusChange] + [ + onFocusChange, + propertyKey, + collapsible, + hasValidSchema, + state.expanded, + onToggle, + ] ); const handleFieldClick = useCallback( @@ -135,16 +182,50 @@ const PropertyField: React.FC = ({ [propertyKey, onFocusChange] ); + // Generate the anchor URL for the link + const linkHref = useMemo(() => { + if (typeof window === 'undefined') return '#'; + const anchor = `#${propertyKey}`; + return `${window.location.origin}${window.location.pathname}${anchor}`; + }, [propertyKey]); + const renderPropertyDetails = useCallback(() => { - return ; - }, [property, onCopy]); + return ( + + ); + }, [ + property, + onCopy, + rootSchema, + onCopyLink, + _propertyStates, + _toggleProperty, + focusedProperty, + onFocusChange, + options, + searchQuery, + ]); const propertyClasses = [ 'property', property.depth > 0 ? 'nested-property' : '', state.expanded ? 'expanded' : '', property.depth > 0 ? `depth-${Math.min(property.depth, 3)}` : '', - includeExamples && hasExamples(property.schema) ? 'has-examples' : '', + includeExamples && hasValidSchema && hasExamples(property.schema) + ? 'has-examples' + : '', + !hasValidSchema ? 'invalid-schema' : '', ] .filter(Boolean) .join(' '); @@ -157,25 +238,28 @@ const PropertyField: React.FC = ({ onClick={handleFieldClick} >
{isActiveRoute && ( @@ -187,22 +271,59 @@ const PropertyField: React.FC = ({ )} - - + + {collapsible && hasValidSchema && ( + + )} + {!hasValidSchema && ( + + ⚠️ + + )}
@@ -212,15 +333,8 @@ const PropertyField: React.FC = ({ > {property.schema.__isPatternProperty ? ( - Pattern Property -
- This represents dynamic field names. Unlike fixed property - names, this can match multiple different field names in - your data. -
- } + title="Pattern property" + content="This represents dynamic field names. Unlike fixed property names, this can match multiple different field names in your data." placement="top" > @@ -232,25 +346,38 @@ const PropertyField: React.FC = ({ )} {schemaType && ( - - {schemaType} - + + + {schemaType} + + )} {property.required && ( - + required )} - {!state.expanded && property.schema.description && ( - - {property.schema.description} + {!state.expanded && + hasValidSchema && + property.schema.description && ( + + {property.schema.description} + + )} + {!state.expanded && !hasValidSchema && ( + + Schema data is undefined or invalid )}
- {state.expanded && ( + {state.expanded && hasValidSchema && (
= ({ className="schema-details-right" data-debug="examples-panel-container" > - + {rootSchema ? ( + + ) : ( +
+
+ + Root schema unavailable +
+
+ )}
) : ( diff --git a/src/property/PropertyRow.styles.css b/src/property/PropertyRow.styles.css index 16bac74..2905b6a 100644 --- a/src/property/PropertyRow.styles.css +++ b/src/property/PropertyRow.styles.css @@ -118,11 +118,32 @@ font-weight: 500; } -/* Override cursor for tooltipped pattern property badges */ +/* Override cursor for tooltipped badges */ .schema-container .tooltip-trigger .badge-pattern { cursor: help; } +.schema-container .tooltip-trigger .badge-type { + cursor: help; +} + +.schema-container .tooltip-trigger .badge-required { + cursor: help; +} + +/* Ensure tooltipped buttons maintain proper cursor */ +.schema-container .tooltip-trigger .row-button { + cursor: pointer; +} + +.schema-container .tooltip-trigger .row-button.link-button { + cursor: pointer; +} + +.schema-container .tooltip-trigger .row-button.expand-button { + cursor: pointer; +} + .schema-container .pattern-code { background: rgba(59, 130, 246, 0.08); border: 1px solid rgba(59, 130, 246, 0.2); @@ -403,7 +424,7 @@ } .schema-container .code-control-button:hover { - background: var(--schema-surface-hover); + background: rgba(59, 130, 246, 0.1); color: var(--schema-text); border-color: var(--schema-accent); } @@ -582,3 +603,141 @@ outline: none; box-shadow: none; } + +/* ===== SEARCH HIT INDICATOR ===== */ + +.schema-container .search-hit-indicator { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.25rem; + height: 1.25rem; + color: #00bcd4; + font-size: 0.875rem; + opacity: 1; + transition: all var(--schema-transition); +} + +.schema-container .search-hit-indicator:hover { + opacity: 1; + color: #00acc1; +} + +.schema-container .search-hit-indicator svg { + color: #00bcd4; + fill: currentColor; + stroke: currentColor; +} + +.schema-container .search-hit-indicator:hover svg { + color: #00acc1; + fill: currentColor; + stroke: currentColor; +} + +.schema-container .property-controls .row-button.search-hit-indicator { + background: transparent; + border: none; + cursor: help; + padding: 0; + border-radius: 0; + color: #06b6d4; +} + +.schema-container .property-controls .row-button.search-hit-indicator:hover { + background: transparent; + transform: none; + border-color: transparent; + color: #0891b2; +} + +.schema-container .row-button.search-hit-indicator:focus { + outline: none; + box-shadow: none; +} + +/* ===== PARENT SEARCH HIT INDICATOR ===== */ + +.schema-container .search-hit-indicator.parent-match { + color: #26c6da; + opacity: 0.6; +} + +.schema-container .search-hit-indicator.parent-match:hover { + color: #00acc1; + opacity: 0.8; +} + +.schema-container .search-hit-indicator.parent-match svg { + color: #26c6da; + fill: currentColor; + stroke: currentColor; +} + +.schema-container .search-hit-indicator.parent-match:hover svg { + color: #00acc1; + fill: currentColor; + stroke: currentColor; +} + +/* ===== LINK BUTTON ANCHOR STYLES ===== */ + +/* ===== EXAMPLES PANEL UNAVAILABLE ===== */ + +.schema-container .examples-panel-unavailable { + display: flex; + align-items: center; + justify-content: center; + padding: var(--schema-space-lg); + background: var(--schema-surface-hover); + border: 1px solid var(--schema-border-subtle); + border-radius: var(--schema-radius-sm); + min-height: 4rem; +} + +.schema-container .examples-panel-message { + display: flex; + align-items: center; + gap: var(--schema-space-sm); + color: var(--schema-text-muted); + font-size: var(--schema-text-sm); + text-align: center; +} + +.schema-container .examples-panel-icon { + font-size: var(--schema-text-base); + font-weight: 600; + color: var(--schema-text-secondary); + opacity: 0.7; +} + +/* ===== INVALID SCHEMA STATES ===== */ + +.schema-container .property.invalid-schema { + background: rgba(239, 68, 68, 0.05); + border: 1px solid rgba(239, 68, 68, 0.2); +} + +.schema-container .invalid-schema-indicator { + display: inline-flex; + align-items: center; + justify-content: center; + font-size: var(--schema-text-sm); + color: #ef4444; + cursor: help; + opacity: 0.8; +} + +.schema-container .invalid-schema-description { + color: #ef4444 !important; + font-style: italic; +} + +.schema-container .property.invalid-schema .property-name { + color: var(--schema-text-muted); + opacity: 0.7; +} + +.schema-container .property.invalid-schema:hover { + background: rgba(239, 68, 68, 0.08); +} diff --git a/src/property/PropertyRow.tsx b/src/property/PropertyRow.tsx index dbc6102..e12be75 100644 --- a/src/property/PropertyRow.tsx +++ b/src/property/PropertyRow.tsx @@ -4,16 +4,47 @@ import { FaLink, FaChevronDown, FaChevronRight, + FaSearch, } from 'react-icons/fa'; import './PropertyRow.styles.css'; import { SchemaProperty, PropertyState, JsonSchema } from '../types'; -import { getSchemaType, hasExamples, extractProperties } from '../utils.js'; +import { getSchemaType, hasExamples, extractProperties } from '../utils'; import ExamplesPanel from './ExamplesPanel'; import { Badge, Tooltip } from '../components'; import Row from '../Row'; import PropertyDetails from './PropertyDetails'; import Rows from '../Rows'; +const getTypeDescription = (type: string): string => { + switch (type.toLowerCase()) { + case 'string': + return 'Text data - can contain letters, numbers, and symbols.'; + case 'number': + return 'Numeric data - integers and decimal numbers.'; + case 'integer': + return 'Whole number data - no decimal places allowed.'; + case 'boolean': + return 'True or false value.'; + case 'array': + return 'List of items - can contain multiple values.'; + case 'object': + return 'Structured data with properties and values.'; + case 'null': + return 'Represents no value or empty data.'; + case 'oneof': + return 'Must match exactly one of the defined schemas.'; + case 'anyof': + return 'Must match at least one of the defined schemas.'; + case 'enum': + return 'Must be one of a specific set of predefined values.'; + default: + if (type.includes('|')) { + return 'Can be one of multiple data types.'; + } + return 'The expected data type for this property.'; + } +}; + interface PropertyRowProps { property: SchemaProperty; propertyKey: string; @@ -29,6 +60,8 @@ interface PropertyRowProps { toggleProperty?: (key: string) => void; focusedProperty?: string | null; onFocusChange?: (propertyKey: string | null) => void; + options?: { defaultExampleLanguage?: 'json' | 'yaml' | 'toml' }; + searchQuery?: string; } const PropertyRow: React.FC = ({ @@ -46,16 +79,17 @@ const PropertyRow: React.FC = ({ toggleProperty, focusedProperty, onFocusChange, + options, + searchQuery, }) => { const [isActiveRoute, setIsActiveRoute] = useState(false); + // Check if schema is valid + const hasValidSchema = property.schema != null; + // Extract nested properties if this property has them const nestedProperties = useMemo(() => { - if ( - (!property.schema.properties && !property.schema.patternProperties) || - !rootSchema - ) - return []; + if (!rootSchema || !hasValidSchema) return []; const props = extractProperties( property.schema, property.path, @@ -67,7 +101,13 @@ const PropertyRow: React.FC = ({ return props.sort((a: SchemaProperty, b: SchemaProperty) => a.name.localeCompare(b.name) ); - }, [property.schema, property.path, property.depth, rootSchema]); + }, [ + property.schema, + property.path, + property.depth, + rootSchema, + hasValidSchema, + ]); const hasNestedProperties = nestedProperties.length > 0; @@ -95,42 +135,58 @@ const PropertyRow: React.FC = ({ const handleHeaderClick = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); - if (collapsible) { + if (collapsible && hasValidSchema) { onToggle(); } // Set this property as focused when clicking header onFocusChange?.(propertyKey); }, - [collapsible, onToggle, propertyKey, onFocusChange] + [collapsible, onToggle, propertyKey, onFocusChange, hasValidSchema] ); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - if (collapsible) { + if (collapsible && hasValidSchema) { onToggle(); } } }, - [collapsible, onToggle] + [collapsible, onToggle, hasValidSchema] ); const handleLinkClick = useCallback( (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - // Expand the row if it's collapsed (but don't collapse if expanded) - if (!state.expanded) { - onToggle(); + // Allow middle-click and cmd+click to navigate without expanding + if (e.button === 1 || e.ctrlKey || e.metaKey) { + return; // Let browser handle navigation } + // Set this property as focused when clicking link onFocusChange?.(propertyKey); - onCopyLink(propertyKey, e.currentTarget as HTMLElement); + // Expand the field if it's not already expanded + if (collapsible && hasValidSchema && !state.expanded) { + onToggle(); + } }, - [onCopyLink, propertyKey, state.expanded, onToggle, onFocusChange] + [ + onFocusChange, + propertyKey, + collapsible, + hasValidSchema, + state.expanded, + onToggle, + ] ); + // Generate the anchor URL for the link + const linkHref = useMemo(() => { + if (typeof window === 'undefined') return '#'; + const anchor = `#${propertyKey}`; + return `${window.location.origin}${window.location.pathname}${anchor}`; + }, [propertyKey]); + const handleFieldClick = useCallback( (e: React.MouseEvent) => { // Don't interfere with button clicks or text selection @@ -171,6 +227,8 @@ const PropertyRow: React.FC = ({ toggleProperty={toggleProperty} focusedProperty={focusedProperty} onFocusChange={onFocusChange} + options={options} + searchQuery={searchQuery} /> ); }, [ @@ -182,6 +240,8 @@ const PropertyRow: React.FC = ({ toggleProperty, focusedProperty, onFocusChange, + options, + searchQuery, ]); const propertyClasses = [ @@ -189,7 +249,10 @@ const PropertyRow: React.FC = ({ property.depth > 0 ? 'nested-property' : '', state.expanded ? 'expanded' : '', property.depth > 0 ? `depth-${Math.min(property.depth, 3)}` : '', - includeExamples && hasExamples(property.schema) ? 'has-examples' : '', + includeExamples && hasValidSchema && hasExamples(property.schema) + ? 'has-examples' + : '', + !hasValidSchema ? 'invalid-schema' : '', ] .filter(Boolean) .join(' '); @@ -205,45 +268,97 @@ const PropertyRow: React.FC = ({ className="row-header-container property-header-container" onClick={handleHeaderClick} onKeyDown={handleKeyDown} - tabIndex={collapsible ? 0 : -1} - role={collapsible ? 'button' : undefined} - aria-expanded={state.expanded} + tabIndex={collapsible && hasValidSchema ? 0 : -1} + role={collapsible && hasValidSchema ? 'button' : undefined} + aria-expanded={hasValidSchema ? state.expanded : undefined} aria-label={ - state.expanded + hasValidSchema && state.expanded ? `Collapse ${property.name}` - : `Expand ${property.name}` + : hasValidSchema + ? `Expand ${property.name}` + : `${property.name} - Invalid schema` } style={{ - cursor: collapsible ? 'pointer' : 'default', + cursor: collapsible && hasValidSchema ? 'pointer' : 'default', }} >
- {isActiveRoute && ( - + {searchQuery && state.matchesSearch && ( + - + )} - - + + {isActiveRoute ? : } + + + {collapsible && hasValidSchema && ( + + + + )} + {!hasValidSchema && ( + + ⚠️ + + )}
@@ -253,15 +368,8 @@ const PropertyRow: React.FC = ({ > {property.schema.__isPatternProperty ? ( - Pattern Property -
- This represents dynamic field names. Unlike fixed property - names, this can match multiple different field names in - your data. -
- } + title="Pattern property" + content="This represents dynamic field names. Unlike fixed property names, this can match multiple different field names in your data." placement="top" > @@ -273,25 +381,44 @@ const PropertyRow: React.FC = ({ )} {schemaType && ( - - {schemaType} - + + + {schemaType} + + )} {property.required && ( - - required - + + + required + + )} - {!state.expanded && property.schema.description && ( - - {property.schema.description} + {!state.expanded && + hasValidSchema && + property.schema.description && ( + + {property.schema.description} + + )} + {!state.expanded && !hasValidSchema && ( + + Schema data is undefined or invalid )} - {state.expanded && ( + {state.expanded && hasValidSchema && ( <>
= ({ className="schema-details-right" data-debug="examples-panel-container" > - + {rootSchema ? ( + + ) : ( +
+
+ + Root schema unavailable +
+
+ )}
) : ( @@ -342,6 +479,8 @@ const PropertyRow: React.FC = ({ toggleProperty={toggleProperty} focusedProperty={focusedProperty} onFocusChange={onFocusChange} + options={options} + searchQuery={searchQuery} /> )} diff --git a/src/types.ts b/src/types.ts index c0654d9..9649715 100644 --- a/src/types.ts +++ b/src/types.ts @@ -101,6 +101,7 @@ export interface DeckardOptions { collapsible?: boolean; autoExpand?: boolean; theme?: 'light' | 'dark' | 'auto'; + defaultExampleLanguage?: 'json' | 'yaml' | 'toml'; } export interface ExampleFormats { @@ -113,6 +114,8 @@ export interface PropertyState { expanded: boolean; hasDetails: boolean; matchesSearch: boolean; + isDirectMatch?: boolean; + hasNestedMatches?: boolean; } export interface SearchState { diff --git a/tsconfig.json b/tsconfig.json index 79078b5..0e8c48b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], + "types": ["vitest/globals"], "module": "ESNext", "skipLibCheck": true, diff --git a/vite.config.ts b/vite.config.ts index 0fec068..5ae86de 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -18,6 +18,7 @@ export default defineConfig({ }), ], build: { + emptyOutDir: false, lib: { entry: resolve(__dirname, 'src/index.ts'), name: 'DeckardReact', diff --git a/vitest.config.ts b/vitest.config.ts index 6e7a09d..b7064b7 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -8,10 +8,10 @@ export default defineConfig({ setupFiles: ['./src/__tests__/setup.ts'], globals: true, include: ['src/**/*.{test,spec}.{ts,tsx}'], - exclude: ['node_modules', 'dist', 'src/__tests__/setup.ts', 'src/__tests__/test-utils.tsx'], + exclude: ['node_modules', 'dist'], coverage: { provider: 'v8', - exclude: ['src/**/*.d.ts', 'src/index.ts'], + exclude: ['src/**/*.d.ts', 'src/index.ts', 'src/__tests__/setup.ts'], }, }, });