this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: extract builder branch rules editor

+907 -452
+108 -6
bun.lock
··· 33 33 }, 34 34 "devDependencies": { 35 35 "@tailwindcss/postcss": "4.2.2", 36 + "@testing-library/dom": "^10.4.1", 37 + "@testing-library/react": "^16.3.2", 38 + "@types/jsdom": "^28.0.1", 36 39 "@types/node": "25.6.0", 37 40 "@types/react": "19.2.14", 38 41 "@types/react-dom": "19.2.3", 39 42 "eslint": "9.39.1", 40 43 "eslint-config-next": "16.2.3", 44 + "jsdom": "^29.0.2", 41 45 "prettier": "^3.8.2", 42 46 "prisma": "7.7.0", 43 47 "tailwindcss": "4.2.2", ··· 47 51 }, 48 52 "packages": { 49 53 "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], 54 + 55 + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.10", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-02OhhkKtgNRuicQ/nF3TRnGsxL9wp0r3Y7VlKWyOHHGmGyvXv03y+PnymU8FKFJMTjIr1Bk8U2g1HWSLrpAHww=="], 56 + 57 + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.0.9", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-r3ElRr7y8ucyN2KdICwGsmj19RoN13CLCa/pvGydghWK6ZzeKQ+TcDjVdtEZz2ElpndM5jXw//B9CEee0mWnVg=="], 58 + 59 + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], 50 60 51 61 "@auth/core": ["@auth/core@0.41.1", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "jose": "^6.0.6", "oauth4webapi": "^3.3.0", "preact": "10.24.3", "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^7.0.7" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw=="], 52 62 ··· 86 96 87 97 "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], 88 98 99 + "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], 100 + 101 + "@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="], 102 + 103 + "@csstools/css-calc": ["@csstools/css-calc@3.2.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w=="], 104 + 105 + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.0", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.2.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ=="], 106 + 107 + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], 108 + 109 + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.3", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg=="], 110 + 111 + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], 112 + 89 113 "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], 90 114 91 115 "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], ··· 125 149 "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], 126 150 127 151 "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], 152 + 153 + "@exodus/bytes": ["@exodus/bytes@1.15.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="], 128 154 129 155 "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], 130 156 ··· 362 388 363 389 "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", "tailwindcss": "4.2.2" } }, "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ=="], 364 390 391 + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], 392 + 393 + "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], 394 + 365 395 "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], 396 + 397 + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], 366 398 367 399 "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], 368 400 ··· 371 403 "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], 372 404 373 405 "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], 406 + 407 + "@types/jsdom": ["@types/jsdom@28.0.1", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0", "undici-types": "^7.21.0" } }, "sha512-GJq2QE4TAZ5ajSoCasn5DOFm8u1mI3tIFvM5tIq3W5U/RTB6gsHwc6Yhpl91X9VSDOUVblgXmG+2+sSvFQrdlw=="], 374 408 375 409 "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], 376 410 ··· 387 421 "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], 388 422 389 423 "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], 424 + 425 + "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], 390 426 391 427 "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], 392 428 ··· 458 494 459 495 "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], 460 496 461 - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 497 + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 498 + 499 + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], 462 500 463 501 "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], 464 502 465 503 "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], 466 504 467 - "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], 505 + "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], 468 506 469 507 "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], 470 508 ··· 502 540 503 541 "better-result": ["better-result@2.8.2", "", {}, "sha512-YOf0VSj5nUPI27doTtXF+BBnsiRq3qY7avHqfIWnppxTLGyvkLq1QV2RTxkwoZwJ60ywLfZ0raFF4J/G886i7A=="], 504 542 543 + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], 544 + 505 545 "brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="], 506 546 507 547 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], ··· 568 608 569 609 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 570 610 611 + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], 612 + 571 613 "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], 572 614 573 615 "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], 616 + 617 + "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="], 574 618 575 619 "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], 576 620 ··· 583 627 "date-fns-jalali": ["date-fns-jalali@4.1.0-0", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="], 584 628 585 629 "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 630 + 631 + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], 586 632 587 633 "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], 588 634 ··· 610 656 611 657 "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], 612 658 659 + "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], 660 + 613 661 "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], 614 662 615 663 "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], ··· 623 671 "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], 624 672 625 673 "enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="], 674 + 675 + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], 626 676 627 677 "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], 628 678 ··· 782 832 783 833 "hono": ["hono@4.12.12", "", {}, "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q=="], 784 834 835 + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], 836 + 785 837 "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], 786 838 787 839 "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], ··· 842 894 843 895 "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], 844 896 897 + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], 898 + 845 899 "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], 846 900 847 901 "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], ··· 875 929 "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], 876 930 877 931 "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], 932 + 933 + "jsdom": ["jsdom@29.0.2", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.5", "@asamuzakjp/dom-selector": "^7.0.6", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.7", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.24.5", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w=="], 878 934 879 935 "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], 880 936 ··· 930 986 931 987 "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], 932 988 933 - "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], 989 + "lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="], 934 990 935 991 "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], 936 992 937 993 "lucide-react": ["lucide-react@1.8.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw=="], 994 + 995 + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], 938 996 939 997 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], 940 998 ··· 972 1030 973 1031 "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], 974 1032 1033 + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], 1034 + 975 1035 "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], 976 1036 977 1037 "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], ··· 1104 1164 1105 1165 "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], 1106 1166 1167 + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], 1168 + 1107 1169 "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], 1108 1170 1109 1171 "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], ··· 1158 1220 1159 1221 "prettier": ["prettier@3.8.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q=="], 1160 1222 1161 - "pretty-format": ["pretty-format@3.8.0", "", {}, "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="], 1223 + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], 1162 1224 1163 1225 "prisma": ["prisma@7.7.0", "", { "dependencies": { "@prisma/config": "7.7.0", "@prisma/dev": "0.24.3", "@prisma/engines": "7.7.0", "@prisma/studio-core": "0.27.3", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-HlgwRBt1uEFB9LStHL4HLYDvoi4BNu1rYA0hPG0zCAEyK9SaZBqp7E5Rjpc3Qh8Lex/ye/svoHZ0OWoFNhWxuQ=="], 1164 1226 ··· 1182 1244 1183 1245 "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], 1184 1246 1185 - "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], 1247 + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], 1186 1248 1187 1249 "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], 1188 1250 ··· 1230 1292 1231 1293 "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], 1232 1294 1295 + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], 1296 + 1233 1297 "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], 1234 1298 1235 1299 "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], ··· 1302 1366 1303 1367 "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], 1304 1368 1369 + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], 1370 + 1305 1371 "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], 1306 1372 1307 1373 "tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="], ··· 1312 1378 1313 1379 "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], 1314 1380 1381 + "tldts": ["tldts@7.0.28", "", { "dependencies": { "tldts-core": "^7.0.28" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw=="], 1382 + 1383 + "tldts-core": ["tldts-core@7.0.28", "", {}, "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ=="], 1384 + 1315 1385 "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], 1386 + 1387 + "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], 1388 + 1389 + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], 1316 1390 1317 1391 "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], 1318 1392 ··· 1340 1414 1341 1415 "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], 1342 1416 1343 - "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], 1417 + "undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="], 1418 + 1419 + "undici-types": ["undici-types@7.25.0", "", {}, "sha512-AXNgS1Byr27fTI+2bsPEkV9CxkT8H6xNyRI68b3TatlZo3RkzlqQBLL+w7SmGPVpokjHbcuNVQUWE7FRTg+LRA=="], 1344 1420 1345 1421 "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], 1346 1422 ··· 1372 1448 1373 1449 "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], 1374 1450 1451 + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], 1452 + 1453 + "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], 1454 + 1455 + "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], 1456 + 1457 + "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], 1458 + 1375 1459 "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 1376 1460 1377 1461 "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], ··· 1389 1473 "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], 1390 1474 1391 1475 "xlsx": ["xlsx@0.18.5", "", { "dependencies": { "adler-32": "~1.3.0", "cfb": "~1.2.1", "codepage": "~1.15.0", "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" }, "bin": { "xlsx": "bin/xlsx.njs" } }, "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ=="], 1476 + 1477 + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], 1478 + 1479 + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], 1392 1480 1393 1481 "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], 1394 1482 ··· 1440 1528 1441 1529 "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 1442 1530 1531 + "@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], 1532 + 1443 1533 "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], 1444 1534 1445 1535 "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], ··· 1447 1537 "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], 1448 1538 1449 1539 "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], 1540 + 1541 + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 1450 1542 1451 1543 "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], 1452 1544 ··· 1454 1546 1455 1547 "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], 1456 1548 1549 + "eslint-plugin-jsx-a11y/aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], 1550 + 1457 1551 "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], 1458 1552 1459 1553 "is-bun-module/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], 1460 1554 1555 + "jsdom/parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], 1556 + 1461 1557 "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], 1462 1558 1463 1559 "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], ··· 1466 1562 1467 1563 "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], 1468 1564 1565 + "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], 1566 + 1469 1567 "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], 1470 1568 1471 1569 "pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], 1570 + 1571 + "preact-render-to-string/pretty-format": ["pretty-format@3.8.0", "", {}, "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="], 1572 + 1573 + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], 1472 1574 1473 1575 "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], 1474 1576
+224
components/builder/branch-rules-editor.test.tsx
··· 1 + import { describe, expect, test } from "bun:test"; 2 + import { cleanup, fireEvent, render } from "@testing-library/react"; 3 + import { useState } from "react"; 4 + 5 + import { BranchRulesEditor } from "@/components/builder/branch-rules-editor"; 6 + import { I18nProvider } from "@/components/i18n-provider"; 7 + import { 8 + getDefaultBlockConfig, 9 + type BlockConfig, 10 + type SerializedBlock, 11 + } from "@/lib/blocks"; 12 + import type { BranchValidationIssue } from "@/lib/branching"; 13 + import { type BranchRuleDraft } from "@/lib/form-builder-drafts"; 14 + import type { BuilderBlock } from "@/lib/form-types"; 15 + import { installTestDom } from "@/test/install-dom"; 16 + 17 + function createBlock( 18 + overrides: Partial<BuilderBlock> & 19 + Pick<BuilderBlock, "id" | "type" | "position">, 20 + ): BuilderBlock { 21 + const now = new Date("2026-04-14T12:00:00.000Z"); 22 + 23 + return { 24 + formId: "form-1", 25 + title: "", 26 + description: "", 27 + required: false, 28 + createdAt: now, 29 + updatedAt: now, 30 + config: getDefaultBlockConfig(overrides.type), 31 + ...overrides, 32 + } as BuilderBlock; 33 + } 34 + 35 + function BranchRulesEditorHarness({ 36 + allBlocks, 37 + blockDraft, 38 + branchValidationIssues = [], 39 + }: { 40 + allBlocks: BuilderBlock[]; 41 + blockDraft: BuilderBlock; 42 + branchValidationIssues?: BranchValidationIssue[]; 43 + }) { 44 + const [currentBlockDraft, setCurrentBlockDraft] = useState(blockDraft); 45 + const [branchRulesDraft, setBranchRulesDraft] = useState<BranchRuleDraft[]>( 46 + [], 47 + ); 48 + 49 + function updateConfig(patch: Record<string, unknown>) { 50 + setCurrentBlockDraft( 51 + (current) => 52 + ({ 53 + ...current, 54 + config: { 55 + ...(current.config as Record<string, unknown>), 56 + ...patch, 57 + } as BlockConfig, 58 + }) as SerializedBlock, 59 + ); 60 + } 61 + 62 + return ( 63 + <I18nProvider locale="en" messages={{}}> 64 + <BranchRulesEditor 65 + allBlocks={allBlocks} 66 + blockDraft={currentBlockDraft} 67 + branchRulesDraft={branchRulesDraft} 68 + branchValidationIssues={branchValidationIssues} 69 + setBranchRulesDraft={setBranchRulesDraft} 70 + updateConfig={updateConfig} 71 + /> 72 + <pre data-testid="branch-rules-draft"> 73 + {JSON.stringify(branchRulesDraft)} 74 + </pre> 75 + <pre data-testid="block-config"> 76 + {JSON.stringify(currentBlockDraft.config)} 77 + </pre> 78 + </I18nProvider> 79 + ); 80 + } 81 + 82 + describe("BranchRulesEditor", () => { 83 + test("adds an agreement branch rule using the first available target and agreed default", () => { 84 + const restoreDom = installTestDom(); 85 + const blockDraft = createBlock({ 86 + id: "agreement", 87 + type: "AGREEMENT", 88 + position: 0, 89 + title: "Consent", 90 + }); 91 + const targetBlock = createBlock({ 92 + id: "follow-up", 93 + type: "SHORT_TEXT", 94 + position: 1, 95 + title: "Why?", 96 + }); 97 + 98 + const view = render( 99 + <BranchRulesEditorHarness 100 + allBlocks={[blockDraft, targetBlock]} 101 + blockDraft={blockDraft} 102 + />, 103 + ); 104 + 105 + fireEvent.click( 106 + view.getByRole("button", { name: "builder.addBranchRule" }), 107 + ); 108 + 109 + const branchRules = JSON.parse( 110 + view.getByTestId("branch-rules-draft").textContent ?? "[]", 111 + ) as Array<Record<string, string | null>>; 112 + const blockConfig = JSON.parse( 113 + view.getByTestId("block-config").textContent ?? "{}", 114 + ) as Record<string, unknown>; 115 + 116 + expect(branchRules.length).toBe(1); 117 + expect(branchRules[0]?.operator).toBe("equals"); 118 + expect(branchRules[0]?.value).toBe("agreed"); 119 + expect(branchRules[0]?.targetBlockId).toBe("follow-up"); 120 + expect(JSON.stringify(blockConfig.branchRules)).toBe( 121 + JSON.stringify([ 122 + { 123 + operator: "equals", 124 + value: "agreed", 125 + targetBlockId: "follow-up", 126 + }, 127 + ]), 128 + ); 129 + 130 + cleanup(); 131 + restoreDom(); 132 + }); 133 + 134 + test("shows no-target guidance and disables adding branch rules when there are no future targets", () => { 135 + const restoreDom = installTestDom(); 136 + const blockDraft = createBlock({ 137 + id: "last-question", 138 + type: "SHORT_TEXT", 139 + position: 0, 140 + title: "Last question", 141 + }); 142 + 143 + const view = render( 144 + <BranchRulesEditorHarness 145 + allBlocks={[blockDraft]} 146 + blockDraft={blockDraft} 147 + />, 148 + ); 149 + 150 + const addButton = view.getByRole("button", { 151 + name: "builder.addBranchRule", 152 + }); 153 + 154 + expect(addButton.hasAttribute("disabled")).toBe(true); 155 + expect(view.getByText("builder.branchingNoTargets") !== null).toBe(true); 156 + 157 + cleanup(); 158 + restoreDom(); 159 + }); 160 + 161 + test("renders branch issues only for the selected block", () => { 162 + const restoreDom = installTestDom(); 163 + const blockDraft = createBlock({ 164 + id: "question-1", 165 + type: "SINGLE_CHOICE", 166 + position: 0, 167 + title: "Choose one", 168 + config: { 169 + ...getDefaultBlockConfig("SINGLE_CHOICE"), 170 + options: ["A", "B"], 171 + }, 172 + }); 173 + const nextBlock = createBlock({ 174 + id: "question-2", 175 + type: "SHORT_TEXT", 176 + position: 1, 177 + title: "Next", 178 + }); 179 + const issues: BranchValidationIssue[] = [ 180 + { 181 + severity: "blocker", 182 + code: "invalid-target", 183 + message: "missing target", 184 + blockId: "question-1", 185 + ruleIndex: 0, 186 + targetBlockId: "missing-block", 187 + }, 188 + { 189 + severity: "warning", 190 + code: "partial-choice-coverage", 191 + message: "partial coverage", 192 + blockId: "question-1", 193 + }, 194 + { 195 + severity: "warning", 196 + code: "fragile-text-match", 197 + message: "other block warning", 198 + blockId: "question-2", 199 + }, 200 + ]; 201 + 202 + const view = render( 203 + <BranchRulesEditorHarness 204 + allBlocks={[blockDraft, nextBlock]} 205 + blockDraft={blockDraft} 206 + branchValidationIssues={issues} 207 + />, 208 + ); 209 + 210 + expect( 211 + view.getByText("branching.issueMessages.invalid-target") !== null, 212 + ).toBe(true); 213 + expect( 214 + view.getByText("branching.issueMessages.partial-choice-coverage") !== 215 + null, 216 + ).toBe(true); 217 + expect(view.queryByText("branching.issueMessages.fragile-text-match")).toBe( 218 + null, 219 + ); 220 + 221 + cleanup(); 222 + restoreDom(); 223 + }); 224 + });
+490
components/builder/branch-rules-editor.tsx
··· 1 + "use client"; 2 + 3 + import { 4 + closestCenter, 5 + DndContext, 6 + type DragEndEvent, 7 + PointerSensor, 8 + useSensor, 9 + useSensors, 10 + } from "@dnd-kit/core"; 11 + import { 12 + arrayMove, 13 + SortableContext, 14 + useSortable, 15 + verticalListSortingStrategy, 16 + } from "@dnd-kit/sortable"; 17 + import { CSS } from "@dnd-kit/utilities"; 18 + import { GripVertical, Plus, Trash2 } from "lucide-react"; 19 + import type { Dispatch, SetStateAction } from "react"; 20 + 21 + import { useI18n } from "@/components/i18n-provider"; 22 + import { Button } from "@/components/ui/button"; 23 + import { Input } from "@/components/ui/input"; 24 + import { 25 + Select, 26 + SelectContent, 27 + SelectItem, 28 + SelectTrigger, 29 + SelectValue, 30 + } from "@/components/ui/select"; 31 + import { 32 + AGREEMENT_ANSWER_VALUES, 33 + branchOperatorNeedsValue, 34 + getVisibleBranchOperators, 35 + type BranchOperator, 36 + } from "@/lib/blocks"; 37 + import { 38 + getBlockDisplayLabel, 39 + getBranchRuleValueHint, 40 + getBranchTargetBlocks, 41 + getBranchValidationIssueI18n, 42 + type BranchValidationIssue, 43 + } from "@/lib/branching"; 44 + import { 45 + createDefaultBranchRuleDraft, 46 + serializeBranchRuleDrafts, 47 + type BranchRuleDraft, 48 + } from "@/lib/form-builder-drafts"; 49 + import type { BuilderBlock } from "@/lib/form-types"; 50 + 51 + function SortableBranchRuleRow({ 52 + block, 53 + rule, 54 + index, 55 + placeholder, 56 + targetOptions, 57 + onChange, 58 + onRemove, 59 + }: { 60 + block: BuilderBlock; 61 + rule: BranchRuleDraft; 62 + index: number; 63 + placeholder: string; 64 + targetOptions: BuilderBlock[]; 65 + onChange: ( 66 + ruleId: string, 67 + patch: Partial< 68 + Pick<BranchRuleDraft, "operator" | "value" | "targetBlockId"> 69 + >, 70 + ) => void; 71 + onRemove: (ruleId: string) => void; 72 + }) { 73 + const { t } = useI18n(); 74 + const { attributes, listeners, setNodeRef, transform, transition } = 75 + useSortable({ id: rule.id }); 76 + const staleTargetMissing = 77 + rule.targetBlockId && 78 + !targetOptions.some((targetBlock) => targetBlock.id === rule.targetBlockId); 79 + const visibleOperators = getVisibleBranchOperators(block.type); 80 + const supportedOperators = visibleOperators.includes(rule.operator) 81 + ? visibleOperators 82 + : [rule.operator, ...visibleOperators]; 83 + const showValueInput = 84 + branchOperatorNeedsValue(rule.operator) && block.type !== "AGREEMENT"; 85 + const valueInputType = 86 + block.type === "NUMBER" 87 + ? "number" 88 + : block.type === "DATE" 89 + ? "date" 90 + : "text"; 91 + const discreteOptions = 92 + block.type === "SINGLE_CHOICE" || block.type === "MULTIPLE_CHOICE" 93 + ? (block.config as { options: string[] }).options 94 + : block.type === "AGREEMENT" 95 + ? [AGREEMENT_ANSWER_VALUES.AGREED, AGREEMENT_ANSWER_VALUES.NOT_AGREED] 96 + : null; 97 + 98 + return ( 99 + <div 100 + ref={setNodeRef} 101 + style={{ 102 + transform: CSS.Transform.toString(transform), 103 + transition, 104 + }} 105 + className="grid gap-3 rounded-[18px] border border-[color:var(--line)] bg-[var(--surface)] p-3" 106 + > 107 + <div className="flex items-center gap-2"> 108 + <button 109 + type="button" 110 + className="inline-flex size-10 shrink-0 items-center justify-center rounded-xl border border-[color:var(--line)] text-[var(--muted)] transition hover:bg-[var(--accent-soft)] hover:text-[var(--ink)]" 111 + aria-label={t("builder.dragBranchRuleAria", { number: index + 1 })} 112 + {...attributes} 113 + {...listeners} 114 + > 115 + <GripVertical className="size-4" /> 116 + </button> 117 + <div className="grid min-w-0 flex-1 gap-3 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1fr)_minmax(220px,0.9fr)]"> 118 + <label className="grid gap-2 text-sm text-[var(--muted)]"> 119 + <span className="font-medium text-[var(--ink)]"> 120 + {t("builder.branchOperator")} 121 + </span> 122 + <Select 123 + value={rule.operator} 124 + onValueChange={(value) => 125 + onChange(rule.id, { 126 + operator: value as BranchOperator, 127 + value: 128 + block.type === "AGREEMENT" 129 + ? AGREEMENT_ANSWER_VALUES.AGREED 130 + : branchOperatorNeedsValue(value as BranchOperator) 131 + ? rule.value 132 + : null, 133 + }) 134 + } 135 + > 136 + <SelectTrigger className="h-10 w-full text-sm font-medium"> 137 + <SelectValue /> 138 + </SelectTrigger> 139 + <SelectContent> 140 + {supportedOperators.map((operator) => ( 141 + <SelectItem key={operator} value={operator}> 142 + {block.type === "SINGLE_CHOICE" && 143 + (operator === "equals" || operator === "not_equals") 144 + ? t( 145 + `builder.branchOperatorLabels.singleChoice.${operator}`, 146 + ) 147 + : block.type === "MULTIPLE_CHOICE" && 148 + operator === "contains_any" 149 + ? t( 150 + "builder.branchOperatorLabels.multipleChoice.contains_any", 151 + ) 152 + : block.type === "AGREEMENT" && 153 + (operator === "equals" || operator === "not_equals") 154 + ? t( 155 + `builder.branchOperatorLabels.agreement.${operator}`, 156 + ) 157 + : block.type === "DATE" && 158 + [ 159 + "equals", 160 + "not_equals", 161 + "gt", 162 + "gte", 163 + "lt", 164 + "lte", 165 + ].includes(operator) 166 + ? t(`builder.branchOperatorLabels.date.${operator}`) 167 + : t(`builder.branchOperators.${operator}`)} 168 + </SelectItem> 169 + ))} 170 + </SelectContent> 171 + </Select> 172 + </label> 173 + 174 + {showValueInput ? ( 175 + <label className="grid gap-2 text-sm text-[var(--muted)]"> 176 + <span className="font-medium text-[var(--ink)]"> 177 + {t("builder.branchWhenAnswer")} 178 + </span> 179 + {discreteOptions ? ( 180 + <Select 181 + value={rule.value ?? undefined} 182 + onValueChange={(value) => onChange(rule.id, { value })} 183 + > 184 + <SelectTrigger className="h-10 w-full text-sm font-medium"> 185 + <SelectValue placeholder={placeholder} /> 186 + </SelectTrigger> 187 + <SelectContent> 188 + {discreteOptions.map((option) => ( 189 + <SelectItem key={option} value={option}> 190 + {block.type === "AGREEMENT" 191 + ? t(`builder.branchAgreementValues.${option}`) 192 + : option} 193 + </SelectItem> 194 + ))} 195 + </SelectContent> 196 + </Select> 197 + ) : ( 198 + <Input 199 + type={valueInputType} 200 + value={rule.value ?? ""} 201 + placeholder={placeholder} 202 + onChange={(event) => 203 + onChange(rule.id, { value: event.target.value }) 204 + } 205 + /> 206 + )} 207 + </label> 208 + ) : ( 209 + <div className="grid gap-2 text-sm text-[var(--muted)]"> 210 + <span className="font-medium text-[var(--ink)]"> 211 + {t("builder.branchWhenAnswer")} 212 + </span> 213 + <div className="flex h-10 items-center rounded-xl border border-[color:var(--line)] bg-[var(--surface-strong)] px-3 text-sm text-[var(--muted)]"> 214 + {t("builder.branchNoValueNeeded")} 215 + </div> 216 + </div> 217 + )} 218 + 219 + <label className="grid gap-2 text-sm text-[var(--muted)]"> 220 + <span className="font-medium text-[var(--ink)]"> 221 + {t("builder.branchGoTo")} 222 + </span> 223 + <Select 224 + value={rule.targetBlockId} 225 + onValueChange={(value) => 226 + onChange(rule.id, { targetBlockId: value }) 227 + } 228 + > 229 + <SelectTrigger className="h-10 w-full text-sm font-medium"> 230 + <SelectValue /> 231 + </SelectTrigger> 232 + <SelectContent> 233 + {staleTargetMissing ? ( 234 + <SelectItem value={rule.targetBlockId}> 235 + {t("builder.branchMissingTarget")} 236 + </SelectItem> 237 + ) : null} 238 + {targetOptions.map((targetBlock) => ( 239 + <SelectItem key={targetBlock.id} value={targetBlock.id}> 240 + {getBlockDisplayLabel(targetBlock)} 241 + </SelectItem> 242 + ))} 243 + </SelectContent> 244 + </Select> 245 + </label> 246 + </div> 247 + <button 248 + type="button" 249 + className="inline-flex size-10 shrink-0 items-center justify-center rounded-xl border border-[color:var(--line)] bg-[var(--surface-strong)] text-[var(--muted)] transition hover:bg-[var(--danger-hover)] hover:text-[var(--danger-contrast)] hover:border-[var(--danger-hover)]" 250 + aria-label={t("builder.removeBranchRuleAria", { number: index + 1 })} 251 + onClick={() => onRemove(rule.id)} 252 + > 253 + <Trash2 className="size-4" /> 254 + </button> 255 + </div> 256 + </div> 257 + ); 258 + } 259 + 260 + export function BranchRulesEditor({ 261 + allBlocks, 262 + blockDraft, 263 + branchRulesDraft, 264 + branchValidationIssues, 265 + setBranchRulesDraft, 266 + updateConfig, 267 + }: { 268 + allBlocks: BuilderBlock[]; 269 + blockDraft: BuilderBlock; 270 + branchRulesDraft: BranchRuleDraft[]; 271 + branchValidationIssues: BranchValidationIssue[]; 272 + setBranchRulesDraft: Dispatch<SetStateAction<BranchRuleDraft[]>>; 273 + updateConfig: (patch: Record<string, unknown>) => void; 274 + }) { 275 + const { t } = useI18n(); 276 + const branchRuleSensors = useSensors( 277 + useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), 278 + ); 279 + const branchTargetOptions = getBranchTargetBlocks(allBlocks, blockDraft.id); 280 + const blockBranchIssues = branchValidationIssues.filter( 281 + (issue) => issue.blockId === blockDraft.id, 282 + ); 283 + const blockBranchBlockers = blockBranchIssues.filter( 284 + (issue) => issue.severity === "blocker", 285 + ); 286 + const blockBranchWarnings = blockBranchIssues.filter( 287 + (issue) => issue.severity === "warning", 288 + ); 289 + const branchRulePlaceholder = getBranchRuleValueHint(blockDraft); 290 + const defaultNextBlockId = 291 + "defaultNextBlockId" in blockDraft.config 292 + ? blockDraft.config.defaultNextBlockId 293 + : null; 294 + const defaultNextSelectValue = defaultNextBlockId ?? "__linear__"; 295 + const staleDefaultNextMissing = Boolean( 296 + defaultNextBlockId && 297 + !branchTargetOptions.some( 298 + (targetBlock) => targetBlock.id === defaultNextBlockId, 299 + ), 300 + ); 301 + 302 + function syncBranchRules(nextRules: BranchRuleDraft[]) { 303 + setBranchRulesDraft(nextRules); 304 + updateConfig({ 305 + branchRules: serializeBranchRuleDrafts(nextRules), 306 + }); 307 + } 308 + 309 + function updateBranchRule( 310 + ruleId: string, 311 + patch: Partial< 312 + Pick<BranchRuleDraft, "operator" | "value" | "targetBlockId"> 313 + >, 314 + ) { 315 + syncBranchRules( 316 + branchRulesDraft.map((rule) => 317 + rule.id === ruleId ? { ...rule, ...patch } : rule, 318 + ), 319 + ); 320 + } 321 + 322 + function addBranchRule() { 323 + const firstTarget = branchTargetOptions[0]; 324 + 325 + if (!firstTarget) { 326 + return; 327 + } 328 + 329 + syncBranchRules([ 330 + ...branchRulesDraft, 331 + createDefaultBranchRuleDraft({ 332 + blockType: blockDraft.type, 333 + targetBlockId: firstTarget.id, 334 + }), 335 + ]); 336 + } 337 + 338 + function removeBranchRule(ruleId: string) { 339 + syncBranchRules(branchRulesDraft.filter((rule) => rule.id !== ruleId)); 340 + } 341 + 342 + function handleBranchRuleDragEnd(event: DragEndEvent) { 343 + const { active, over } = event; 344 + 345 + if (!over || active.id === over.id) { 346 + return; 347 + } 348 + 349 + const oldIndex = branchRulesDraft.findIndex( 350 + (rule) => rule.id === active.id, 351 + ); 352 + const newIndex = branchRulesDraft.findIndex((rule) => rule.id === over.id); 353 + 354 + if (oldIndex < 0 || newIndex < 0) { 355 + return; 356 + } 357 + 358 + syncBranchRules(arrayMove(branchRulesDraft, oldIndex, newIndex)); 359 + } 360 + 361 + return ( 362 + <div className="grid gap-4 rounded-[18px] border border-[color:var(--line)] bg-[var(--bg-strong)] px-4 py-4 text-sm text-[var(--muted)]"> 363 + <div className="flex items-center justify-between gap-3"> 364 + <div> 365 + <p className="font-medium text-[var(--ink)]"> 366 + {t("builder.branchingTitle")} 367 + </p> 368 + <p className="mt-1 text-xs text-[var(--muted)]"> 369 + {t("builder.branchingDescription")} 370 + </p> 371 + </div> 372 + <Button 373 + variant="secondary" 374 + size="sm" 375 + onClick={addBranchRule} 376 + disabled={branchTargetOptions.length === 0} 377 + > 378 + <Plus className="size-4" /> 379 + {t("builder.addBranchRule")} 380 + </Button> 381 + </div> 382 + 383 + {branchRulesDraft.length ? ( 384 + <DndContext 385 + sensors={branchRuleSensors} 386 + collisionDetection={closestCenter} 387 + onDragEnd={handleBranchRuleDragEnd} 388 + > 389 + <SortableContext 390 + items={branchRulesDraft.map((rule) => rule.id)} 391 + strategy={verticalListSortingStrategy} 392 + > 393 + <div className="grid gap-3"> 394 + {branchRulesDraft.map((rule, index) => ( 395 + <SortableBranchRuleRow 396 + key={rule.id} 397 + block={blockDraft} 398 + rule={rule} 399 + index={index} 400 + placeholder={branchRulePlaceholder} 401 + targetOptions={branchTargetOptions} 402 + onChange={updateBranchRule} 403 + onRemove={removeBranchRule} 404 + /> 405 + ))} 406 + </div> 407 + </SortableContext> 408 + </DndContext> 409 + ) : null} 410 + 411 + <label className="grid gap-2 text-sm text-[var(--muted)] sm:max-w-md"> 412 + <span className="font-medium text-[var(--ink)]"> 413 + {t("builder.branchOtherwise")} 414 + </span> 415 + <Select 416 + value={defaultNextSelectValue} 417 + onValueChange={(value) => 418 + updateConfig({ 419 + defaultNextBlockId: value === "__linear__" ? null : value, 420 + }) 421 + } 422 + > 423 + <SelectTrigger className="h-10 w-full text-sm font-medium"> 424 + <SelectValue /> 425 + </SelectTrigger> 426 + <SelectContent> 427 + <SelectItem value="__linear__"> 428 + {t("builder.branchDefaultLinear")} 429 + </SelectItem> 430 + {staleDefaultNextMissing && defaultNextBlockId ? ( 431 + <SelectItem value={defaultNextBlockId}> 432 + {t("builder.branchMissingTarget")} 433 + </SelectItem> 434 + ) : null} 435 + {branchTargetOptions.map((targetBlock) => ( 436 + <SelectItem key={targetBlock.id} value={targetBlock.id}> 437 + {getBlockDisplayLabel(targetBlock)} 438 + </SelectItem> 439 + ))} 440 + </SelectContent> 441 + </Select> 442 + </label> 443 + 444 + {branchTargetOptions.length === 0 ? ( 445 + <p className="text-xs text-[var(--muted)]"> 446 + {t("builder.branchingNoTargets")} 447 + </p> 448 + ) : null} 449 + <p className="text-xs text-[var(--muted)]"> 450 + {t("builder.branchingHelp", { value: branchRulePlaceholder })} 451 + </p> 452 + 453 + {blockBranchBlockers.length ? ( 454 + <div className="rounded-xl border border-amber-500/30 bg-amber-500/10 px-3 py-3 text-sm text-[var(--ink)]"> 455 + <p className="font-medium"> 456 + {t("builder.branchingBlockersForBlockTitle")} 457 + </p> 458 + <ul className="mt-2 list-disc space-y-1 pl-5 text-[var(--muted)]"> 459 + {blockBranchBlockers.map((issue, index) => { 460 + const issueI18n = getBranchValidationIssueI18n(issue, allBlocks); 461 + return ( 462 + <li key={`${issue.code}-${issue.ruleIndex ?? index}`}> 463 + {t(issueI18n.key, issueI18n.values)} 464 + </li> 465 + ); 466 + })} 467 + </ul> 468 + </div> 469 + ) : null} 470 + 471 + {blockBranchWarnings.length ? ( 472 + <div className="rounded-xl border border-sky-500/30 bg-sky-500/10 px-3 py-3 text-sm text-[var(--ink)]"> 473 + <p className="font-medium"> 474 + {t("builder.branchingWarningsForBlockTitle")} 475 + </p> 476 + <ul className="mt-2 list-disc space-y-1 pl-5 text-[var(--muted)]"> 477 + {blockBranchWarnings.map((issue, index) => { 478 + const issueI18n = getBranchValidationIssueI18n(issue, allBlocks); 479 + return ( 480 + <li key={`${issue.code}-${issue.ruleIndex ?? index}`}> 481 + {t(issueI18n.key, issueI18n.values)} 482 + </li> 483 + ); 484 + })} 485 + </ul> 486 + </div> 487 + ) : null} 488 + </div> 489 + ); 490 + }
+10 -446
components/form-builder-panels.tsx
··· 36 36 import * as React from "react"; 37 37 import type { Dispatch, SetStateAction } from "react"; 38 38 39 + import { BranchRulesEditor } from "@/components/builder/branch-rules-editor"; 39 40 import { useI18n } from "@/components/i18n-provider"; 40 41 import { FormStatusBadge } from "@/components/form-status-badge"; 41 42 import { Button } from "@/components/ui/button"; ··· 46 47 PopoverContent, 47 48 PopoverTrigger, 48 49 } from "@/components/ui/popover"; 49 - import { 50 - Select, 51 - SelectContent, 52 - SelectItem, 53 - SelectTrigger, 54 - SelectValue, 55 - } from "@/components/ui/select"; 56 50 import { Textarea } from "@/components/ui/textarea"; 57 51 import { 58 - AGREEMENT_ANSWER_VALUES, 59 - branchOperatorNeedsValue, 60 52 blockTypeTranslationKeys, 61 - getVisibleBranchOperators, 62 53 isQuestionBlock, 63 54 type AgreementBlockConfig, 64 55 type BlockConfig, 65 - type BranchOperator, 66 56 type LinkBlockConfig, 67 57 type LongTextBlockConfig, 68 58 type NumberBlockConfig, 69 59 type ShortTextBlockConfig, 70 60 type TextBlockConfig, 71 61 } from "@/lib/blocks"; 72 - import { 73 - getBlockDisplayLabel, 74 - getBranchRuleValueHint, 75 - getBranchTargetBlocks, 76 - getBranchValidationIssueI18n, 77 - type BranchValidationIssue, 78 - } from "@/lib/branching"; 62 + import { type BranchValidationIssue } from "@/lib/branching"; 79 63 import { 80 64 createChoiceOptionDraft, 81 - createDefaultBranchRuleDraft, 82 - serializeBranchRuleDrafts, 83 65 serializeChoiceOptionDrafts, 84 66 type BranchRuleDraft, 85 67 type ChoiceOptionDraft, ··· 487 469 ); 488 470 } 489 471 490 - function SortableBranchRuleRow({ 491 - block, 492 - rule, 493 - index, 494 - placeholder, 495 - targetOptions, 496 - onChange, 497 - onRemove, 498 - }: { 499 - block: BuilderBlock; 500 - rule: BranchRuleDraft; 501 - index: number; 502 - placeholder: string; 503 - targetOptions: BuilderBlock[]; 504 - onChange: ( 505 - ruleId: string, 506 - patch: Partial< 507 - Pick<BranchRuleDraft, "operator" | "value" | "targetBlockId"> 508 - >, 509 - ) => void; 510 - onRemove: (ruleId: string) => void; 511 - }) { 512 - const { t } = useI18n(); 513 - const { attributes, listeners, setNodeRef, transform, transition } = 514 - useSortable({ id: rule.id }); 515 - const staleTargetMissing = 516 - rule.targetBlockId && 517 - !targetOptions.some((targetBlock) => targetBlock.id === rule.targetBlockId); 518 - const visibleOperators = getVisibleBranchOperators(block.type); 519 - const supportedOperators = visibleOperators.includes(rule.operator) 520 - ? visibleOperators 521 - : [rule.operator, ...visibleOperators]; 522 - const showValueInput = 523 - branchOperatorNeedsValue(rule.operator) && block.type !== "AGREEMENT"; 524 - const valueInputType = 525 - block.type === "NUMBER" 526 - ? "number" 527 - : block.type === "DATE" 528 - ? "date" 529 - : "text"; 530 - const discreteOptions = 531 - block.type === "SINGLE_CHOICE" || block.type === "MULTIPLE_CHOICE" 532 - ? (block.config as { options: string[] }).options 533 - : block.type === "AGREEMENT" 534 - ? [AGREEMENT_ANSWER_VALUES.AGREED, AGREEMENT_ANSWER_VALUES.NOT_AGREED] 535 - : null; 536 - 537 - return ( 538 - <div 539 - ref={setNodeRef} 540 - style={{ 541 - transform: CSS.Transform.toString(transform), 542 - transition, 543 - }} 544 - className="grid gap-3 rounded-[18px] border border-[color:var(--line)] bg-[var(--surface)] p-3" 545 - > 546 - <div className="flex items-center gap-2"> 547 - <button 548 - type="button" 549 - className="inline-flex size-10 shrink-0 items-center justify-center rounded-xl border border-[color:var(--line)] text-[var(--muted)] transition hover:bg-[var(--accent-soft)] hover:text-[var(--ink)]" 550 - aria-label={t("builder.dragBranchRuleAria", { number: index + 1 })} 551 - {...attributes} 552 - {...listeners} 553 - > 554 - <GripVertical className="size-4" /> 555 - </button> 556 - <div className="grid min-w-0 flex-1 gap-3 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1fr)_minmax(220px,0.9fr)]"> 557 - <label className="grid gap-2 text-sm text-[var(--muted)]"> 558 - <span className="font-medium text-[var(--ink)]"> 559 - {t("builder.branchOperator")} 560 - </span> 561 - <Select 562 - value={rule.operator} 563 - onValueChange={(value) => 564 - onChange(rule.id, { 565 - operator: value as BranchOperator, 566 - value: 567 - block.type === "AGREEMENT" 568 - ? AGREEMENT_ANSWER_VALUES.AGREED 569 - : branchOperatorNeedsValue(value as BranchOperator) 570 - ? rule.value 571 - : null, 572 - }) 573 - } 574 - > 575 - <SelectTrigger className="h-10 w-full text-sm font-medium"> 576 - <SelectValue /> 577 - </SelectTrigger> 578 - <SelectContent> 579 - {supportedOperators.map((operator) => ( 580 - <SelectItem key={operator} value={operator}> 581 - {block.type === "SINGLE_CHOICE" && 582 - (operator === "equals" || operator === "not_equals") 583 - ? t( 584 - `builder.branchOperatorLabels.singleChoice.${operator}`, 585 - ) 586 - : block.type === "MULTIPLE_CHOICE" && 587 - operator === "contains_any" 588 - ? t( 589 - "builder.branchOperatorLabels.multipleChoice.contains_any", 590 - ) 591 - : block.type === "AGREEMENT" && 592 - (operator === "equals" || operator === "not_equals") 593 - ? t( 594 - `builder.branchOperatorLabels.agreement.${operator}`, 595 - ) 596 - : block.type === "DATE" && 597 - [ 598 - "equals", 599 - "not_equals", 600 - "gt", 601 - "gte", 602 - "lt", 603 - "lte", 604 - ].includes(operator) 605 - ? t(`builder.branchOperatorLabels.date.${operator}`) 606 - : t(`builder.branchOperators.${operator}`)} 607 - </SelectItem> 608 - ))} 609 - </SelectContent> 610 - </Select> 611 - </label> 612 - 613 - {showValueInput ? ( 614 - <label className="grid gap-2 text-sm text-[var(--muted)]"> 615 - <span className="font-medium text-[var(--ink)]"> 616 - {t("builder.branchWhenAnswer")} 617 - </span> 618 - {discreteOptions ? ( 619 - <Select 620 - value={rule.value ?? undefined} 621 - onValueChange={(value) => onChange(rule.id, { value })} 622 - > 623 - <SelectTrigger className="h-10 w-full text-sm font-medium"> 624 - <SelectValue placeholder={placeholder} /> 625 - </SelectTrigger> 626 - <SelectContent> 627 - {discreteOptions.map((option) => ( 628 - <SelectItem key={option} value={option}> 629 - {block.type === "AGREEMENT" 630 - ? t(`builder.branchAgreementValues.${option}`) 631 - : option} 632 - </SelectItem> 633 - ))} 634 - </SelectContent> 635 - </Select> 636 - ) : ( 637 - <Input 638 - type={valueInputType} 639 - value={rule.value ?? ""} 640 - placeholder={placeholder} 641 - onChange={(event) => 642 - onChange(rule.id, { value: event.target.value }) 643 - } 644 - /> 645 - )} 646 - </label> 647 - ) : ( 648 - <div className="grid gap-2 text-sm text-[var(--muted)]"> 649 - <span className="font-medium text-[var(--ink)]"> 650 - {t("builder.branchWhenAnswer")} 651 - </span> 652 - <div className="flex h-10 items-center rounded-xl border border-[color:var(--line)] bg-[var(--surface-strong)] px-3 text-sm text-[var(--muted)]"> 653 - {t("builder.branchNoValueNeeded")} 654 - </div> 655 - </div> 656 - )} 657 - 658 - <label className="grid gap-2 text-sm text-[var(--muted)]"> 659 - <span className="font-medium text-[var(--ink)]"> 660 - {t("builder.branchGoTo")} 661 - </span> 662 - <Select 663 - value={rule.targetBlockId} 664 - onValueChange={(value) => 665 - onChange(rule.id, { targetBlockId: value }) 666 - } 667 - > 668 - <SelectTrigger className="h-10 w-full text-sm font-medium"> 669 - <SelectValue /> 670 - </SelectTrigger> 671 - <SelectContent> 672 - {staleTargetMissing ? ( 673 - <SelectItem value={rule.targetBlockId}> 674 - {t("builder.branchMissingTarget")} 675 - </SelectItem> 676 - ) : null} 677 - {targetOptions.map((targetBlock) => ( 678 - <SelectItem key={targetBlock.id} value={targetBlock.id}> 679 - {getBlockDisplayLabel(targetBlock)} 680 - </SelectItem> 681 - ))} 682 - </SelectContent> 683 - </Select> 684 - </label> 685 - </div> 686 - <button 687 - type="button" 688 - className="inline-flex size-10 shrink-0 items-center justify-center rounded-xl border border-[color:var(--line)] bg-[var(--surface-strong)] text-[var(--muted)] transition hover:bg-[var(--danger-hover)] hover:text-[var(--danger-contrast)] hover:border-[var(--danger-hover)]" 689 - aria-label={t("builder.removeBranchRuleAria", { number: index + 1 })} 690 - onClick={() => onRemove(rule.id)} 691 - > 692 - <Trash2 className="size-4" /> 693 - </button> 694 - </div> 695 - </div> 696 - ); 697 - } 698 - 699 472 export function BlockEditorPanel({ 700 473 allBlocks, 701 474 blockDraft, ··· 727 500 const optionSensors = useSensors( 728 501 useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), 729 502 ); 730 - const branchRuleSensors = useSensors( 731 - useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), 732 - ); 733 - const branchTargetOptions = getBranchTargetBlocks(allBlocks, blockDraft.id); 734 - const blockBranchIssues = branchValidationIssues.filter( 735 - (issue) => issue.blockId === blockDraft.id, 736 - ); 737 - const blockBranchBlockers = blockBranchIssues.filter( 738 - (issue) => issue.severity === "blocker", 739 - ); 740 - const blockBranchWarnings = blockBranchIssues.filter( 741 - (issue) => issue.severity === "warning", 742 - ); 743 - const branchRulePlaceholder = getBranchRuleValueHint(blockDraft); 744 - const defaultNextBlockId = 745 - "defaultNextBlockId" in blockDraft.config 746 - ? blockDraft.config.defaultNextBlockId 747 - : null; 748 - const defaultNextSelectValue = defaultNextBlockId ?? "__linear__"; 749 - const staleDefaultNextMissing = Boolean( 750 - defaultNextBlockId && 751 - !branchTargetOptions.some( 752 - (targetBlock) => targetBlock.id === defaultNextBlockId, 753 - ), 754 - ); 755 503 756 504 function updateConfig(patch: Record<string, unknown>) { 757 505 setBlockDraft((current) => ··· 822 570 } 823 571 824 572 syncChoiceOptions(arrayMove(choiceOptionsDraft, oldIndex, newIndex)); 825 - } 826 - 827 - function syncBranchRules(nextRules: BranchRuleDraft[]) { 828 - setBranchRulesDraft(nextRules); 829 - updateConfig({ 830 - branchRules: serializeBranchRuleDrafts(nextRules), 831 - }); 832 - } 833 - 834 - function updateBranchRule( 835 - ruleId: string, 836 - patch: Partial< 837 - Pick<BranchRuleDraft, "operator" | "value" | "targetBlockId"> 838 - >, 839 - ) { 840 - syncBranchRules( 841 - branchRulesDraft.map((rule) => 842 - rule.id === ruleId ? { ...rule, ...patch } : rule, 843 - ), 844 - ); 845 - } 846 - 847 - function addBranchRule() { 848 - const firstTarget = branchTargetOptions[0]; 849 - 850 - if (!firstTarget) { 851 - return; 852 - } 853 - 854 - syncBranchRules([ 855 - ...branchRulesDraft, 856 - createDefaultBranchRuleDraft({ 857 - blockType: blockDraft.type, 858 - targetBlockId: firstTarget.id, 859 - }), 860 - ]); 861 - } 862 - 863 - function removeBranchRule(ruleId: string) { 864 - syncBranchRules(branchRulesDraft.filter((rule) => rule.id !== ruleId)); 865 - } 866 - 867 - function handleBranchRuleDragEnd(event: DragEndEvent) { 868 - const { active, over } = event; 869 - 870 - if (!over || active.id === over.id) { 871 - return; 872 - } 873 - 874 - const oldIndex = branchRulesDraft.findIndex( 875 - (rule) => rule.id === active.id, 876 - ); 877 - const newIndex = branchRulesDraft.findIndex((rule) => rule.id === over.id); 878 - 879 - if (oldIndex < 0 || newIndex < 0) { 880 - return; 881 - } 882 - 883 - syncBranchRules(arrayMove(branchRulesDraft, oldIndex, newIndex)); 884 573 } 885 574 886 575 return ( ··· 1181 870 )} 1182 871 1183 872 {isQuestionBlock(blockDraft.type) ? ( 1184 - <div className="grid gap-4 rounded-[18px] border border-[color:var(--line)] bg-[var(--bg-strong)] px-4 py-4 text-sm text-[var(--muted)]"> 1185 - <div className="flex items-center justify-between gap-3"> 1186 - <div> 1187 - <p className="font-medium text-[var(--ink)]"> 1188 - {t("builder.branchingTitle")} 1189 - </p> 1190 - <p className="mt-1 text-xs text-[var(--muted)]"> 1191 - {t("builder.branchingDescription")} 1192 - </p> 1193 - </div> 1194 - <Button 1195 - variant="secondary" 1196 - size="sm" 1197 - onClick={addBranchRule} 1198 - disabled={branchTargetOptions.length === 0} 1199 - > 1200 - <Plus className="size-4" /> 1201 - {t("builder.addBranchRule")} 1202 - </Button> 1203 - </div> 1204 - 1205 - {branchRulesDraft.length ? ( 1206 - <DndContext 1207 - sensors={branchRuleSensors} 1208 - collisionDetection={closestCenter} 1209 - onDragEnd={handleBranchRuleDragEnd} 1210 - > 1211 - <SortableContext 1212 - items={branchRulesDraft.map((rule) => rule.id)} 1213 - strategy={verticalListSortingStrategy} 1214 - > 1215 - <div className="grid gap-3"> 1216 - {branchRulesDraft.map((rule, index) => ( 1217 - <SortableBranchRuleRow 1218 - key={rule.id} 1219 - block={blockDraft} 1220 - rule={rule} 1221 - index={index} 1222 - placeholder={branchRulePlaceholder} 1223 - targetOptions={branchTargetOptions} 1224 - onChange={updateBranchRule} 1225 - onRemove={removeBranchRule} 1226 - /> 1227 - ))} 1228 - </div> 1229 - </SortableContext> 1230 - </DndContext> 1231 - ) : null} 1232 - 1233 - <label className="grid gap-2 text-sm text-[var(--muted)] sm:max-w-md"> 1234 - <span className="font-medium text-[var(--ink)]"> 1235 - {t("builder.branchOtherwise")} 1236 - </span> 1237 - <Select 1238 - value={defaultNextSelectValue} 1239 - onValueChange={(value) => 1240 - updateConfig({ 1241 - defaultNextBlockId: value === "__linear__" ? null : value, 1242 - }) 1243 - } 1244 - > 1245 - <SelectTrigger className="h-10 w-full text-sm font-medium"> 1246 - <SelectValue /> 1247 - </SelectTrigger> 1248 - <SelectContent> 1249 - <SelectItem value="__linear__"> 1250 - {t("builder.branchDefaultLinear")} 1251 - </SelectItem> 1252 - {staleDefaultNextMissing && defaultNextBlockId ? ( 1253 - <SelectItem value={defaultNextBlockId}> 1254 - {t("builder.branchMissingTarget")} 1255 - </SelectItem> 1256 - ) : null} 1257 - {branchTargetOptions.map((targetBlock) => ( 1258 - <SelectItem key={targetBlock.id} value={targetBlock.id}> 1259 - {getBlockDisplayLabel(targetBlock)} 1260 - </SelectItem> 1261 - ))} 1262 - </SelectContent> 1263 - </Select> 1264 - </label> 1265 - 1266 - {branchTargetOptions.length === 0 ? ( 1267 - <p className="text-xs text-[var(--muted)]"> 1268 - {t("builder.branchingNoTargets")} 1269 - </p> 1270 - ) : null} 1271 - <p className="text-xs text-[var(--muted)]"> 1272 - {t("builder.branchingHelp", { value: branchRulePlaceholder })} 1273 - </p> 1274 - 1275 - {blockBranchBlockers.length ? ( 1276 - <div className="rounded-xl border border-amber-500/30 bg-amber-500/10 px-3 py-3 text-sm text-[var(--ink)]"> 1277 - <p className="font-medium"> 1278 - {t("builder.branchingBlockersForBlockTitle")} 1279 - </p> 1280 - <ul className="mt-2 list-disc space-y-1 pl-5 text-[var(--muted)]"> 1281 - {blockBranchBlockers.map((issue, index) => { 1282 - const issueI18n = getBranchValidationIssueI18n( 1283 - issue, 1284 - allBlocks, 1285 - ); 1286 - return ( 1287 - <li key={`${issue.code}-${issue.ruleIndex ?? index}`}> 1288 - {t(issueI18n.key, issueI18n.values)} 1289 - </li> 1290 - ); 1291 - })} 1292 - </ul> 1293 - </div> 1294 - ) : null} 1295 - 1296 - {blockBranchWarnings.length ? ( 1297 - <div className="rounded-xl border border-sky-500/30 bg-sky-500/10 px-3 py-3 text-sm text-[var(--ink)]"> 1298 - <p className="font-medium"> 1299 - {t("builder.branchingWarningsForBlockTitle")} 1300 - </p> 1301 - <ul className="mt-2 list-disc space-y-1 pl-5 text-[var(--muted)]"> 1302 - {blockBranchWarnings.map((issue, index) => { 1303 - const issueI18n = getBranchValidationIssueI18n( 1304 - issue, 1305 - allBlocks, 1306 - ); 1307 - return ( 1308 - <li key={`${issue.code}-${issue.ruleIndex ?? index}`}> 1309 - {t(issueI18n.key, issueI18n.values)} 1310 - </li> 1311 - ); 1312 - })} 1313 - </ul> 1314 - </div> 1315 - ) : null} 1316 - </div> 873 + <BranchRulesEditor 874 + allBlocks={allBlocks} 875 + blockDraft={blockDraft} 876 + branchRulesDraft={branchRulesDraft} 877 + branchValidationIssues={branchValidationIssues} 878 + setBranchRulesDraft={setBranchRulesDraft} 879 + updateConfig={updateConfig} 880 + /> 1317 881 ) : null} 1318 882 </div> 1319 883
+4
package.json
··· 35 35 }, 36 36 "devDependencies": { 37 37 "@tailwindcss/postcss": "4.2.2", 38 + "@testing-library/dom": "^10.4.1", 39 + "@testing-library/react": "^16.3.2", 40 + "@types/jsdom": "^28.0.1", 38 41 "@types/node": "25.6.0", 39 42 "@types/react": "19.2.14", 40 43 "@types/react-dom": "19.2.3", 41 44 "eslint": "9.39.1", 42 45 "eslint-config-next": "16.2.3", 46 + "jsdom": "^29.0.2", 43 47 "prettier": "^3.8.2", 44 48 "prisma": "7.7.0", 45 49 "tailwindcss": "4.2.2",
+71
test/install-dom.ts
··· 1 + import { JSDOM } from "jsdom"; 2 + 3 + export function installTestDom() { 4 + const dom = new JSDOM("<!doctype html><html><body></body></html>", { 5 + url: "http://localhost/", 6 + }); 7 + const { window } = dom; 8 + const globalObject = globalThis as typeof globalThis & 9 + Record<string, unknown>; 10 + 11 + const previousValues = { 12 + window: globalObject.window, 13 + document: globalObject.document, 14 + navigator: globalObject.navigator, 15 + HTMLElement: globalObject.HTMLElement, 16 + Element: globalObject.Element, 17 + Node: globalObject.Node, 18 + Event: globalObject.Event, 19 + MouseEvent: globalObject.MouseEvent, 20 + PointerEvent: globalObject.PointerEvent, 21 + KeyboardEvent: globalObject.KeyboardEvent, 22 + MutationObserver: globalObject.MutationObserver, 23 + DOMRect: globalObject.DOMRect, 24 + getComputedStyle: globalObject.getComputedStyle, 25 + requestAnimationFrame: globalObject.requestAnimationFrame, 26 + cancelAnimationFrame: globalObject.cancelAnimationFrame, 27 + ResizeObserver: globalObject.ResizeObserver, 28 + }; 29 + 30 + globalObject.window = window as unknown as Window & typeof globalThis; 31 + globalObject.document = window.document; 32 + globalObject.navigator = window.navigator; 33 + globalObject.HTMLElement = window.HTMLElement; 34 + globalObject.Element = window.Element; 35 + globalObject.Node = window.Node; 36 + globalObject.Event = window.Event; 37 + globalObject.MouseEvent = window.MouseEvent; 38 + globalObject.PointerEvent = window.PointerEvent ?? window.MouseEvent; 39 + globalObject.KeyboardEvent = window.KeyboardEvent; 40 + globalObject.MutationObserver = window.MutationObserver; 41 + globalObject.DOMRect = window.DOMRect; 42 + globalObject.getComputedStyle = window.getComputedStyle.bind(window); 43 + globalObject.requestAnimationFrame = ((callback: FrameRequestCallback) => 44 + window.setTimeout( 45 + () => callback(Date.now()), 46 + 0, 47 + )) as typeof requestAnimationFrame; 48 + globalObject.cancelAnimationFrame = ((id: number) => 49 + window.clearTimeout(id)) as typeof cancelAnimationFrame; 50 + globalObject.ResizeObserver = class ResizeObserver { 51 + observe() {} 52 + unobserve() {} 53 + disconnect() {} 54 + } as unknown as typeof ResizeObserver; 55 + 56 + if (!window.HTMLElement.prototype.scrollIntoView) { 57 + window.HTMLElement.prototype.scrollIntoView = () => {}; 58 + } 59 + 60 + return () => { 61 + for (const [key, value] of Object.entries(previousValues)) { 62 + if (typeof value === "undefined") { 63 + delete globalObject[key]; 64 + } else { 65 + globalObject[key] = value; 66 + } 67 + } 68 + 69 + dom.window.close(); 70 + }; 71 + }