From e9d6c917ff676f13b0d885167823cd67619f2e42 Mon Sep 17 00:00:00 2001 From: creativoma Date: Sun, 19 Apr 2026 10:50:59 +0200 Subject: [PATCH 1/3] feat: add JSON schema widget support with checkbox and radio inputs --- CHANGELOG.md | 14 + package.json | 2 + .../core/src/__tests__/widget.test.ts | 114 +++++ packages/@visual-json/core/src/index.ts | 2 + packages/@visual-json/core/src/types.ts | 1 + packages/@visual-json/core/src/widget.ts | 23 + .../src/__tests__/checkbox-input.test.tsx | 48 +++ .../react/src/__tests__/radio-input.test.tsx | 64 +++ .../@visual-json/react/src/checkbox-input.tsx | 34 ++ packages/@visual-json/react/src/form-view.tsx | 29 +- .../@visual-json/react/src/radio-input.tsx | 58 +++ pnpm-lock.yaml | 395 +++++++++++++++++- vitest.config.ts | 3 +- 13 files changed, 777 insertions(+), 10 deletions(-) create mode 100644 packages/@visual-json/core/src/__tests__/widget.test.ts create mode 100644 packages/@visual-json/core/src/widget.ts create mode 100644 packages/@visual-json/react/src/__tests__/checkbox-input.test.tsx create mode 100644 packages/@visual-json/react/src/__tests__/radio-input.test.tsx create mode 100644 packages/@visual-json/react/src/checkbox-input.tsx create mode 100644 packages/@visual-json/react/src/radio-input.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 11ee373..ae9f6be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## Unreleased + + +### New Features + +- **JSON Schema widget support** — `@visual-json/core` now exports `getWidgetType`, which maps a node type and its `JsonSchemaProperty` to a UI widget (`checkbox`, `radio`, `select`, or `input`). Boolean nodes always render as a checkbox; string/number nodes with `enum` render as radio buttons (≤ 3 options) or a select dropdown (> 3 options). The `x-widget` extension field overrides the default for any node (#25) +- **`CheckboxInput` and `RadioInput` components** — New React components in `@visual-json/react` for rendering boolean and enum fields. `CheckboxInput` replaces the previous true/false enum dropdown for booleans. `RadioInput` renders mutually exclusive options for small enums + +### Improvements + +- **Smarter enum rendering** — Enum fields with more than 3 options now render as a ` onValueChange(e.target.checked ? "true" : "false")} + onClick={(e) => e.stopPropagation()} + style={{ + accentColor: "var(--vj-accent, #007acc)", + cursor: "pointer", + width: 13, + height: 13, + }} + /> + + ); +} diff --git a/packages/@visual-json/react/src/form-view.tsx b/packages/@visual-json/react/src/form-view.tsx index 90797af..fd0f6e3 100644 --- a/packages/@visual-json/react/src/form-view.tsx +++ b/packages/@visual-json/react/src/form-view.tsx @@ -6,6 +6,7 @@ import { removeNode, getPropertySchema, resolveRef, + getWidgetType, type TreeNode, type JsonSchemaProperty, type JsonSchema, @@ -13,6 +14,8 @@ import { import { useStudio } from "./context"; import { Breadcrumbs } from "./breadcrumbs"; import { EnumInput } from "./enum-input"; +import { RadioInput } from "./radio-input"; +import { CheckboxInput } from "./checkbox-input"; import { getDisplayKey, getVisibleNodes } from "@internal/ui"; import { deleteSelectedNodes, computeSelectAllIds } from "./selection-utils"; import { @@ -587,7 +590,7 @@ function renderEditInput( inputRef: React.RefObject, valueColor: string, ) { - const hasEnumValues = propSchema?.enum && propSchema.enum.length > 0; + const widgetType = getWidgetType(node.type, propSchema); const inputStyle: React.CSSProperties = { background: "none", @@ -600,19 +603,31 @@ function renderEditInput( color: valueColor, }; - if (node.type === "boolean") { + if (widgetType === "checkbox") { return ( - } + /> + ); + } + + if (widgetType === "radio") { + const options = propSchema?.enum + ? propSchema.enum.map(String) + : ["true", "false"]; + return ( + } - inputStyle={inputStyle} /> ); } - if (hasEnumValues && propSchema?.enum) { + if (widgetType === "select" && propSchema?.enum) { return ( void; + inputRef?: RefObject; +} + +export function RadioInput({ + options, + value, + onValueChange, + inputRef, +}: RadioInputProps) { + return ( +
e.stopPropagation()} + > + {options.map((option, i) => ( + + ))} +
+ ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff328ab..85f163f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@eslint/js': specifier: ^10.0.1 version: 10.0.1(eslint@10.0.1(jiti@2.6.1)) + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) eslint: specifier: ^10.0.1 version: 10.0.1(jiti@2.6.1) @@ -20,6 +23,9 @@ importers: husky: specifier: ^9.1.7 version: 9.1.7 + jsdom: + specifier: ^29.0.2 + version: 29.0.2(@noble/hashes@1.8.0) lint-staged: specifier: ^16.2.7 version: 16.2.7 @@ -40,7 +46,7 @@ importers: version: 8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3) vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.0.2(@noble/hashes@1.8.0))(lightningcss@1.31.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(yaml@2.8.2) apps/vscode: dependencies: @@ -465,6 +471,21 @@ packages: resolution: {integrity: sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA==} hasBin: true + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -587,6 +608,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -606,6 +631,46 @@ packages: '@borewit/text-codec@0.2.1': resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.0': + resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@dotenvx/dotenvx@1.52.0': resolution: {integrity: sha512-CaQcc8JvtzQhUSm9877b6V4Tb7HCotkcyud9X2YwdqtQKwgljkMRwU96fVYKnzN3V0Hj74oP7Es+vZ0mS+Aa1w==} hasBin: true @@ -970,6 +1035,15 @@ packages: resolution: {integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@floating-ui/core@1.7.4': resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} @@ -2410,6 +2484,25 @@ packages: '@tailwindcss/postcss@4.2.0': resolution: {integrity: sha512-u6YBacGpOm/ixPfKqfgrJEjMfrYmPD7gEFRoygS/hnQaRtV0VCBdpkx5Ouw9pnaLRwwlgGCuJw8xLpaR0hOrQg==} + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -2423,6 +2516,9 @@ packages: '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -2809,6 +2905,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -2830,6 +2930,9 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.1: resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} engines: {node: '>= 0.4'} @@ -2881,6 +2984,9 @@ packages: just-bash: optional: true + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -3100,6 +3206,10 @@ packages: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -3112,6 +3222,10 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} @@ -3124,6 +3238,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} @@ -3196,6 +3313,9 @@ packages: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dotenv@17.3.1: resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} engines: {node: '>=12'} @@ -3692,6 +3812,10 @@ packages: resolution: {integrity: sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA==} engines: {node: '>=16.9.0'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -3839,6 +3963,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -3901,6 +4028,15 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@29.0.2: + resolution: {integrity: sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -4090,6 +4226,10 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lru-cache@11.3.5: + resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -4102,6 +4242,10 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -4172,6 +4316,9 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -4544,6 +4691,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -4697,6 +4847,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -4769,6 +4923,9 @@ packages: peerDependencies: react: ^19.2.4 + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -4933,6 +5090,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -5210,6 +5371,9 @@ packages: peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} @@ -5290,6 +5454,14 @@ packages: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -5429,6 +5601,10 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -5686,6 +5862,10 @@ packages: typescript: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} @@ -5693,6 +5873,18 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -5743,6 +5935,13 @@ packages: resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} engines: {node: '>=20'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -5840,6 +6039,26 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.2 + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -6007,6 +6226,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/runtime@7.29.2': {} + '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 @@ -6044,6 +6265,34 @@ snapshots: '@borewit/text-codec@0.2.1': {} + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + '@dotenvx/dotenvx@1.52.0': dependencies: commander: 11.1.0 @@ -6255,6 +6504,10 @@ snapshots: '@eslint/core': 1.1.0 levn: 0.4.1 + '@exodus/bytes@1.15.0(@noble/hashes@1.8.0)': + optionalDependencies: + '@noble/hashes': 1.8.0 + '@floating-ui/core@1.7.4': dependencies: '@floating-ui/utils': 0.2.10 @@ -7669,6 +7922,27 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.2.0 + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.29.2 + '@testing-library/dom': 10.4.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 @@ -7686,6 +7960,8 @@ snapshots: '@types/argparse@1.0.38': {} + '@types/aria-query@5.0.4': {} + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -8212,6 +8488,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} ansis@4.2.0: {} @@ -8228,6 +8506,10 @@ snapshots: dependencies: tslib: 2.8.1 + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + aria-query@5.3.1: {} assertion-error@2.0.1: {} @@ -8260,6 +8542,10 @@ snapshots: optionalDependencies: just-bash: 2.10.2 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -8460,18 +8746,32 @@ snapshots: mdn-data: 2.12.2 source-map-js: 1.2.1 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + cssesc@3.0.0: {} csstype@3.2.3: {} data-uri-to-buffer@4.0.1: {} + data-urls@7.0.0(@noble/hashes@1.8.0): + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@1.8.0) + transitivePeerDependencies: + - '@noble/hashes' + de-indent@1.0.2: {} debug@4.4.3: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 @@ -8521,6 +8821,8 @@ snapshots: diff@8.0.3: {} + dom-accessibility-api@0.5.16: {} + dotenv@17.3.1: {} dunder-proto@1.0.1: @@ -9201,6 +9503,12 @@ snapshots: hono@4.12.0: {} + html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0): + dependencies: + '@exodus/bytes': 1.15.0(@noble/hashes@1.8.0) + transitivePeerDependencies: + - '@noble/hashes' + html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} @@ -9307,6 +9615,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} is-reference@3.0.3: @@ -9350,6 +9660,32 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@29.0.2(@noble/hashes@1.8.0): + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) + '@exodus/bytes': 1.15.0(@noble/hashes@1.8.0) + css-tree: 3.2.1 + data-urls: 7.0.0(@noble/hashes@1.8.0) + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@1.8.0) + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.3.5 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.25.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@1.8.0) + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -9525,6 +9861,8 @@ snapshots: longest-streak@3.1.0: {} + lru-cache@11.3.5: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -9537,6 +9875,8 @@ snapshots: dependencies: react: 19.2.4 + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -9714,6 +10054,8 @@ snapshots: mdn-data@2.12.2: {} + mdn-data@2.27.1: {} + media-typer@1.1.0: {} merge-descriptors@2.0.0: {} @@ -10260,6 +10602,10 @@ snapshots: dependencies: entities: 6.0.1 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + parseurl@1.3.3: {} path-browserify@1.0.1: {} @@ -10379,6 +10725,12 @@ snapshots: prettier@3.8.1: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -10507,6 +10859,8 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 + react-is@17.0.2: {} + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): dependencies: react: 19.2.4 @@ -10741,6 +11095,10 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} scule@1.3.0: {} @@ -11132,6 +11490,8 @@ snapshots: react: 19.2.4 use-sync-external-store: 1.6.0(react@19.2.4) + symbol-tree@3.2.4: {} + tagged-tag@1.0.0: {} tailwind-merge@3.5.0: {} @@ -11206,6 +11566,14 @@ snapshots: dependencies: tldts: 7.0.23 + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.23 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} trim-lines@3.0.1: {} @@ -11348,6 +11716,8 @@ snapshots: undici-types@7.18.2: {} + undici@7.25.0: {} + unicorn-magic@0.3.0: {} unified@11.0.5: @@ -11551,7 +11921,7 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(yaml@2.8.2): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@29.0.2(@noble/hashes@1.8.0))(lightningcss@1.31.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) @@ -11576,6 +11946,7 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 25.3.0 + jsdom: 29.0.2(@noble/hashes@1.8.0) transitivePeerDependencies: - jiti - less @@ -11607,10 +11978,26 @@ snapshots: optionalDependencies: typescript: 5.9.3 + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + web-namespaces@2.0.1: {} web-streams-polyfill@3.3.3: {} + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1(@noble/hashes@1.8.0): + dependencies: + '@exodus/bytes': 1.15.0(@noble/hashes@1.8.0) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + which@2.0.2: dependencies: isexe: 2.0.0 @@ -11653,6 +12040,10 @@ snapshots: is-wsl: 3.1.1 powershell-utils: 0.1.0 + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + y18n@5.0.8: {} yallist@3.1.1: {} diff --git a/vitest.config.ts b/vitest.config.ts index 26e501f..3cbd7e3 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - include: ["packages/**/src/**/*.test.ts"], + include: ["packages/**/src/**/*.test.{ts,tsx}"], + environmentMatchGlobs: [["packages/@visual-json/react/**", "jsdom"]], }, }); From 1cd8aead18b897c2869637c977913c4419d9002d Mon Sep 17 00:00:00 2001 From: creativoma Date: Sun, 19 Apr 2026 10:51:03 +0200 Subject: [PATCH 2/3] refactor: extract editor samples data to separate module --- examples/react/app/editor.tsx | 434 +------------------------------- examples/react/app/samples.ts | 453 ++++++++++++++++++++++++++++++++++ 2 files changed, 461 insertions(+), 426 deletions(-) create mode 100644 examples/react/app/samples.ts diff --git a/examples/react/app/editor.tsx b/examples/react/app/editor.tsx index fa7cabb..629145f 100644 --- a/examples/react/app/editor.tsx +++ b/examples/react/app/editor.tsx @@ -3,6 +3,7 @@ import { useState, useCallback, useEffect, useRef } from "react"; import type { JsonValue, JsonSchema } from "@visual-json/core"; import { resolveSchema } from "@visual-json/core"; +import { samples } from "./samples"; import { JsonEditor, DiffView } from "@visual-json/react"; import { Button } from "@/components/ui/button"; import { @@ -41,429 +42,6 @@ const VIEW_MODES: { id: ViewMode; label: string }[] = [ { id: "raw", label: "Raw" }, ]; -const samples: { name: string; filename: string; data: JsonValue }[] = [ - { - name: "package.json", - filename: "package.json", - data: { - name: "my-app", - version: "1.0.0", - private: true, - scripts: { - dev: "next dev", - build: "next build", - start: "next start", - lint: "next lint", - }, - dependencies: { - next: "^15.0.0", - react: "^19.0.0", - "react-dom": "^19.0.0", - }, - devDependencies: { - "@types/react": "^19.0.0", - typescript: "^5.6.0", - eslint: "^9.0.0", - }, - engines: { node: ">=18" }, - }, - }, - { - name: "OpenAPI spec", - filename: "openapi.json", - data: { - openapi: "3.1.0", - info: { - title: "Tasks API", - version: "1.0.0", - description: "A simple task management API", - contact: { name: "API Support", email: "support@example.com" }, - license: { name: "MIT", url: "https://opensource.org/licenses/MIT" }, - }, - servers: [ - { - url: "https://api.example.com/v1", - description: "Production", - }, - { - url: "https://staging-api.example.com/v1", - description: "Staging", - }, - ], - paths: { - "/tasks": { - get: { - summary: "List tasks", - operationId: "listTasks", - tags: ["tasks"], - parameters: [ - { - name: "status", - in: "query", - required: false, - schema: { type: "string", enum: ["open", "closed", "all"] }, - }, - { - name: "limit", - in: "query", - required: false, - schema: { type: "integer", default: 20, maximum: 100 }, - }, - ], - responses: { - "200": { - description: "A list of tasks", - content: { - "application/json": { - schema: { - type: "array", - items: { $ref: "#/components/schemas/Task" }, - }, - }, - }, - }, - }, - }, - post: { - summary: "Create a task", - operationId: "createTask", - tags: ["tasks"], - requestBody: { - required: true, - content: { - "application/json": { - schema: { $ref: "#/components/schemas/TaskInput" }, - }, - }, - }, - responses: { - "201": { - description: "Task created", - content: { - "application/json": { - schema: { $ref: "#/components/schemas/Task" }, - }, - }, - }, - "422": { description: "Validation error" }, - }, - }, - }, - "/tasks/{taskId}": { - get: { - summary: "Get a task", - operationId: "getTask", - tags: ["tasks"], - parameters: [ - { - name: "taskId", - in: "path", - required: true, - schema: { type: "string", format: "uuid" }, - }, - ], - responses: { - "200": { - description: "Task details", - content: { - "application/json": { - schema: { $ref: "#/components/schemas/Task" }, - }, - }, - }, - "404": { description: "Task not found" }, - }, - }, - patch: { - summary: "Update a task", - operationId: "updateTask", - tags: ["tasks"], - parameters: [ - { - name: "taskId", - in: "path", - required: true, - schema: { type: "string", format: "uuid" }, - }, - ], - requestBody: { - required: true, - content: { - "application/json": { - schema: { $ref: "#/components/schemas/TaskInput" }, - }, - }, - }, - responses: { - "200": { - description: "Task updated", - content: { - "application/json": { - schema: { $ref: "#/components/schemas/Task" }, - }, - }, - }, - }, - }, - delete: { - summary: "Delete a task", - operationId: "deleteTask", - tags: ["tasks"], - parameters: [ - { - name: "taskId", - in: "path", - required: true, - schema: { type: "string", format: "uuid" }, - }, - ], - responses: { - "204": { description: "Task deleted" }, - }, - }, - }, - }, - components: { - schemas: { - Task: { - type: "object", - required: ["id", "title", "status", "createdAt"], - properties: { - id: { type: "string", format: "uuid" }, - title: { type: "string", example: "Write docs" }, - description: { type: "string", nullable: true }, - status: { - type: "string", - enum: ["open", "in_progress", "closed"], - }, - priority: { type: "string", enum: ["low", "medium", "high"] }, - assignee: { type: "string", nullable: true }, - tags: { type: "array", items: { type: "string" } }, - createdAt: { type: "string", format: "date-time" }, - updatedAt: { type: "string", format: "date-time" }, - }, - }, - TaskInput: { - type: "object", - required: ["title"], - properties: { - title: { type: "string", minLength: 1, maxLength: 200 }, - description: { type: "string", nullable: true }, - status: { - type: "string", - enum: ["open", "in_progress", "closed"], - }, - priority: { type: "string", enum: ["low", "medium", "high"] }, - assignee: { type: "string", nullable: true }, - tags: { type: "array", items: { type: "string" } }, - }, - }, - }, - securitySchemes: { - bearerAuth: { - type: "http", - scheme: "bearer", - bearerFormat: "JWT", - }, - }, - }, - security: [{ bearerAuth: [] }], - }, - }, - { - name: "json-render spec", - filename: "spec.json", - data: { - root: "card_1", - elements: { - card_1: { - type: "Card", - props: { title: "User Profile" }, - children: ["stack_1"], - }, - stack_1: { - type: "Stack", - props: { gap: 16 }, - children: ["avatar_1", "heading_1", "text_1", "button_group"], - }, - avatar_1: { - type: "Avatar", - props: { - src: "https://example.com/avatar.jpg", - alt: "Jane Doe", - size: "lg", - }, - }, - heading_1: { type: "Heading", props: { level: 2, text: "Jane Doe" } }, - text_1: { - type: "Text", - props: { text: "Senior Software Engineer at Acme Corp." }, - }, - button_group: { - type: "ButtonGroup", - children: ["btn_edit", "btn_share"], - }, - btn_edit: { - type: "Button", - props: { label: "Edit Profile", variant: "default" }, - }, - btn_share: { - type: "Button", - props: { label: "Share", variant: "outline" }, - }, - }, - }, - }, - { - name: "json-render (nested)", - filename: "dashboard.json", - data: { - type: "Stack", - props: { direction: "vertical", gap: "lg" }, - state: { - activeTab: "overview", - notifications: true, - darkMode: false, - count: 42, - }, - children: [ - { - type: "Stack", - props: { direction: "horizontal", gap: "md", align: "center" }, - children: [ - { type: "Heading", props: { text: "Dashboard", level: "h1" } }, - { - type: "Text", - props: { text: "Manage your workspace", variant: "muted" }, - }, - ], - }, - { - type: "Tabs", - props: { - tabs: [ - { label: "Overview", value: "overview" }, - { label: "Settings", value: "settings" }, - ], - value: { $bindState: "/activeTab" }, - }, - }, - { - type: "Stack", - props: { direction: "vertical", gap: "md" }, - visible: [{ $state: "/activeTab", eq: "overview" }], - children: [ - { - type: "Grid", - props: { columns: 3, gap: "md" }, - children: [ - { - type: "Card", - props: { title: "Active Users" }, - children: [ - { - type: "Metric", - props: { label: "Users", value: 1284, change: "+12%" }, - }, - ], - }, - { - type: "Card", - props: { title: "Revenue" }, - children: [ - { - type: "Metric", - props: { - label: "Revenue", - value: "$48,200", - change: "+8.2%", - }, - }, - ], - }, - { - type: "Card", - props: { title: "Orders" }, - children: [ - { - type: "Metric", - props: { - label: "Orders", - value: { $state: "/count" }, - change: "-3%", - }, - }, - ], - }, - ], - }, - { - type: "Card", - props: { title: "Recent Activity" }, - children: [ - { - type: "Stack", - props: { direction: "vertical", gap: "sm" }, - children: [ - { - type: "Text", - props: { - text: "Alice deployed v2.4.0 to production", - }, - }, - { - type: "Text", - props: { - text: "Bob opened PR #312: Fix auth redirect", - }, - }, - { - type: "Text", - props: { text: "Carol added 3 new test cases" }, - }, - ], - }, - ], - }, - ], - }, - { - type: "Card", - props: { title: "Preferences" }, - visible: [{ $state: "/activeTab", eq: "settings" }], - children: [ - { - type: "Stack", - props: { direction: "vertical", gap: "md" }, - children: [ - { - type: "Switch", - props: { - label: "Email notifications", - checked: { $bindState: "/notifications" }, - }, - }, - { - type: "Switch", - props: { - label: "Dark mode", - checked: { $bindState: "/darkMode" }, - }, - }, - { - type: "Button", - props: { label: "Save preferences", variant: "primary" }, - on: { press: { action: "confetti" } }, - }, - ], - }, - ], - }, - ], - }, - }, -]; - export function Editor({ defaultSidebarOpen, }: { @@ -472,7 +50,9 @@ export function Editor({ const [activeSample, setActiveSample] = useState(samples[0].filename); const [jsonValue, setJsonValue] = useState(samples[0].data); const [viewMode, setViewMode] = useState("tree"); - const [schema, setSchema] = useState(null); + const [schema, setSchema] = useState( + samples[0].schema ?? null, + ); const [filename, setFilename] = useState(samples[0].filename); const [originalJson, setOriginalJson] = useState(samples[0].data); const [isDragOver, setIsDragOver] = useState(false); @@ -492,6 +72,8 @@ export function Editor({ const fileInputRef = useRef(null); useEffect(() => { + const currentSample = samples.find((s) => s.filename === filename); + if (currentSample?.schema !== undefined) return; let cancelled = false; resolveSchema(jsonValue, filename).then((s) => { if (!cancelled) setSchema(s); @@ -524,7 +106,7 @@ export function Editor({ setFilename(fname); setJsonValue(sample.data); setOriginalJson(structuredClone(sample.data)); - setSchema(null); + setSchema(sample.schema ?? null); setRawText(JSON.stringify(sample.data, null, 2)); setRawError(null); } @@ -859,7 +441,7 @@ export function Editor({ onChange={(e) => setPasteText(e.target.value)} placeholder="Paste your JSON here..." spellCheck={false} - className="min-h-[200px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm font-mono resize-none outline-none" + className="min-h-50 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm font-mono resize-none outline-none" /> diff --git a/examples/react/app/samples.ts b/examples/react/app/samples.ts new file mode 100644 index 0000000..503e2e2 --- /dev/null +++ b/examples/react/app/samples.ts @@ -0,0 +1,453 @@ +import type { JsonValue, JsonSchema } from "@visual-json/core"; + +export type Sample = { + name: string; + filename: string; + data: JsonValue; + schema?: JsonSchema; +}; + +export const samples: Sample[] = [ + { + name: "Schema widgets", + filename: "widget-demo.json", + data: { + theme: "light", + priority: "high", + status: "open", + active: true, + }, + schema: { + type: "object", + properties: { + theme: { type: "string", enum: ["light", "dark", "system"] }, + priority: { type: "string", enum: ["low", "medium", "high"] }, + status: { + type: "string", + enum: ["open", "in_progress", "done", "cancelled"], + }, + active: { type: "boolean" }, + }, + }, + }, + { + name: "package.json", + filename: "package.json", + data: { + name: "my-app", + version: "1.0.0", + private: true, + scripts: { + dev: "next dev", + build: "next build", + start: "next start", + lint: "next lint", + }, + dependencies: { + next: "^15.0.0", + react: "^19.0.0", + "react-dom": "^19.0.0", + }, + devDependencies: { + "@types/react": "^19.0.0", + typescript: "^5.6.0", + eslint: "^9.0.0", + }, + engines: { node: ">=18" }, + }, + }, + { + name: "OpenAPI spec", + filename: "openapi.json", + data: { + openapi: "3.1.0", + info: { + title: "Tasks API", + version: "1.0.0", + description: "A simple task management API", + contact: { name: "API Support", email: "support@example.com" }, + license: { name: "MIT", url: "https://opensource.org/licenses/MIT" }, + }, + servers: [ + { + url: "https://api.example.com/v1", + description: "Production", + }, + { + url: "https://staging-api.example.com/v1", + description: "Staging", + }, + ], + paths: { + "/tasks": { + get: { + summary: "List tasks", + operationId: "listTasks", + tags: ["tasks"], + parameters: [ + { + name: "status", + in: "query", + required: false, + schema: { type: "string", enum: ["open", "closed", "all"] }, + }, + { + name: "limit", + in: "query", + required: false, + schema: { type: "integer", default: 20, maximum: 100 }, + }, + ], + responses: { + "200": { + description: "A list of tasks", + content: { + "application/json": { + schema: { + type: "array", + items: { $ref: "#/components/schemas/Task" }, + }, + }, + }, + }, + }, + }, + post: { + summary: "Create a task", + operationId: "createTask", + tags: ["tasks"], + requestBody: { + required: true, + content: { + "application/json": { + schema: { $ref: "#/components/schemas/TaskInput" }, + }, + }, + }, + responses: { + "201": { + description: "Task created", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Task" }, + }, + }, + }, + "422": { description: "Validation error" }, + }, + }, + }, + "/tasks/{taskId}": { + get: { + summary: "Get a task", + operationId: "getTask", + tags: ["tasks"], + parameters: [ + { + name: "taskId", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + }, + ], + responses: { + "200": { + description: "Task details", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Task" }, + }, + }, + }, + "404": { description: "Task not found" }, + }, + }, + patch: { + summary: "Update a task", + operationId: "updateTask", + tags: ["tasks"], + parameters: [ + { + name: "taskId", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + }, + ], + requestBody: { + required: true, + content: { + "application/json": { + schema: { $ref: "#/components/schemas/TaskInput" }, + }, + }, + }, + responses: { + "200": { + description: "Task updated", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Task" }, + }, + }, + }, + }, + }, + delete: { + summary: "Delete a task", + operationId: "deleteTask", + tags: ["tasks"], + parameters: [ + { + name: "taskId", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + }, + ], + responses: { + "204": { description: "Task deleted" }, + }, + }, + }, + }, + components: { + schemas: { + Task: { + type: "object", + required: ["id", "title", "status", "createdAt"], + properties: { + id: { type: "string", format: "uuid" }, + title: { type: "string", example: "Write docs" }, + description: { type: "string", nullable: true }, + status: { + type: "string", + enum: ["open", "in_progress", "closed"], + }, + priority: { type: "string", enum: ["low", "medium", "high"] }, + assignee: { type: "string", nullable: true }, + tags: { type: "array", items: { type: "string" } }, + createdAt: { type: "string", format: "date-time" }, + updatedAt: { type: "string", format: "date-time" }, + }, + }, + TaskInput: { + type: "object", + required: ["title"], + properties: { + title: { type: "string", minLength: 1, maxLength: 200 }, + description: { type: "string", nullable: true }, + status: { + type: "string", + enum: ["open", "in_progress", "closed"], + }, + priority: { type: "string", enum: ["low", "medium", "high"] }, + assignee: { type: "string", nullable: true }, + tags: { type: "array", items: { type: "string" } }, + }, + }, + }, + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + }, + security: [{ bearerAuth: [] }], + }, + }, + { + name: "json-render spec", + filename: "spec.json", + data: { + root: "card_1", + elements: { + card_1: { + type: "Card", + props: { title: "User Profile" }, + children: ["stack_1"], + }, + stack_1: { + type: "Stack", + props: { gap: 16 }, + children: ["avatar_1", "heading_1", "text_1", "button_group"], + }, + avatar_1: { + type: "Avatar", + props: { + src: "https://example.com/avatar.jpg", + alt: "Jane Doe", + size: "lg", + }, + }, + heading_1: { type: "Heading", props: { level: 2, text: "Jane Doe" } }, + text_1: { + type: "Text", + props: { text: "Senior Software Engineer at Acme Corp." }, + }, + button_group: { + type: "ButtonGroup", + children: ["btn_edit", "btn_share"], + }, + btn_edit: { + type: "Button", + props: { label: "Edit Profile", variant: "default" }, + }, + btn_share: { + type: "Button", + props: { label: "Share", variant: "outline" }, + }, + }, + }, + }, + { + name: "json-render (nested)", + filename: "dashboard.json", + data: { + type: "Stack", + props: { direction: "vertical", gap: "lg" }, + state: { + activeTab: "overview", + notifications: true, + darkMode: false, + count: 42, + }, + children: [ + { + type: "Stack", + props: { direction: "horizontal", gap: "md", align: "center" }, + children: [ + { type: "Heading", props: { text: "Dashboard", level: "h1" } }, + { + type: "Text", + props: { text: "Manage your workspace", variant: "muted" }, + }, + ], + }, + { + type: "Tabs", + props: { + tabs: [ + { label: "Overview", value: "overview" }, + { label: "Settings", value: "settings" }, + ], + value: { $bindState: "/activeTab" }, + }, + }, + { + type: "Stack", + props: { direction: "vertical", gap: "md" }, + visible: [{ $state: "/activeTab", eq: "overview" }], + children: [ + { + type: "Grid", + props: { columns: 3, gap: "md" }, + children: [ + { + type: "Card", + props: { title: "Active Users" }, + children: [ + { + type: "Metric", + props: { label: "Users", value: 1284, change: "+12%" }, + }, + ], + }, + { + type: "Card", + props: { title: "Revenue" }, + children: [ + { + type: "Metric", + props: { + label: "Revenue", + value: "$48,200", + change: "+8.2%", + }, + }, + ], + }, + { + type: "Card", + props: { title: "Orders" }, + children: [ + { + type: "Metric", + props: { + label: "Orders", + value: { $state: "/count" }, + change: "-3%", + }, + }, + ], + }, + ], + }, + { + type: "Card", + props: { title: "Recent Activity" }, + children: [ + { + type: "Stack", + props: { direction: "vertical", gap: "sm" }, + children: [ + { + type: "Text", + props: { + text: "Alice deployed v2.4.0 to production", + }, + }, + { + type: "Text", + props: { + text: "Bob opened PR #312: Fix auth redirect", + }, + }, + { + type: "Text", + props: { text: "Carol added 3 new test cases" }, + }, + ], + }, + ], + }, + ], + }, + { + type: "Card", + props: { title: "Preferences" }, + visible: [{ $state: "/activeTab", eq: "settings" }], + children: [ + { + type: "Stack", + props: { direction: "vertical", gap: "md" }, + children: [ + { + type: "Switch", + props: { + label: "Email notifications", + checked: { $bindState: "/notifications" }, + }, + }, + { + type: "Switch", + props: { + label: "Dark mode", + checked: { $bindState: "/darkMode" }, + }, + }, + { + type: "Button", + props: { label: "Save preferences", variant: "primary" }, + on: { press: { action: "confetti" } }, + }, + ], + }, + ], + }, + ], + }, + }, +]; From 1288a0c643dfef71d5368de96939cb925b49a736 Mon Sep 17 00:00:00 2001 From: creativoma Date: Sun, 19 Apr 2026 10:59:27 +0200 Subject: [PATCH 3/3] fix: scope radio button name to field path to prevent cross-field exclusion --- .../react/src/__tests__/radio-input.test.tsx | 23 ++++++++++++++++--- packages/@visual-json/react/src/form-view.tsx | 1 + .../@visual-json/react/src/radio-input.tsx | 4 +++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/@visual-json/react/src/__tests__/radio-input.test.tsx b/packages/@visual-json/react/src/__tests__/radio-input.test.tsx index 6ddb407..ffe1208 100644 --- a/packages/@visual-json/react/src/__tests__/radio-input.test.tsx +++ b/packages/@visual-json/react/src/__tests__/radio-input.test.tsx @@ -10,7 +10,12 @@ describe("RadioInput", () => { it("renders all options", () => { const { getByLabelText } = render( - {}} />, + {}} + />, ); expect(getByLabelText("low")).toBeTruthy(); expect(getByLabelText("medium")).toBeTruthy(); @@ -19,7 +24,12 @@ describe("RadioInput", () => { it("marks the current value as checked", () => { const { getByLabelText } = render( - {}} />, + {}} + />, ); expect((getByLabelText("medium") as HTMLInputElement).checked).toBe(true); expect((getByLabelText("low") as HTMLInputElement).checked).toBe(false); @@ -30,6 +40,7 @@ describe("RadioInput", () => { const onValueChange = vi.fn(); const { getByLabelText } = render( { it("all radios share the same name (mutual exclusion)", () => { const { getByLabelText } = render( - {}} />, + {}} + />, ); const names = options.map( (o) => (getByLabelText(o) as HTMLInputElement).name, @@ -53,6 +69,7 @@ describe("RadioInput", () => { const ref = { current: null }; const { getByLabelText } = render( {}} diff --git a/packages/@visual-json/react/src/form-view.tsx b/packages/@visual-json/react/src/form-view.tsx index fd0f6e3..0d9d35a 100644 --- a/packages/@visual-json/react/src/form-view.tsx +++ b/packages/@visual-json/react/src/form-view.tsx @@ -619,6 +619,7 @@ function renderEditInput( : ["true", "false"]; return ( void; @@ -8,6 +9,7 @@ interface RadioInputProps { } export function RadioInput({ + id, options, value, onValueChange, @@ -38,7 +40,7 @@ export function RadioInput({ onValueChange(option)}