diff --git a/README.md b/README.md index 13ef9cade..3234bfa74 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,27 @@ Finally, if you're going to use a component that requires global state (like ` ``` +### Widget navigation icons + +Resource navigation links in widget rows and cards use the imported Lucide +`square-arrow-out-up-right` component in neutral gray, sized to match remove +icons. The `linkIcon` helper still accepts a custom image URL as its third +argument; explicit custom icons retain their existing image rendering. + +### Attachment-list delete confirmations + +`widgets.attachmentList(dom, subject, container, options)` accepts an optional +`options.confirmationContainer` HTML element. When provided, delete confirmations +are rendered in normal flow in that element, outside the list's rows, with Cancel +focused. Only one confirmation is shown per confirmation container; unrelated +children are preserved. Mount the container in a visible location above or beside +the list. + +Omitting this option preserves the existing row-local popup and positioning. +`personTR` / `renderAsRow` and `renderAsDiv` accept the same option. +`deleteButtonWithCheck` accepts the container as an optional fifth argument; +existing four-argument calls are unchanged. + ## Use directly in a browser Solid-UI provides **ESM** bundles for direct browser usage. You'll also need to include the CSS styles: diff --git a/package-lock.json b/package-lock.json index 24d8202b6..f1cf73b95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "solid-ui", - "version": "5.0.1", + "version": "5.0.2-9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "solid-ui", - "version": "5.0.1", + "version": "5.0.2-9", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -30,9 +30,9 @@ "escape-html": "^1.0.3", "lit": "^3.3.3", "mime-types": "^3.0.2", - "pane-registry": "5.0.1", + "pane-registry": "5.0.2-3", "rdflib": "2.4.1", - "solid-logic": "6.0.0", + "solid-logic": "6.0.1-6", "solid-namespace": "^0.5.4", "uuid": "^14.0.0" }, @@ -91,10 +91,10 @@ } }, "node_modules/@acemir/cssom": { - "version": "0.9.31", - "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", - "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", - "devOptional": true, + "version": "0.9.32", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.32.tgz", + "integrity": "sha512-OijrAUu7rrJhSIoHNg6LMPS5e344nwUHA+1EFYYjrh64D+GmytUsfBQjci45TjT+rd5Spsp8B5xsZDT+TgyXXQ==", + "dev": true, "license": "MIT" }, "node_modules/@adobe/css-tools": { @@ -122,7 +122,7 @@ "version": "5.1.11", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", @@ -139,7 +139,7 @@ "version": "6.8.1", "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", @@ -153,7 +153,7 @@ "version": "11.5.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", - "devOptional": true, + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -163,7 +163,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" @@ -173,7 +173,7 @@ "version": "2.3.9", "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@awesome.me/webawesome": { @@ -256,21 +256,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.13.tgz", + "integrity": "sha512-j1nvOQCZYSgp7HLmisnCXFTpVvFODiijQ9QHkORfGKWH6YDtskTEeCJS7eV545GZ9UBzrq0e2xsOd5QQ5PjIqw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", + "@babel/generator": "^7.29.13", + "@babel/helper-compilation-targets": "^7.29.13", + "@babel/helper-module-transforms": "^7.29.13", + "@babel/helpers": "^7.29.13", + "@babel/parser": "^7.29.13", "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", + "@babel/traverse": "^7.29.13", + "@babel/types": "^7.29.11", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -287,14 +287,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", - "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.13.tgz", + "integrity": "sha512-T5FRo+RzVHvGjWOuF1Ljc44b+NXAOazodHkGUHp4I3zCkQ4/sNVJm56h1S278LMR0bY5xBhztGziSbli8zObIw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.8", - "@babel/types": "^7.29.8", + "@babel/parser": "^7.29.13", + "@babel/types": "^7.29.11", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -317,9 +317,9 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.13.tgz", + "integrity": "sha512-8WxJ4/Vmes3ghacj8VOJy3NW8eyHiFY/4thU/LSBuBE470+igjR/j1ZTNn8fV5VdNrLx6iXUwig526UdOvfP0w==", "dev": true, "license": "MIT", "dependencies": { @@ -334,9 +334,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", - "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.13.tgz", + "integrity": "sha512-fo8/Fr44/DMV71FQjk/HA0Npmu3bo5DFoWTCW1dhUR8Vx3NFfAVeEy0TbcBpt4e+hd2QhdmWgmspHo/9iWj8RA==", "dev": true, "license": "MIT", "dependencies": { @@ -344,8 +344,8 @@ "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", - "@babel/traverse": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.13", + "@babel/traverse": "^7.29.13", "semver": "^6.3.1" }, "engines": { @@ -356,14 +356,14 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", - "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.10.tgz", + "integrity": "sha512-/YY9soQuWa8TURtHXHT+GbklpbAA+dZErxFGHIr6qgDg96xuGWrTAs1+MfjgVJPF0YLQsOVsOYs8lhgsPbOinQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", - "regexpu-core": "^6.3.1", + "regexpu-core": "^6.5.3", "semver": "^6.3.1" }, "engines": { @@ -429,15 +429,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.13.tgz", + "integrity": "sha512-/U96wOz+SCoZ7Da/8kjDGltcNY280JPQcRNPK0Hk3PTbdpaiBDQImtC6JsbVwb1HGMr3usZQU2WsWVjhYNmRzw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" + "@babel/traverse": "^7.29.13" }, "engines": { "node": ">=6.9.0" @@ -506,24 +506,24 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", - "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.13.tgz", + "integrity": "sha512-Abm4rKIH0tmCayJLwcoeOkdzpYUphTbO52fHhyEBHn7S0R+ni4epNLI/8nq1L0DN2Bl0u5qmgWm2Og/z4fBb+Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" + "@babel/traverse": "^7.29.13", + "@babel/types": "^7.29.11" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "devOptional": true, + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.13.tgz", + "integrity": "sha512-fWegr+WsEsiMVW4KUNdIIzzMTZSzNiqhy0//6+2MRi2d5NFNsL/AYwBQW8eBnRbTBVRnDVzjaJEUqKaCWjMMdQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -533,7 +533,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -565,27 +565,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.13.tgz", + "integrity": "sha512-laoRpD/kO/iA4CDlgJbixHekg7oPvWALK0PQyK3Ob2I2njZRvVAD77RfWCIImtqWCq6KijoBoZK7bp/HlTWASQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" + "@babel/types": "^7.29.11" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", - "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", - "devOptional": true, + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.13.tgz", + "integrity": "sha512-fJM50YYjlTiBhgdkGmC1tBpv+FMAyo4annf2X6Aks+0yRlaiVkYW/3f07xAfVVkYIoZ9fFZp+mEThUFnuybhkQ==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.8" + "@babel/types": "^7.29.11" }, "bin": { "parser": "bin/babel-parser.js" @@ -644,14 +644,14 @@ } }, "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", - "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.13.tgz", + "integrity": "sha512-7RpEgFZq9xNRHsWwPLFQpjPOi0ZECE2QZWMBes+vgK7E02krjOfPiYtnHTIUF2nCDyeCJPvSAj0AP4NTVf4hsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.13" }, "engines": { "node": ">=6.9.0" @@ -910,9 +910,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", - "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.13.tgz", + "integrity": "sha512-q7f5zYLtDLqO11ydQK1w2RqZ1m5kmcAlmgZlfXssKgqVyL8+HgDvWXMxvuEz8TuJfq1UNsY9sB6ABjjtQkN6wA==", "dev": true, "license": "MIT", "dependencies": { @@ -960,9 +960,9 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", - "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.11.tgz", + "integrity": "sha512-f8kTDM/nTvpLeafb+TJ4jboFr/Y3BbC+QzgPpPGzSKJqWLnj9LW0WLqv4XKA11LX5sr+EgvLE6Ix3YsCEusodw==", "dev": true, "license": "MIT", "dependencies": { @@ -971,7 +971,7 @@ "@babel/helper-globals": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", - "@babel/traverse": "^7.29.7" + "@babel/traverse": "^7.29.10" }, "engines": { "node": ">=6.9.0" @@ -998,14 +998,14 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", - "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.13.tgz", + "integrity": "sha512-X2meXB39NOPPmmshSRJzyd50hX9uCzbscNQwVCjLCtq363PZIJFFxJIeZPpgAewC570vpdeGIHTcltwd0QUlJg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", - "@babel/traverse": "^7.29.7" + "@babel/traverse": "^7.29.13" }, "engines": { "node": ">=6.9.0" @@ -1130,14 +1130,14 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", - "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.13.tgz", + "integrity": "sha512-dFjiIw+xBYpgLhYDc1QFQOZmMX44se0YCmXUjPsm9SQEj9HaCJAjvVnK6/yK9iE4E1LaKU5fIAKVJPZWCbKwXw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.13" }, "engines": { "node": ">=6.9.0" @@ -1587,9 +1587,9 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz", - "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==", + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.11.tgz", + "integrity": "sha512-5xWGhfuMgbOfIjsk/I2c4uMnoqRwQdKYi4QjvybQrcEVE0G9/p3qmIkp2Sxe8YF8DGNNVs5whfwNqIM5684XoQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1874,9 +1874,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.13.tgz", + "integrity": "sha512-v+oN26xP4YIlQsbQ/CbTH1WG5m/l/DjKKZNy/Kp3f/AFvbgt7Em/D0FeMM3O2hX0YzeN2M16W8lOpyhIkfCBcQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1898,18 +1898,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", - "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.13.tgz", + "integrity": "sha512-0ve0xMlFA73dgY1oyJRY6RoW+/bncdcOQqg6jjYcVfXBK90OdLXjm9R6lmRetdZVrPjXkKIoMVBqYFPjcAlZ4Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.8", + "@babel/generator": "^7.29.13", "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.8", + "@babel/parser": "^7.29.13", "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8", + "@babel/types": "^7.29.11", "debug": "^4.3.1" }, "engines": { @@ -1917,13 +1917,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "devOptional": true, + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-string-parser": "^7.29.11", "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { @@ -1934,29 +1934,29 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "devOptional": true, + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.3.tgz", + "integrity": "sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw==", + "dev": true, "license": "MIT", "dependencies": { - "css-tree": "^3.0.0" + "css-tree": "^3.2.1" }, "bin": { "specificity": "bin/cli.js" } }, "node_modules/@codemirror/autocomplete": { - "version": "6.20.3", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", - "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", + "version": "6.20.4", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.4.tgz", + "integrity": "sha512-yF8RhOf2SXlg00+qtupzLhrglcas61boqaONiNcbPIebHCpJocpqTBA6dyrtUrQO47hqyTdeob/3XnYilT7TaQ==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -2062,12 +2062,13 @@ } }, "node_modules/@codemirror/language": { - "version": "6.12.4", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz", - "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.13.1.tgz", + "integrity": "sha512-zfvU2r3TyylQLMj/te+5CnQ0rSqZi8B1oTCBbQkagcLW4Xc6imab6qN+pEJhsuNOLaJqiRyBXp9UhWA1DhVvDw==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", + "@codemirror/streamparser": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", @@ -2076,12 +2077,12 @@ } }, "node_modules/@codemirror/legacy-modes": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.4.tgz", - "integrity": "sha512-/cZr6qZyl08iYNLGsJ862CXXNI51LryRFRE40ejgoIjXZz0C1rGkD3/Ek5jM/8w1ceRjqtt4qx/KLMh4zBTgew==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.5.tgz", + "integrity": "sha512-9ltT214oMb3Y1w5eAZMSigzrjzZnMI2Wa3Feq73rU/i4C56MkwrpfswfQYOr28C5pXr6dWwspNTMyeHXlzWMxg==", "license": "MIT", "dependencies": { - "@codemirror/language": "^6.0.0" + "@codemirror/streamparser": "^6.0.0" } }, "node_modules/@codemirror/lint": { @@ -2104,10 +2105,16 @@ "@marijn/find-cluster-break": "^1.0.0" } }, + "node_modules/@codemirror/streamparser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/streamparser/-/streamparser-6.0.0.tgz", + "integrity": "sha512-JTFPF6xrfjt0mnfGPgzcLiMwYmvIyNaoizp8BYzv8FE8KrT6GBGDZ190rtt+O7Wke3gG/MFHyiY9YGNeimof4w==", + "license": "MIT" + }, "node_modules/@codemirror/view": { - "version": "6.43.13", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.13.tgz", - "integrity": "sha512-sihaFrUzAsYBQsL9J2t69y8nfMQGwcYmggAZsk+kjPbjYZMyuf2hU8tUNTZ+P+isb6XRr8JE22TZlJxBoVdH1A==", + "version": "6.43.15", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.15.tgz", + "integrity": "sha512-HHsfpqFeHyXLs983pSRe38Xajs0vFWZQ2BuX0m7Ki8w3jZ5iD8cuR3lBz2/5/X53Dhf32SBHKh6rtSTB6tKeGw==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.7.0", @@ -2117,9 +2124,9 @@ } }, "node_modules/@csstools/cascade-layer-name-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-3.0.1.tgz", - "integrity": "sha512-EqmSJE8CcNoGXcgpiOD12PPMeuJZwiGMX69HPoam+GzltcjZjr6THQ0S+8a0NtQuefJtILzoDyQj+fp7+Qiorg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-3.0.2.tgz", + "integrity": "sha512-B731RJ9pfJj3D5GJPp8ccsaaHMdwBmlOb4QWAYQUFpmMxonxJvNeeIoo0E89CjMJaKDvQXy9MKpSXY/i7XgeFA==", "dev": true, "funding": [ { @@ -2136,7 +2143,7 @@ "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2" } }, @@ -2144,7 +2151,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz", "integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -2161,10 +2168,10 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.1.tgz", - "integrity": "sha512-EtC7SoN1j6J4E4DCwg5QgbO5TGxgxIA1RXqe+W+qUM+BUcezx9wT+/tiQ/WO2yCX4i5X+Cuf9ciJ22aP4UEwWw==", - "devOptional": true, + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.3.tgz", + "integrity": "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==", + "dev": true, "funding": [ { "type": "github", @@ -2180,15 +2187,15 @@ "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2" } }, "node_modules/@csstools/css-color-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.4.tgz", - "integrity": "sha512-DyefytAZ735mX4Dq/WcDAXFtXhaEFvme0ZS9tVEBAc2whxUthXr0R0L2rmEPm59SrMBkGrFzQviBXMs/UtnABQ==", - "devOptional": true, + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.7.tgz", + "integrity": "sha512-zgm9yjQz5iJB4VL8RyVwd71TWB8Q89FSxt/NzXoWqPSkEBOVtn9L6vAPtSc9hpKfB0Awo/AL2VKFHjYetBLFHQ==", + "dev": true, "funding": [ { "type": "github", @@ -2202,21 +2209,21 @@ "license": "MIT", "dependencies": { "@csstools/color-helpers": "^6.1.2", - "@csstools/css-calc": "^3.4.1" + "@csstools/css-calc": "^3.4.3" }, "engines": { "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.1.tgz", - "integrity": "sha512-ShL8BqPfbKJrJiKFH0xBbN0i7Nrh9HXYRuF+pzyj93R/BL2YAsUxJeqANErzqM+0JGl7vjHp+3OIgd/DL69YIA==", - "devOptional": true, + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.2.tgz", + "integrity": "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==", + "dev": true, "funding": [ { "type": "github", @@ -2236,10 +2243,10 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.14.tgz", - "integrity": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", - "devOptional": true, + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.16.tgz", + "integrity": "sha512-LsDbJPP/B8cPasUJyTA5S99F7C4G93ngW8hUluXz8CFeOaaRnBPtBiIlvgcPYU0HmHNz8CXg+1vwFrIB4F6Dgw==", + "dev": true, "funding": [ { "type": "github", @@ -2264,7 +2271,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz", "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -2281,9 +2288,9 @@ } }, "node_modules/@csstools/media-query-list-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-5.0.1.tgz", - "integrity": "sha512-DR5NpEOOxDOhmMWPJlhYAV9gTmV4XpfYjtE+aXSCurkxS65KzNogI4wqzRIoujieQcmFkeRtK1nRIF8ImUZTDg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-5.0.2.tgz", + "integrity": "sha512-qx19O8AbbLLbDSAcI6YOby/Vq8rV51psgIvlF5OZT/L8fQJDCyQifL6k+kYU4a8ySpzJ4RBzZkIs530bAn8y4Q==", "dev": true, "funding": [ { @@ -2300,7 +2307,7 @@ "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2" } }, @@ -2327,30 +2334,41 @@ } }, "node_modules/@digitalbazaar/http-client/node_modules/undici": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", - "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", + "version": "6.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz", + "integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==", "license": "MIT", "engines": { "node": ">=18.17" } }, + "node_modules/@dokieli/web-access-control": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@dokieli/web-access-control/-/web-access-control-1.1.0.tgz", + "integrity": "sha512-LId2cNz0pKafc6dwohaX6j+mr886uEV2o8L0VrAWUDxU0LPN4+8Yc8Yx3D94zrsRSJuBWRtQzSjgtEqhYg5y/w==", + "license": "Apache-2.0", + "dependencies": { + "@rdfjs/data-model": "^2.1.1", + "@rdfjs/types": "^2.0.1", + "n3": "^2.2.0" + } + }, "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-beta.2.tgz", + "integrity": "sha512-MzCVoWtcg4Pm2B3je5mMvYMvj3MmBKw+fU7vbsJc9/iT8ip6/eYIHo/Dsm/mr+wsHB6SEyJDuxB09Yy6r/8+4A==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "2.2.0", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-beta.2.tgz", + "integrity": "sha512-JcrSPzPvZuNZvJqPTF9Fw25LbOJ8avzFVlaf2grmH4Cku0oGP47tRgqPZadcfj3YoZMHkFVnCYFbQM8I+i5QEw==", "dev": true, "license": "MIT", "optional": true, @@ -2359,9 +2377,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.2.0.tgz", + "integrity": "sha512-JP7OYH38Q7+pyTAe14raZHLQIK7BDKbdwji+Xi3pcd/6QkJYQH3SX4wu6EplGrGJWRepoNzedfm0G3B9Z4e9Rw==", "dev": true, "license": "MIT", "optional": true, @@ -2376,6 +2394,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2392,6 +2411,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2408,6 +2428,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2424,6 +2445,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2440,6 +2462,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2456,6 +2479,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2472,6 +2496,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2488,6 +2513,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2504,6 +2530,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2520,6 +2547,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2536,6 +2564,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2552,6 +2581,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2568,6 +2598,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2584,6 +2615,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2600,6 +2632,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2616,6 +2649,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2632,6 +2666,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2648,6 +2683,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2664,6 +2700,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2680,6 +2717,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2696,6 +2734,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2712,6 +2751,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2728,6 +2768,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2744,6 +2785,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2760,6 +2802,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2776,6 +2819,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2818,9 +2862,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.3.tgz", + "integrity": "sha512-R76UKbvsqI68DJJVTVyqFKqU98CN4+B+acxNfEA2IFarSzP0SjtKf/3+fFiXzTyv5Fj3PxLkGHe2wnVWWMxobQ==", "dev": true, "license": "MIT", "engines": { @@ -3008,7 +3052,7 @@ "version": "1.16.0", "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz", "integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" @@ -3217,7 +3261,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -3227,13 +3271,14 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3247,15 +3292,15 @@ "license": "MIT" }, "node_modules/@lezer/common": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.3.tgz", - "integrity": "sha512-H0iErY4e43LpXbYDyBci5W4v/RwTgGV3YzYOlJPiGZ8RY8w51kVE+xLn1tg3Z6UmSfyETvtsUK3r7YsgDQEI7Q==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.5.tgz", + "integrity": "sha512-Pw7PVCndruF1pb3ebCxpSVpkMHSDjwRCLcTRw+RizUKco5QbWylwq2g4lnG/RYN7yTTQDXE9F77MwtkAyphrjg==", "license": "MIT" }, "node_modules/@lezer/css": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.8.tgz", - "integrity": "sha512-EJn1zcL9qoDptief6ipWKZKLiOpXkxSe0+t8CH9oiMVcZlq7NBWrjCqnc/41EIjeo/ITj1gFFiATdTkaJDL+Og==", + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.9.tgz", + "integrity": "sha512-ZmzgvTOBdJWGIsY/ItAfN7UW8DV802oZG+jeG6E508SwEzz5YhuAajptpWPICvIyqYAD2+VjMdOS40GgGkyEOQ==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -3284,9 +3329,9 @@ } }, "node_modules/@lezer/javascript": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.5.tgz", - "integrity": "sha512-sWg4yX1J6XW67AaAynVt0iwF0M5c+np36TEu+P2ifAJ8haRYvHnWDV28r1jdwnJehWCwXECutAUy56K4RBZIyg==", + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.6.tgz", + "integrity": "sha512-sEvOqpl4vJQca0NugSQK02vQPbc5KrvoojnYGoqqFgFO0MtcYhwGiHKVEB+LuXYJnfyXo4QPA8ztbrEa4Fw5NA==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -3306,18 +3351,18 @@ } }, "node_modules/@lezer/lr": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", - "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.12.tgz", + "integrity": "sha512-On9HU4oMMiHPMkQDgoTnGahjgG5n/MkN38CgG77sMraKLL+3JBbeQNi1FcCXV9QRqwF9/BUXP3caHyJC1fBOTQ==", "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" } }, "node_modules/@lezer/markdown": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.7.2.tgz", - "integrity": "sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.8.1.tgz", + "integrity": "sha512-M5YmEcKWO4mJ45Y1hECwqyu8cLv25EiXOK554PVOvDOWOP0olwelHHQUWr9sjF+d1bENVhy0nfJ8E57XZUTiXg==", "license": "MIT", "dependencies": { "@lezer/common": "^1.5.0", @@ -3442,27 +3487,27 @@ } }, "node_modules/@mswjs/interceptors": { - "version": "0.39.8", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.8.tgz", - "integrity": "sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==", + "version": "0.45.11", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.45.11.tgz", + "integrity": "sha512-+ISPERIRIHhSVnFQRGCP9jDJXxn0/z2+pWWbXUym6JUfpUCRW2Pvro4P9RBKGYUKTKAULszKurKMTQIsaJUIgQ==", "dev": true, "license": "MIT", "dependencies": { - "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/logger": "^0.3.0", - "@open-draft/until": "^2.0.0", + "@open-draft/until": "^3.0.1", + "@types/debug": "^4.1.13", + "debug": "^4.4.3", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", - "strict-event-emitter": "^0.5.1" + "rettime": "^0.11.12" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz", - "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.6.tgz", + "integrity": "sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw==", "dev": true, "license": "MIT", "optional": true, @@ -3477,8 +3522,8 @@ "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + "@emnapi/core": "^1.7.1 || 2.0.0-beta.2", + "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2" } }, "node_modules/@nicolo-ribaudo/chokidar-2": { @@ -3516,31 +3561,16 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@open-draft/deferred-promise": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@open-draft/logger": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "node_modules/@open-draft/until": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-3.0.1.tgz", + "integrity": "sha512-s7/9ELP4aP9YZtW7RJaa0Xf3RISaRH9+EFN18FtykJYRHGNrl8f9ymvoNXzjhrjmftFvkQudtuDU9RYhk0xs3A==", "dev": true, "license": "MIT", - "dependencies": { - "is-node-process": "^1.2.0", - "outvariant": "^1.4.0" + "funding": { + "url": "https://github.com/sponsors/kettanaito" } }, - "node_modules/@open-draft/until": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", - "dev": true, - "license": "MIT" - }, "node_modules/@oxc-parser/binding-android-arm-eabi": { "version": "0.127.0", "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.127.0.tgz", @@ -3856,6 +3886,40 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@oxc-parser/binding-win32-arm64-msvc": { "version": "0.127.0", "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.127.0.tgz", @@ -3908,10 +3972,12 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.151.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", - "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "version": "0.157.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.157.0.tgz", + "integrity": "sha512-95iTox1qdt6WGWce3xwNOVayByOfLaYAq+qGPpub6NWbBh9sv0GG3+zyfh9ZBCO/jVu5A0EWI+qQI7g6fevjxg==", + "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/oxc-project" } @@ -4254,6 +4320,15 @@ "parse5": "^7.0.0" } }, + "node_modules/@rdfjs/data-model": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@rdfjs/data-model/-/data-model-2.1.2.tgz", + "integrity": "sha512-yoSVlyCltqbaCTse3uSdf5Vo3q4UskcTm+3klaUQTwOq7Z5aCZLtLPZatXTPzr+S0WEcjTZvD++A+r3QmgamsA==", + "license": "MIT", + "bin": { + "rdfjs-data-model-test": "bin/test.js" + } + }, "node_modules/@rdfjs/types": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-2.0.1.tgz", @@ -4264,108 +4339,121 @@ } }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", - "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.3.1.tgz", + "integrity": "sha512-yeCTS2g0J/CXg0QirfJdet9BW/vhx3UM7Nv8Cz0Cglh82e/7F+8ss2kYWx5xt+wbsFK0LV4485+PZxW0bs5udQ==", "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", - "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.3.1.tgz", + "integrity": "sha512-8RCJ1AODBZ7luSDvVSabNtDkxrRL1U4+kr9UG7X2GMrdPRD4gN07o8X61LouA8JSy8vXkgd8BIEpxXsP3WokgA==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", - "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.3.1.tgz", + "integrity": "sha512-/oKBcpu993n68qe/Jn6mdc+CwtZLCFY34aCoG9PuWvBEMmGunyGAmrxhI5R78jasyJXXT9BX2pCzwQMT6Zdj8Q==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", - "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.3.1.tgz", + "integrity": "sha512-vCmEnX+/BEVN+ar/jes4t+04RjQNrBoAd7PyQN7Wyklo+W1H1juKhcmlwjGaHQbZLdsYglZ8pBRwlQc+u5XLWA==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", - "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.3.1.tgz", + "integrity": "sha512-/76gLsW8sjHDcLFvqsXKWOKCtfzyUUuEmKP2jz6twjGVaVAkULflI7ny+NDE955RX0xYR4qLzvk+g8IgnYHsNQ==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", - "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.3.1.tgz", + "integrity": "sha512-oci8r+L/3490VrKGcbJcqweykeFdV4DPn3FUxitWmzZQeGmsZCZ0oeCTU1XK3nV3UeqCVNAZy4lXsYE3RyCPHg==", "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", - "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.3.1.tgz", + "integrity": "sha512-tcMHkX6NWVRE6Aqr3zXjpAvP9DIMxcifdaCvaTEVpdqRZn1EAeP3PcUtABxmJHU60PJB+dH3uMR/vsHsoAxdYA==", "cpu": [ "arm64" ], + "dev": true, "libc": [ "glibc" ], @@ -4374,17 +4462,19 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", - "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.3.1.tgz", + "integrity": "sha512-rH04pFXdQfVI71O4U/yrcQlEmQAhUzuG9kNGhNdxGSAxtxB45lQ7XpuXmIV53eQTEjfWyl9NmAV9MOE/fux2AQ==", "cpu": [ "arm64" ], + "dev": true, "libc": [ "musl" ], @@ -4393,17 +4483,19 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", - "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.3.1.tgz", + "integrity": "sha512-yXmS9FIJXFEIj58cey6eNtspMmi8/yeKDrqsKZ6taHnM+DB2HX5eGjdNV3V8anfThOch+zKL0TaBZIg/TeH6Vw==", "cpu": [ "ppc64" ], + "dev": true, "libc": [ "glibc" ], @@ -4412,17 +4504,19 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", - "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.3.1.tgz", + "integrity": "sha512-JSATRKvP4es7A0hGhuj4EQtZIdlACqWDxeIg59vPAvo9APsyiFmWqdFdPjeihRQx1gmuLw8mD6r+bUEIzitP3g==", "cpu": [ "s390x" ], + "dev": true, "libc": [ "glibc" ], @@ -4431,17 +4525,19 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", - "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.3.1.tgz", + "integrity": "sha512-9+XaosVKfDsDIWUOoZwL1bdHD+52sZFbtbft67oGcgt5qDUq+0HJGGxZA2iD/YA0w7mPB2JrThjP0C/4bg/2+A==", "cpu": [ "x64" ], + "dev": true, "libc": [ "glibc" ], @@ -4450,17 +4546,19 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", - "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.3.1.tgz", + "integrity": "sha512-zmTIXU38DaLB10om1EkwdbwBBTqiVB2IDmPXpiucbFwg9qtfQyxZktMIbrJkO/NFbqP5xiGsTaqSmhrdhpSt5Q==", "cpu": [ "x64" ], + "dev": true, "libc": [ "musl" ], @@ -4469,54 +4567,81 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", - "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.3.1.tgz", + "integrity": "sha512-hTJfT0txdxpVKSZAmrUsCSPZD7Ewh3PCtSWpLOJYkR7nYzJRTGzlyPsxMMk4yvry7RxCGvB3keEDUfxQM0/O1g==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "openharmony" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.3.1.tgz", + "integrity": "sha512-uTcfNRCMeAQLqGi8a/+TApIkSeqrj1qx3t2sxAWvlN65sCLyg8z68zsCQLyYKEAQPPaHrEWqiceMah8Zd9U01A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/core": "2.0.0-beta.2", + "@emnapi/runtime": "2.0.0-beta.2", + "@napi-rs/wasm-runtime": "1.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", - "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.3.1.tgz", + "integrity": "sha512-vWby/lJJgE16EBaO96mg30Rj1JMT5vI/tV/bOBL5atHTO58T8Rfude/aqUyG+iALSn3NMcO2TI9fFAtPUA4/Iw==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", - "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.3.1.tgz", + "integrity": "sha512-9mPxCYnOQ/Cu+YW/P18Ldxu9rmLFhRIxr8kSHjYwolgpUlzfAHoe+ykHP1WupPRR11+R9erOdxG7LIE8gd9YpA==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -4553,15 +4678,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.1.0.tgz", + "integrity": "sha512-K+NU2HDCEem4sClgHH4ZZXwdJeNFkJo+hRk479DIwdIviAPN2hFJH1KHHwg8CxHQXvi0M5XY4deTXINfIiSG6w==", + "dev": true, "license": "MIT" }, "node_modules/@rollup/pluginutils": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", - "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.5.1.tgz", + "integrity": "sha512-P9ZvnfouRu4MSC+ilERaw6FmZs8zqKhpHxEs4aGDGWQAMzQGXnhvCSvoNwokfNvdt7t3Y5Y4p6gStQgezeIqug==", "dev": true, "license": "MIT", "dependencies": { @@ -4638,9 +4764,9 @@ } }, "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.1.0.tgz", + "integrity": "sha512-cVBuhDQjBZaNMTwBquYfPydrUP+E3hE6oYGasx8h6ZrODIM7/r+gxLDXdd66oszbrggZHEntdE6j+5gYIVyMWQ==", "dev": true, "license": "MIT" }, @@ -4664,6 +4790,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/addon-docs": { @@ -4757,9 +4884,9 @@ "license": "MIT" }, "node_modules/@storybook/icons": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-2.1.0.tgz", - "integrity": "sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-2.2.0.tgz", + "integrity": "sha512-FpUB/xizIrV1ILhCIYFo+TtQNN9YEoLjfcnAW0JtOXL+BcM2aisSysI0B54JMWDnljriXySBHgN6ps4s9lb75A==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4865,56 +4992,56 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", - "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.5.tgz", + "integrity": "sha512-yT53FzV4ke0cr/BguDxhWMpm5MJu1O2muzk+tKw6lKHjODAsYArBbWwFbJiaksfzFv1tpkO9QHGzmfs1hJT66g==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.24.1", + "enhanced-resolve": "^5.24.5", "jiti": "^2.7.0", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", + "lightningcss": "1.33.0", + "magic-string": "^1.1.0", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.3" + "tailwindcss": "4.3.5" } }, "node_modules/@tailwindcss/node/node_modules/tailwindcss": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", - "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.5.tgz", + "integrity": "sha512-FcQk9BuffmEHJP2G0XmB2SplU1aLTI7j07Kunk+U74Ny+XiWOtzXz9aujFZ/avntw1e2cOv/3PkgzflQ7gX2vQ==", "dev": true, "license": "MIT" }, "node_modules/@tailwindcss/oxide": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", - "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.5.tgz", + "integrity": "sha512-PynqGLZsZvwM9tqD/CUxkOIfyd6K5elZ2MooauzJryP/UqXYgi3yYbtt3N0Xe+MHV/eFKPFQCEfIKAqlAS1CBg==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.3", - "@tailwindcss/oxide-darwin-arm64": "4.3.3", - "@tailwindcss/oxide-darwin-x64": "4.3.3", - "@tailwindcss/oxide-freebsd-x64": "4.3.3", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", - "@tailwindcss/oxide-linux-x64-musl": "4.3.3", - "@tailwindcss/oxide-wasm32-wasi": "4.3.3", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + "@tailwindcss/oxide-android-arm64": "4.3.5", + "@tailwindcss/oxide-darwin-arm64": "4.3.5", + "@tailwindcss/oxide-darwin-x64": "4.3.5", + "@tailwindcss/oxide-freebsd-x64": "4.3.5", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.5", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.5", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.5", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.5", + "@tailwindcss/oxide-linux-x64-musl": "4.3.5", + "@tailwindcss/oxide-wasm32-wasi": "4.3.5", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.5", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.5" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", - "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.5.tgz", + "integrity": "sha512-Se0z5xMqcSQanJD1IYmO0cGpK3iv0fp3ZsIT7xRJZ1Da5PghvNoc46zz4jIQn+aan3dV9T8AermFNN9QPmsh0Q==", "cpu": [ "arm64" ], @@ -4929,9 +5056,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", - "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.5.tgz", + "integrity": "sha512-UPrgTDuUbvmqGMZuCnmI3FKee/tYKzKdp00/kuB4oSGB213VYg8QTdF7lVhnPoBXq/mIfJ+ILoBFqNOboWcMwQ==", "cpu": [ "arm64" ], @@ -4946,9 +5073,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", - "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.5.tgz", + "integrity": "sha512-E+p7HEtjI0b8D7kbbWxH067cGui6d4c79COGko9fAPVE3rwc1E7ld2HFCbaG86AOGWS5VKQYqZOaf38ya4uxTQ==", "cpu": [ "x64" ], @@ -4963,9 +5090,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", - "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.5.tgz", + "integrity": "sha512-yWEWcWyAG7rWOcTpJTjdhQ3pcaFY2h+0bqTRfgU3G2fz87ZU3AKdvdZKhslD+kyHnjBUWKvNdRYwIYC5dI1tIg==", "cpu": [ "x64" ], @@ -4980,9 +5107,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", - "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.5.tgz", + "integrity": "sha512-WmeVjtJhZlr+smpqcdR2kQkkn6arHIJnUIBxTO2EJcTLGY1ecVXNo2hbSn8xc0IH2aEtys936iMWKGyj0GaKnA==", "cpu": [ "arm" ], @@ -4997,9 +5124,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", - "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.5.tgz", + "integrity": "sha512-DTCSyucc6ShMp1QMqV5R94Z7xlgNgUzkGmFF1rL5HH2Qjy4eSmEqy+YLGkqqYvWa4sILtgrBt1JjRKbneQtiFQ==", "cpu": [ "arm64" ], @@ -5017,9 +5144,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", - "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.5.tgz", + "integrity": "sha512-H9lsvO4x9E4nE4Hi8h7NHHw0O/kcVSu+iOyFHNGdYzazGgDjFHZbE6S3hQ8kMcbNXSBlMdNi67LV5EVqtpaJtw==", "cpu": [ "arm64" ], @@ -5037,9 +5164,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", - "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.5.tgz", + "integrity": "sha512-4uxQvtBvPuIdl13WfUBj9xMZHH14PtHZ8OZdtLmdyDtG66sCa1t599NZT+jagkUzRtWhuul1qRZQ1nAJscXN5g==", "cpu": [ "x64" ], @@ -5057,9 +5184,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", - "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.5.tgz", + "integrity": "sha512-0JQAzs7jwbA7O6+z/adU+Jk3AXwsu3npAhtr5escXS7TqEqWcaDV7qHuI+0Vy/KQRuVGnLQREcSJWYauyV3oqg==", "cpu": [ "x64" ], @@ -5077,9 +5204,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", - "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.5.tgz", + "integrity": "sha512-OJhFmlEfdzMr+avACdi40RPtnkW0B6hXyxL/8r+tnEHBWZji36CYRjEXrrvgBmaYsD3oEOBqwYFRKsYvzFe7Bw==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -5088,28 +5215,94 @@ "@emnapi/wasi-threads", "tslib" ], - "cpu": [ - "wasm32" - ], "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.11.1", - "@emnapi/runtime": "^1.11.1", - "@emnapi/wasi-threads": "^1.2.2", - "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.2", + "@emnapi/core": "^1.11.3", + "@emnapi/runtime": "^1.11.3", + "@emnapi/wasi-threads": "^1.2.3", + "@napi-rs/wasm-runtime": "^1.2.2", + "@tybys/wasm-util": "^0.10.3", "tslib": "^2.8.1" }, "engines": { - "node": ">=14.0.0" + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", - "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.5.tgz", + "integrity": "sha512-9o8jgthi+QE5eandnboiJT64VQfnIArfZzSTdgbhEK5aafdiu2jTW9Honi/mwYHxBZuH1vAC57IFyrh+0iK5Gg==", "cpu": [ "arm64" ], @@ -5124,9 +5317,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", - "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.5.tgz", + "integrity": "sha512-XiKFfIy1RWe7qK4WqByxNW7Oz1/nkOy8o/BWhHminyV1PmpUd8Eedj5v2A8h2kpqeVlz6xRnwk9Fr/ZXNLcZfg==", "cpu": [ "x64" ], @@ -5141,24 +5334,24 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", - "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.5.tgz", + "integrity": "sha512-BCNiY0a1ZjdOHVqw7kyb7U72BwfMF3dfUuN+tQsLWky/J5JOI/r7abibglXfygrZhVO/3u3V+E25kkKTeMkXaQ==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.3.3", - "@tailwindcss/oxide": "4.3.3", - "tailwindcss": "4.3.3" + "@tailwindcss/node": "4.3.5", + "@tailwindcss/oxide": "4.3.5", + "tailwindcss": "4.3.5" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "node_modules/@tailwindcss/vite/node_modules/tailwindcss": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", - "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.5.tgz", + "integrity": "sha512-FcQk9BuffmEHJP2G0XmB2SplU1aLTI7j07Kunk+U74Ny+XiWOtzXz9aujFZ/avntw1e2cOv/3PkgzflQ7gX2vQ==", "dev": true, "license": "MIT" }, @@ -5248,22 +5441,35 @@ "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, "license": "MIT", "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", + "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", + "dev": true, "license": "MIT" }, "node_modules/@types/hast": { @@ -5290,9 +5496,9 @@ } }, "node_modules/@types/jsdom/node_modules/entities": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", - "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-9.0.0.tgz", + "integrity": "sha512-oJFNDJg3kTPwAD1cNn2+WmUq4wFoo09npc3Ydf1D7ED4smRiWrsKnaLxykrTWgyGEcusc87P5dxlAleol9kpkg==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5303,13 +5509,13 @@ } }, "node_modules/@types/jsdom/node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.1.1.tgz", + "integrity": "sha512-EcR/6WDcU7b2N3vQSxKALCk9lXGtUMQxXio008OEMBE0kavu/7JLfBEeFPzi43nMqVDSbEn3QCK+BMCc9zO//w==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^8.0.0" + "entities": "^9.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -5336,10 +5542,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "24.19.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.0.tgz", - "integrity": "sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==", + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", "license": "MIT", "dependencies": { "undici-types": ">=7.24.0 <7.24.7" @@ -5382,17 +5595,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.71.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.0.tgz", - "integrity": "sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.2.tgz", + "integrity": "sha512-222PKFXKihmFpi/pS5njoBgdAqsPhj2/GBmFbrNw8Tq8gEG9Sv+kSlKFy4u2q+mJv9ZzJgM2Gkv/bUzRZwZw5g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.71.0", - "@typescript-eslint/type-utils": "8.71.0", - "@typescript-eslint/utils": "8.71.0", - "@typescript-eslint/visitor-keys": "8.71.0", + "@typescript-eslint/scope-manager": "8.72.2", + "@typescript-eslint/type-utils": "8.72.2", + "@typescript-eslint/utils": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -5405,15 +5618,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.71.0", + "@typescript-eslint/parser": "^8.72.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.10.tgz", - "integrity": "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", "dev": true, "license": "MIT", "engines": { @@ -5421,16 +5634,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.71.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.0.tgz", - "integrity": "sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.2.tgz", + "integrity": "sha512-llP+T1YuJOLlxEciP6OyQGtz5SMxyn+9LA2BvZw5RmToGVa1y4beEDVQL6Gje7T5WFYh19WREjDCN9Ev4gm5AA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.71.0", - "@typescript-eslint/types": "8.71.0", - "@typescript-eslint/typescript-estree": "8.71.0", - "@typescript-eslint/visitor-keys": "8.71.0", + "@typescript-eslint/scope-manager": "8.72.2", + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2", "debug": "^4.4.3" }, "engines": { @@ -5446,14 +5659,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.71.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.0.tgz", - "integrity": "sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.2.tgz", + "integrity": "sha512-6y5dLTZdHhI8Eh3yIQ/bfQhRKwn5e+TpJj64o18Cngc0iW/BCrVvrr6zGYqXPnYH31u6YXAYiFh0+qxX5mybJw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.71.0", - "@typescript-eslint/types": "^8.71.0", + "@typescript-eslint/tsconfig-utils": "^8.72.2", + "@typescript-eslint/types": "^8.72.2", "debug": "^4.4.3" }, "engines": { @@ -5468,14 +5681,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.71.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.0.tgz", - "integrity": "sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.2.tgz", + "integrity": "sha512-iIoMJ6/8c1/QpFNyFjU0d1YzPco4MsZjQw0k3V+vfzNWAFmUmvhlyMoYEoYYDQZXc+J6Gi1X5XcxlzW2taIIGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.71.0", - "@typescript-eslint/visitor-keys": "8.71.0" + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5486,9 +5699,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.71.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.0.tgz", - "integrity": "sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.2.tgz", + "integrity": "sha512-+h3iCgZXuTNtoLXcnnjP12ZUHLMqoUcIHPfQrFEXTUqvb9dF8QK9b5nv5VtSo5fLkdHuks8ehbTzZj3rImAm9w==", "dev": true, "license": "MIT", "engines": { @@ -5503,15 +5716,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.71.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.0.tgz", - "integrity": "sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.2.tgz", + "integrity": "sha512-STfkR38qIdhfkLpgtXnUjAzsNy4xouqiBO0lrVc/Wpk9Kem5/aHXUx1J8dGC16zpdYN1+yc2ILQ28er1kXX7ag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.71.0", - "@typescript-eslint/typescript-estree": "8.71.0", - "@typescript-eslint/utils": "8.71.0", + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2", + "@typescript-eslint/utils": "8.72.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -5528,9 +5741,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.71.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.0.tgz", - "integrity": "sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.2.tgz", + "integrity": "sha512-Xfn37OLZx03+PCU7qWx9DTv2FCRh1E6fk2+wONeNY15HXIwtcwuveaSwVpGAUptkNmPQegRxW5JyRel9T7wrog==", "dev": true, "license": "MIT", "engines": { @@ -5542,16 +5755,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.71.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.0.tgz", - "integrity": "sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.2.tgz", + "integrity": "sha512-pQEGqIK/cqEvKivVWaFLYpe7XYejFxfuAOi4mu1oAWFvIo36aRHI9esRFTUxcIy/z8N482RJyH+LpZB0ZIU7Ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.71.0", - "@typescript-eslint/tsconfig-utils": "8.71.0", - "@typescript-eslint/types": "8.71.0", - "@typescript-eslint/visitor-keys": "8.71.0", + "@typescript-eslint/project-service": "8.72.2", + "@typescript-eslint/tsconfig-utils": "8.72.2", + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -5570,9 +5783,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", "dev": true, "license": "ISC", "bin": { @@ -5583,16 +5796,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.71.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.0.tgz", - "integrity": "sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.2.tgz", + "integrity": "sha512-hv3eRYbpPbbpezuax1EfGrzpa3C4ySgNZkPUkLOWOAjU8HsOKowEDAr8C7nsNl1C+SBSKDpvno6VKHnlhvp9iA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.71.0", - "@typescript-eslint/types": "8.71.0", - "@typescript-eslint/typescript-estree": "8.71.0" + "@typescript-eslint/scope-manager": "8.72.2", + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5607,13 +5820,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.71.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.0.tgz", - "integrity": "sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.2.tgz", + "integrity": "sha512-NAGrjn0Pitjqzq/GkgMC4kBYm233PQ8GffyQ9+kTA3AlNQYEzOf25EIM02BAHZv5FRJSpHpJk+OGUxVkOB93GQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/types": "8.72.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -5658,9 +5871,9 @@ } }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", - "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.13.0.tgz", + "integrity": "sha512-60E5mvmIiW+i1E/HYeSX4iE4A8s9NJSGYPu7UM0DJght1c2tTkWwuyDyAwzsEXhKM/XbbndfnTiXdfrf27d2hw==", "cpu": [ "arm" ], @@ -5672,9 +5885,9 @@ ] }, "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", - "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.13.0.tgz", + "integrity": "sha512-qlh/JR3Tuy1cJ7xtf5pR4ykvLiK6os7X7EL2+zXZbrVc/s+yB8DoFdQrCAW9/a2TOxPVbR7QVNpkPWh7UWIsEQ==", "cpu": [ "arm64" ], @@ -5686,9 +5899,9 @@ ] }, "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", - "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.13.0.tgz", + "integrity": "sha512-yynnnnJXUC99WeD1GfvuXPKsKNfvgzGYC2eRayFnUfsHVROjqrbFOkFQLZiSx1H8+PLy5BzU8vqyPyLqbRfeBQ==", "cpu": [ "arm64" ], @@ -5700,9 +5913,9 @@ ] }, "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", - "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.13.0.tgz", + "integrity": "sha512-t8PXE0Ip9tmGyhRlfVsnEbzGM5iSvJIeNIXciUACjzpg0mmPs13fGv2x7W8sBa0S8zbIiohxKXS9fL/LXW/ERQ==", "cpu": [ "x64" ], @@ -5714,9 +5927,9 @@ ] }, "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", - "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.13.0.tgz", + "integrity": "sha512-qdtl9GDWDoDx6rhzF4RU5SJutV3lcLOiTiyqD6nBxYfEO6lAgLNukfrA+HJ2u2OiyWvV8JW5iEWD3LoONw7lYA==", "cpu": [ "x64" ], @@ -5728,9 +5941,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", - "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.13.0.tgz", + "integrity": "sha512-VrMUVwdrScTarsh4B6aUhTtziUavSlglo4Hi8XZ6CpAe4+j3nquY+XoP+tYkiSBDsXZ8386ne4pv4YYYBNi02w==", "cpu": [ "arm" ], @@ -5742,9 +5955,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", - "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.13.0.tgz", + "integrity": "sha512-zSbTVwaxiTC+zYJ9xVNTC6c+BYO/kOV/odcTI3bHuFFXa+XmjLoUkb/8UxsZeU5YF4K000m17wrXoTblNWuzOg==", "cpu": [ "arm" ], @@ -5756,9 +5969,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", - "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.13.0.tgz", + "integrity": "sha512-Z/Rax87moZHsuvJ8awdfP+zrMlsCt3d7mggUPIiPe32KpeagimyzJWnZSMQoV67an9uVQghyh92Pc0VprITnYA==", "cpu": [ "arm64" ], @@ -5773,9 +5986,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", - "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.13.0.tgz", + "integrity": "sha512-oUzXz8hNvFLNEAag8lBKJe1euiV2dmGHAR+eQ5fNpsZO0F1K7IMRb4J6pqYsd7uLMRQNPiu5A1WKUJHbFLWbbw==", "cpu": [ "arm64" ], @@ -5790,9 +6003,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", - "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.13.0.tgz", + "integrity": "sha512-BxhGYvZBg0YrvNOeSEGR3eNklwXYy0mUJk7oeGJvZoQMKM68zYvbbf52+fQI3Le4X0fW276FTKIoTwjpMob80A==", "cpu": [ "loong64" ], @@ -5807,9 +6020,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-loong64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", - "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.13.0.tgz", + "integrity": "sha512-nZcIuup9Ar3gAlqoZfX/KW6x83TMI11tLpCjldZG5M1KeY8qNUPNJJ71+sPvG8HMBpoh+ix1pAm5gkMFvcSAcQ==", "cpu": [ "loong64" ], @@ -5824,9 +6037,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", - "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.13.0.tgz", + "integrity": "sha512-AG6AgdDrkRNjAPwD/cftFJGgZUWCiJX+E8vpYCDEnwFHLTHZ7ppBiU6/vyPPwrfjcPUPQCDbbxE/eZUXsx49jQ==", "cpu": [ "ppc64" ], @@ -5841,9 +6054,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", - "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.13.0.tgz", + "integrity": "sha512-6vOqYGcAmzpRWpUURWutzvpf1qJ6eOu5cfYObdtPJnzZ0+Kw4cw+ZkZJqIYEh8fPuTBwSYDL/FwlUhma1G/JVQ==", "cpu": [ "riscv64" ], @@ -5858,9 +6071,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", - "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.13.0.tgz", + "integrity": "sha512-4HeIn0S2KVUowK0A+EweXrDvZ8CKHEfTtif0kx9vt3ORNsqcXNFFIFnHE0tizma3/Gt5OGrU2ovYNuj+D/bN4g==", "cpu": [ "riscv64" ], @@ -5875,9 +6088,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", - "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.13.0.tgz", + "integrity": "sha512-JfkvqY7NZbxyhexLE+g6Z8Hsx4utHOGaVazAVghPiBn8Upw2iERZPpKYnW1FBVU4WStv/BiIsoK/Muke0E7asA==", "cpu": [ "s390x" ], @@ -5892,9 +6105,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", - "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.13.0.tgz", + "integrity": "sha512-iwjLeVdZo/8v5OKAeRIC+4VQV045t1EPZr5B/AVLgUQ04IdtNthWzg6not2cNjZgH2gHpSf4fOXMmN83aLO1FA==", "cpu": [ "x64" ], @@ -5909,9 +6122,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", - "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.13.0.tgz", + "integrity": "sha512-TOh1fqOjyq1OPoht3BDLd7Y+HeMJRwd5g2VNmCQclknEbjp4py5GFRmT4L/rOybF5Af8q0LT9zHs2WGkwlqtrw==", "cpu": [ "x64" ], @@ -5926,9 +6139,9 @@ ] }, "node_modules/@unrs/resolver-binding-openharmony-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", - "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.13.0.tgz", + "integrity": "sha512-3T2MNtF2dd1yjaXCVqrsi+/3VZeksEzU1hygPzqqkBLVVjn+2iJzXEkxxCKIVFrWnpmEbazeHJp/ruYD0RUVWA==", "cpu": [ "arm64" ], @@ -5939,52 +6152,10 @@ "openharmony" ] }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", - "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", - "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.13.0.tgz", + "integrity": "sha512-hKWfSLiN3L4cIxg0jrpyP/66AXLTcyD37jmSn//rGdOUYGlizYxTEFdJ+GZbp2NnaML3YS/A73nCysze8FaJdg==", "cpu": [ "arm64" ], @@ -5996,9 +6167,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", - "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.13.0.tgz", + "integrity": "sha512-N4gDKkDoiyIQEcn3vTqe2ksRxUAtBvQ8c5TiT+WonSa6FHgCAY5+RggnZ134hXJoRj57JEjv4/m+i/pAKHWrTQ==", "cpu": [ "ia32" ], @@ -6010,9 +6181,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", - "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.13.0.tgz", + "integrity": "sha512-DpLMlAqlrk4zBVK3DiDjwKD0oq7pZNy+8BiIBRuByqb6dm5jsdoLMhIiyh/xR7+WejaW/1Ui+XgbgMNa4+zWJg==", "cpu": [ "x64" ], @@ -6033,14 +6204,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", - "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", - "devOptional": true, + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.12.tgz", + "integrity": "sha512-6S7/iVknKglLAxMFxQGNppBI0iqJS2NgM6FBxavitj7KaDq/vywOWGf6dGu7F2L1Qy0G6S3uTp7aGshHrzK8JA==", + "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.11", + "@vitest/utils": "4.1.12", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -6054,8 +6225,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.11", - "vitest": "4.1.11" + "@vitest/browser": "4.1.12", + "vitest": "4.1.12" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -6119,12 +6290,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", - "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.12.tgz", + "integrity": "sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==", + "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.11", + "@vitest/spy": "4.1.12", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -6145,18 +6317,30 @@ } }, "node_modules/@vitest/mocker/node_modules/@vitest/spy": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", - "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.12.tgz", + "integrity": "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==", + "dev": true, "license": "MIT", "funding": { "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/@vitest/pretty-format": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", - "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.12.tgz", + "integrity": "sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA==", + "dev": true, "license": "MIT", "dependencies": { "tinyrainbow": "^3.1.0" @@ -6166,12 +6350,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", - "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.12.tgz", + "integrity": "sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==", + "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.11", + "@vitest/utils": "4.1.12", "pathe": "^2.0.3" }, "funding": { @@ -6179,13 +6364,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", - "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.12.tgz", + "integrity": "sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==", + "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.11", - "@vitest/utils": "4.1.11", + "@vitest/pretty-format": "4.1.12", + "@vitest/utils": "4.1.12", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -6193,6 +6379,16 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/@vitest/spy": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", @@ -6207,12 +6403,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", - "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.12.tgz", + "integrity": "sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==", + "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.11", + "@vitest/pretty-format": "4.1.12", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -6221,32 +6418,62 @@ } }, "node_modules/@volar/language-core": { - "version": "2.4.28", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", - "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "version": "2.4.31", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.31.tgz", + "integrity": "sha512-OM+OZfynHa1/4WlC8klqvwTQVmhYUZuwpAKdfl/rlvK8jaIozMD5qwXsOkxcRJA2tC+TEFsG1K81lNP4s/IAzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.31" + } + }, + "node_modules/@volar/language-service": { + "version": "2.4.31", + "resolved": "https://registry.npmjs.org/@volar/language-service/-/language-service-2.4.31.tgz", + "integrity": "sha512-LM9HFsBVhHZ7RbFUZK9P23FvamK8XCDcN0kNgOTts02wVgnygo3ReLbmBmUZ3GuYnrYn6HHoQvqk/eHIzzTnrg==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.28" + "@volar/language-core": "2.4.31", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "typescript": "<7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@volar/source-map": { - "version": "2.4.28", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", - "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "version": "2.4.31", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.31.tgz", + "integrity": "sha512-odi31S3mKm57aO7Pd5P624FmGIB49U4lUPnYRUKvCDa8WZy5N5Yb+GMp2WJNaeIr4+q0ijNpuHsJLrfEiWvHtA==", "dev": true, "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "2.4.28", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", - "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "version": "2.4.31", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.31.tgz", + "integrity": "sha512-OBHLLAi03AmaRWqzytdFhgtW2B3HM+2/vXFkvnZAlEKltalUTyu+No7g9aZx7e9efrDLOtHsd/3PgCHSfgVJ/Q==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.28", + "@volar/language-core": "2.4.31", + "@volar/language-service": "2.4.31", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "typescript": "<7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@webcontainer/env": { @@ -6285,9 +6512,9 @@ } }, "node_modules/acorn": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", "dev": true, "license": "MIT", "bin": { @@ -6311,7 +6538,7 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 14" @@ -6567,6 +6794,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -6586,10 +6814,10 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.7.tgz", - "integrity": "sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==", - "devOptional": true, + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.8.tgz", + "integrity": "sha512-lPdpagk13ANXV+HClJEpCZM7kUsl5XcvgqKzZhaj6Ve1ZFICcY2sGhAN1n1vT0tML6EclT7bfVgTxqL57Gkpow==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", @@ -6601,7 +6829,7 @@ "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/async-function": { @@ -6703,9 +6931,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.26", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.26.tgz", - "integrity": "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==", + "version": "2.11.35", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.35.tgz", + "integrity": "sha512-GNk8KX52CHqeNKGk8+7tZnQab/2k1t/BgrUHPKNdijzpEhF1saPQZHVKb6m5loSAxB39gyrTYw34Qf9CQ3u1qQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6719,7 +6947,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "require-from-string": "^2.0.2" @@ -6766,9 +6994,9 @@ } }, "node_modules/browserslist": { - "version": "4.29.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", - "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "version": "4.29.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz", + "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==", "dev": true, "funding": [ { @@ -6786,11 +7014,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.11.26", - "caniuse-lite": "^1.0.30001813", - "electron-to-chromium": "^1.5.439", - "node-releases": "^2.0.57", - "update-browserslist-db": "^1.3.3" + "baseline-browser-mapping": "^2.11.33", + "caniuse-lite": "^1.0.30001819", + "electron-to-chromium": "^1.5.464", + "node-releases": "^2.0.60", + "update-browserslist-db": "^1.3.4" }, "bin": { "browserslist": "cli.js" @@ -6900,9 +7128,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001814", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", - "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", + "version": "1.0.30001823", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001823.tgz", + "integrity": "sha512-TQ5s/djDjNk1axreUW+4rULH+BmdCMLiJkhaXnO5wqivG7tITkSUj4ItIdzqdZHKaXcHAV8dhH3JxOM7/7bwqw==", "dev": true, "funding": [ { @@ -7080,15 +7308,15 @@ "license": "MIT" }, "node_modules/concurrently": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.5.tgz", - "integrity": "sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==", + "version": "10.0.7", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.7.tgz", + "integrity": "sha512-zMCeLhawVIj3SLbpg0/BWCdJl1wcBSpcHKbNrhaM01P3xAlEWcH30Lea2q+F4uGZ7YM0scDK0dgpWyn1uCJj6w==", "dev": true, "license": "MIT", "dependencies": { "chalk": "5.6.2", "rxjs": "7.8.2", - "shell-quote": "1.9.0", + "shell-quote": "1.12.0", "supports-color": "10.2.2", "tree-kill": "1.2.2", "yargs": "18.0.0" @@ -7105,9 +7333,9 @@ } }, "node_modules/confbox": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", - "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.2.tgz", + "integrity": "sha512-ZUAKhKlkCOrIGKc8cj7zek9s9z1mveHif/25nUshI2BzCRzF7KH7KHa+Lnh2ubq+e1tIlaFuP763mzW4qVoehg==", "dev": true, "license": "MIT" }, @@ -7115,16 +7343,17 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.50.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.50.0.tgz", - "integrity": "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==", + "version": "3.51.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.51.0.tgz", + "integrity": "sha512-52r6fkXgxv4GLMjmFcMWRx7X9bw071xTTdrDcdyPGRqU2oE+9pCG0bAOMifSBdLw0JRBZKEF0q3B55qPlLuE8g==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.28.7" + "browserslist": "^4.29.3" }, "engines": { "node": ">=6.4.0" @@ -7210,7 +7439,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "mdn-data": "2.27.1", @@ -7231,7 +7460,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@asamuzakjp/css-color": "^5.0.1", @@ -7247,7 +7476,7 @@ "version": "11.5.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", - "devOptional": true, + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -7273,7 +7502,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "whatwg-mimetype": "^5.0.0", @@ -7341,7 +7570,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7359,7 +7588,7 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/deep-eql": { @@ -7472,6 +7701,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -7513,9 +7743,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.442", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.442.tgz", - "integrity": "sha512-najZYZ3+ZpjN1z3VOsrBiv5ej18vQgfV2lvEmxWzfmKrk4bdm3Owseyud4yGU6DfFn9pyunoAHDDyM4KmP78Ew==", + "version": "1.5.478", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.478.tgz", + "integrity": "sha512-pftG+ma5Lm8O6/p45bhYT6StBN5ziQymaRzmCZwFl8DwWHKPPD93FdOmP1aqftcepolQYvrcyTLa17kE8Yf+jg==", "dev": true, "license": "ISC" }, @@ -7527,9 +7757,9 @@ "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", - "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "version": "5.26.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.1.tgz", + "integrity": "sha512-PPnnSJq3azKOIewXR/uppBV/6h98dprBJ/WaQ9Qgj10xX1lJLNfg+XShpbsrSqcDSy2yYYyhfXM1wWhftqgyzw==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -7691,6 +7921,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { @@ -7760,7 +7991,7 @@ "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -7905,9 +8136,9 @@ } }, "node_modules/eslint-compat-utils/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", "dev": true, "license": "ISC", "bin": { @@ -8149,9 +8380,9 @@ } }, "node_modules/eslint-plugin-n": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-18.4.0.tgz", - "integrity": "sha512-TEm6Nqn9+l7EWfJXOL6pmrEdpiM2ZTgo4fNMRDvBTM70jeQ7x3X3Akcx+sRCIRfpcSVwMypH3ZAA33PLyNKmYw==", + "version": "18.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-18.5.0.tgz", + "integrity": "sha512-M7uF6MTN/An+AXqvyIbXq7npJIZOkd5eKo8iMMvWrcDVNyZVdA6TF2lw01aKQfCEidRfpnR78GhAjZorlXNHiw==", "dev": true, "license": "MIT", "dependencies": { @@ -8185,6 +8416,23 @@ } } }, + "node_modules/eslint-plugin-n/node_modules/argparse": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.4.tgz", + "integrity": "sha512-fwWK3Zq22l4XJJjx5mcBHGikIk+hJqO+32dPm0Jur7+rtG5x1scOjSfdT9ZKeWHPpsya/l0HHK4tDImBkTrtvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "PSF-2.0" + }, "node_modules/eslint-plugin-n/node_modules/globals": { "version": "15.15.0", "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", @@ -8199,9 +8447,9 @@ } }, "node_modules/eslint-plugin-n/node_modules/js-yaml": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz", - "integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.5.0.tgz", + "integrity": "sha512-paNr4LFIFPjeeSmwNYdrb8FafLLG/Cr2Af3GjbsHJ5CxIjH4r0c2emlmRdAtpPAieP1gRVzk6buBbVP5gBMGAg==", "dev": true, "funding": [ { @@ -8215,16 +8463,16 @@ ], "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "argparse": "^3.0.3" }, "bin": { "js-yaml": "bin/js-yaml.mjs" } }, "node_modules/eslint-plugin-n/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", "dev": true, "license": "ISC", "bin": { @@ -8559,6 +8807,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" @@ -8596,15 +8845,16 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.0.0" } }, "node_modules/exsolve": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", - "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.2.0.tgz", + "integrity": "sha512-h1pQPR9XoeSMdntRNCjyTznaume4UpfXrULBnlkZ2CUM/G5eSa+pBByGldO5uR8dxbPC+OuJXsPPNf+JZ81ADg==", "dev": true, "license": "MIT" }, @@ -8633,6 +8883,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -9059,9 +9310,9 @@ } }, "node_modules/globals": { - "version": "17.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", - "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", + "version": "17.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.14.0.tgz", + "integrity": "sha512-UtdkTVyAJePQv0J2M9Xe4L0ruNSDV314XiIJsADI2Zf40bcdAonE/D/OhAU8AT99oij9mhLByb+NvA3QWZYgCg==", "dev": true, "license": "MIT", "engines": { @@ -9131,7 +9382,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9212,7 +9463,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@exodus/bytes": "^1.6.0" @@ -9225,14 +9476,14 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -9246,7 +9497,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -9464,9 +9715,9 @@ } }, "node_modules/is-bun-module/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", "dev": true, "license": "ISC", "bin": { @@ -9490,9 +9741,9 @@ } }, "node_modules/is-core-module": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", - "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.18.0.tgz", + "integrity": "sha512-qyax9fgaKOEC4qK8WZn7Q4IcoZC9RC1O1fFxRcRTq/oMMkWT/uXpqpXa12kzWR409zAt1drYN9N9BO26UmBCmw==", "dev": true, "license": "MIT", "dependencies": { @@ -9730,7 +9981,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/is-regex": { @@ -9966,7 +10217,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=8" @@ -9976,7 +10227,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", @@ -9991,7 +10242,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "semver": "^7.5.3" @@ -10004,10 +10255,10 @@ } }, "node_modules/istanbul-lib-report/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "devOptional": true, + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10020,7 +10271,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -10033,7 +10284,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", @@ -10065,7 +10316,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -10114,7 +10365,7 @@ "version": "28.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@acemir/cssom": "^0.9.31", @@ -10152,10 +10403,10 @@ } }, "node_modules/jsdom/node_modules/entities": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", - "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", - "devOptional": true, + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-9.0.0.tgz", + "integrity": "sha512-oJFNDJg3kTPwAD1cNn2+WmUq4wFoo09npc3Ydf1D7ED4smRiWrsKnaLxykrTWgyGEcusc87P5dxlAleol9kpkg==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=20.19.0" @@ -10165,13 +10416,13 @@ } }, "node_modules/jsdom/node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", - "devOptional": true, + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.1.1.tgz", + "integrity": "sha512-EcR/6WDcU7b2N3vQSxKALCk9lXGtUMQxXio008OEMBE0kavu/7JLfBEeFPzi43nMqVDSbEn3QCK+BMCc9zO//w==", + "dev": true, "license": "MIT", "dependencies": { - "entities": "^8.0.0" + "entities": "^9.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -10231,13 +10482,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -10377,9 +10621,9 @@ } }, "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -10393,23 +10637,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], @@ -10428,9 +10672,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ "arm64" ], @@ -10449,9 +10693,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ "x64" ], @@ -10470,9 +10714,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], @@ -10491,9 +10735,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ "arm" ], @@ -10512,9 +10756,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "cpu": [ "arm64" ], @@ -10536,9 +10780,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "cpu": [ "arm64" ], @@ -10560,9 +10804,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "cpu": [ "x64" ], @@ -10584,9 +10828,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ "x64" ], @@ -10608,9 +10852,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ "arm64" ], @@ -10629,9 +10873,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ "x64" ], @@ -10796,19 +11040,20 @@ } }, "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz", + "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", + "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "@jridgewell/sourcemap-codec": "^1.6.0" } }, "node_modules/magicast": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.7", @@ -10831,9 +11076,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.3.tgz", + "integrity": "sha512-kM18BPOqTi0+TB68703/GJRpAML5Me+IDQsR1J26YB3EFKKNEyHIVZuWDX7+WKQB3tHwDwgk24BfhdpWUbytvQ==", "dev": true, "license": "ISC", "bin": { @@ -10841,9 +11086,9 @@ } }, "node_modules/markdown-it": { - "version": "14.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", - "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", + "version": "14.3.3", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.3.tgz", + "integrity": "sha512-L//tDAkjTjJCU6pBUSBTgs3Kg4InVaZuUZog/b6V12UKhcDQ7S78877ibbCanRExwPuYP1CZD7Gd2dxSy1GXRg==", "dev": true, "funding": [ { @@ -10907,7 +11152,7 @@ "version": "2.27.1", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "devOptional": true, + "dev": true, "license": "CC0-1.0" }, "node_modules/mdurl": { @@ -11041,13 +11286,13 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/n3": { - "version": "2.7.12", - "resolved": "https://registry.npmjs.org/n3/-/n3-2.7.12.tgz", - "integrity": "sha512-Hy6dfGg9yniLQpSBirvH2E2yO3EG7dSmA3aefRbtO411oqtzG8roD3h1kTc/N065bivCOPfTFIjf0KlwW7KlnQ==", + "version": "2.13.12", + "resolved": "https://registry.npmjs.org/n3/-/n3-2.13.12.tgz", + "integrity": "sha512-z944NsyJi58WXcYevtNkUL2JhVr0KP6nvED9AgymQq5V35TDaNn1N8kO+OWJk/kj4PR0L/ilWB3d7/bWceg1CQ==", "license": "MIT", "dependencies": { "buffer": "^6.0.3", @@ -11076,12 +11321,13 @@ } }, "node_modules/napi-postinstall": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.8.tgz", + "integrity": "sha512-dHbDC5V1g/EI8OLEtL4Iuwbu0fgzmyykxenn2KnlsX9XdhTXL3gWnfbbP+ab+oUcgV6CFpb8QGyQt/ZXA0UXig==", "dev": true, "license": "MIT", "bin": { + "napi-patch": "lib/patch.js", "napi-postinstall": "lib/cli.js" }, "engines": { @@ -11231,9 +11477,9 @@ } }, "node_modules/neostandard/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", "dev": true, "license": "ISC", "bin": { @@ -11257,18 +11503,16 @@ } }, "node_modules/nock": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/nock/-/nock-15.0.0.tgz", - "integrity": "sha512-EoAVk4Y8Yv4JUQz62sv8zmv+DoBblD/pht/q7aW/td1WietaFWrivzhMGGCLmx2qjpLcOrbyammedW8IRUU5TA==", - "deprecated": "v15.0.0 was released accidentally and is unstable. Please use v14.x until v15 is officially ready.", + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-15.0.6.tgz", + "integrity": "sha512-eyOJ1wemcgJZ+PQC3lecfF3gQc2SAmGlQ4mHGfFXcOa/+7v0qjnX+ft/NV2F+4vCwzkXZqik3gS6ZIjh+QcigQ==", "dev": true, "license": "MIT", "dependencies": { - "@mswjs/interceptors": "^0.39.5", - "json-stringify-safe": "^5.0.1" + "@mswjs/interceptors": "^0.45.11" }, "engines": { - "node": ">=18.20.0 <20 || >=20.12.1" + "node": ">=22.20.0" } }, "node_modules/node-domexception": { @@ -11329,9 +11573,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.57", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", - "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "version": "2.0.63", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.63.tgz", + "integrity": "sha512-6li2QanLsAXpIH2R6j77MzRyvOyrvdTqlKtO4CgBdHGNvN99ZZfyxszHc5o5+IZBWD5u6Hzwg83JoGXmqZNG9Q==", "dev": true, "license": "MIT", "engines": { @@ -11476,6 +11720,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" @@ -11668,20 +11913,20 @@ } }, "node_modules/package-manager-detector": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", - "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", + "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==", "dev": true, "license": "MIT" }, "node_modules/pane-registry": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pane-registry/-/pane-registry-5.0.1.tgz", - "integrity": "sha512-3aeLxm6TnRHfIDoEbpZpNe3Tlm7wWT+ipEpiV6h3cRgxyzmEtrUpS9WB8yJa/VJq0RHXEoaYlLu94fcbuOPnbA==", + "version": "5.0.2-3", + "resolved": "https://registry.npmjs.org/pane-registry/-/pane-registry-5.0.2-3.tgz", + "integrity": "sha512-yYTteGX/ld14YeLnpTN77ygseUzlH6IC0k2A6Du4vqEXg97Tk0MHeqqVyiVCz3K2e0eBFyqUVQ8cDheLz/9JVg==", "license": "MIT", "dependencies": { "rdflib": "2.4.1", - "solid-logic": "6.0.0" + "solid-logic": "6.0.1-6" } }, "node_modules/parent-module": { @@ -11773,9 +12018,9 @@ } }, "node_modules/patch-package/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", "dev": true, "license": "ISC", "bin": { @@ -11846,6 +12091,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, "license": "MIT" }, "node_modules/pathval": { @@ -11873,12 +12119,14 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -11898,14 +12146,14 @@ } }, "node_modules/pkg-types": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.3.tgz", - "integrity": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.4.0.tgz", + "integrity": "sha512-07Fys7iVx0X6TcviMOgogM1Lk33ZyqOezMnU79S+kffWqwolG/5W9V8EgQSeYCIfFTYVgssRbnH630Oa2+soBA==", "dev": true, "license": "MIT", "dependencies": { "confbox": "^0.3.1", - "exsolve": "^1.1.1", + "exsolve": "^1.1.3", "pathe": "^2.0.3" } }, @@ -11920,9 +12168,10 @@ } }, "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", + "dev": true, "funding": [ { "type": "opencollective", @@ -11939,18 +12188,18 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.18", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/postcss-custom-media": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-12.0.2.tgz", - "integrity": "sha512-qn+su6Ux/TVzBSPYlRxC3Kq5DZNWBZt7y1wj38mnHT+whYnM21IxuYVKJ2E9qahBo0KGvcv/1rAB/F5iY1UiIA==", + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-12.0.3.tgz", + "integrity": "sha512-pgOICT/unQexoA/Vh4ZvyAUrcwwA/nHh0SJcOwOombpwDEj1x5N6zK+kz97wy9NslhU6UxhE5c5Q/M2gpYfkJA==", "dev": true, "funding": [ { @@ -11964,10 +12213,10 @@ ], "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^3.0.1", - "@csstools/css-parser-algorithms": "^4.0.1", + "@csstools/cascade-layer-name-parser": "^3.0.2", + "@csstools/css-parser-algorithms": "^4.0.2", "@csstools/css-tokenizer": "^4.0.2", - "@csstools/media-query-list-parser": "^5.0.1" + "@csstools/media-query-list-parser": "^5.0.2" }, "engines": { "node": ">=20.19.0" @@ -11977,9 +12226,10 @@ } }, "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, "funding": [ { "type": "github", @@ -12051,7 +12301,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -12113,9 +12363,9 @@ } }, "node_modules/react": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", - "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", "dev": true, "license": "MIT", "engines": { @@ -12123,16 +12373,16 @@ } }, "node_modules/react-dom": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", - "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { - "react": "^19.3.0" + "react": "^19.3.1" } }, "node_modules/react-is": { @@ -12143,9 +12393,9 @@ "license": "MIT" }, "node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.8.0.tgz", + "integrity": "sha512-2jhTTU3Eilp/2DMT71gbcoqC35eQHasMJH0RHkYxwlxIv3+FR6Y5xAFZIwEGLxKkjZYEFlsiMNIdwUqjK2lLHQ==", "license": "MIT", "dependencies": { "abort-controller": "^3.0.0", @@ -12241,9 +12491,9 @@ } }, "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.5.0.tgz", + "integrity": "sha512-1s+CxXjJ71i7LNVy8nYwg47XV4y6lAnOEhsMtSKq3WpmPfhl9LuKlTqKIn/3yUpE8lBz+W+TT88Bxyhnr0U4yw==", "dev": true, "license": "MIT" }, @@ -12282,16 +12532,16 @@ } }, "node_modules/regexpu-core": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.5.2.tgz", - "integrity": "sha512-zkmVH92DlzjSFc4xKGZp/2BXIFrDIdBQBC8gUXKCsMgKYb1bkCkxfSuoW1werDjaTEMTbULkHtKFHe1L+Abnaw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.5.4.tgz", + "integrity": "sha512-VpdamrZ9bw4YmvMZfTA3Yg8IHU9Guvg21bonvgjC4l3KfBkOqCPJesHakz7cQeE4Vh4zz4r4Gy6igAcnJxZv9g==", "dev": true, "license": "MIT", "dependencies": { - "regenerate": "^1.4.2", + "regenerate": "^1.5.0", "regenerate-unicode-properties": "^10.3.0", "regjsgen": "^0.8.0", - "regjsparser": "^0.13.0", + "regjsparser": "^0.14.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" }, @@ -12307,9 +12557,9 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.3.tgz", - "integrity": "sha512-ycwFAS14Jw4mppvmK4GR/J6u3WpWpjkEApehuHtLc/8VpPNpDMbQ4WjqwplXifGeyKOzHSFLmSPqzksDQE2Sfg==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.14.1.tgz", + "integrity": "sha512-IeYboCLG1KBNRksrZQIZ1SV5KVWZiR0ikFokOcA0n63vLvlaFvCsfxN+fd4aXwL0KW3YD+ByRzIgo9ZAHdbldw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -12323,21 +12573,21 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "version": "1.22.14", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.14.tgz", + "integrity": "sha512-1ZjYvR9sbkiwC5+9QHetqW5HmA2pHRMaoupD01t2HlYnV/vydC2zPwLixhsMieSWVZt0JJRg5LVIk0Ow1D/TrA==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", + "is-core-module": "^2.18.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -12371,13 +12621,22 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/rettime": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.12.tgz", + "integrity": "sha512-5ExtR1ppiDBQkVoPw5NAqdbfH4Iq2VOmQKHMtUoaFxA9bo2krzFYBtyq6K40xxbYp+2w/YOr6D+1ToF6tTN53A==", + "dev": true, + "license": "MIT" + }, "node_modules/rolldown": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", - "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.3.1.tgz", + "integrity": "sha512-RAIazzQpeoYB0r8jWuNDX+fT1NJ+9Cqdt0pSi9jc9zwX21k2x2IUPuYJmZxDB2k8ZisNv4gNIPj+HwRCTJG4uw==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@oxc-project/types": "=0.151.0", + "@oxc-project/types": "=0.157.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -12387,21 +12646,22 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.11", - "@rolldown/binding-android-arm64": "1.2.11", - "@rolldown/binding-darwin-arm64": "1.2.11", - "@rolldown/binding-darwin-x64": "1.2.11", - "@rolldown/binding-freebsd-x64": "1.2.11", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", - "@rolldown/binding-linux-arm64-gnu": "1.2.11", - "@rolldown/binding-linux-arm64-musl": "1.2.11", - "@rolldown/binding-linux-ppc64-gnu": "1.2.11", - "@rolldown/binding-linux-s390x-gnu": "1.2.11", - "@rolldown/binding-linux-x64-gnu": "1.2.11", - "@rolldown/binding-linux-x64-musl": "1.2.11", - "@rolldown/binding-openharmony-arm64": "1.2.11", - "@rolldown/binding-win32-arm64-msvc": "1.2.11", - "@rolldown/binding-win32-x64-msvc": "1.2.11" + "@rolldown/binding-android-arm-eabi": "1.3.1", + "@rolldown/binding-android-arm64": "1.3.1", + "@rolldown/binding-darwin-arm64": "1.3.1", + "@rolldown/binding-darwin-x64": "1.3.1", + "@rolldown/binding-freebsd-x64": "1.3.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.3.1", + "@rolldown/binding-linux-arm64-gnu": "1.3.1", + "@rolldown/binding-linux-arm64-musl": "1.3.1", + "@rolldown/binding-linux-ppc64-gnu": "1.3.1", + "@rolldown/binding-linux-s390x-gnu": "1.3.1", + "@rolldown/binding-linux-x64-gnu": "1.3.1", + "@rolldown/binding-linux-x64-musl": "1.3.1", + "@rolldown/binding-openharmony-arm64": "1.3.1", + "@rolldown/binding-wasm32-wasi": "1.3.1", + "@rolldown/binding-win32-arm64-msvc": "1.3.1", + "@rolldown/binding-win32-x64-msvc": "1.3.1" } }, "node_modules/run-applescript": { @@ -12506,7 +12766,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -12523,9 +12783,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", "dev": true, "license": "ISC", "bin": { @@ -12611,9 +12871,9 @@ } }, "node_modules/shell-quote": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", - "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", "dev": true, "license": "MIT", "engines": { @@ -12703,6 +12963,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, "license": "ISC" }, "node_modules/slash": { @@ -12716,15 +12977,15 @@ } }, "node_modules/solid-logic": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/solid-logic/-/solid-logic-6.0.0.tgz", - "integrity": "sha512-irRWnU28Simo2WBhbCgAz4d+QawGJ4zJuiwuBQyObNwuwr4KbDC9evf0CFdnr/PJD8EhLrYT0Fk3nh1eJmQAbA==", + "version": "6.0.1-6", + "resolved": "https://registry.npmjs.org/solid-logic/-/solid-logic-6.0.1-6.tgz", + "integrity": "sha512-qlvgCfutwGbWZZExkNzmXzSDj8FhAQ//IeCe5zWluyLCfySH+CHihbYX48FNyY6EqEC8M/v9UE0JVSSEuJsSdg==", "license": "MIT", "dependencies": { + "@dokieli/web-access-control": "^1.1.0", "@uvdsl/solid-oidc-client-browser": "^0.2.3", - "solid-namespace": "^0.5.4", - "vite": "^8.0.16", - "vitest": "^4.1.9" + "mime-types": "^3.0.2", + "solid-namespace": "^0.5.4" }, "engines": { "node": ">=18" @@ -12737,9 +12998,9 @@ "license": "MIT" }, "node_modules/solidos-toolkit": { - "version": "0.0.0-dev.998a984ddf8fda587eedb0c8125cb952ac28d191", - "resolved": "https://registry.npmjs.org/solidos-toolkit/-/solidos-toolkit-0.0.0-dev.998a984ddf8fda587eedb0c8125cb952ac28d191.tgz", - "integrity": "sha512-m2b3sZkKbjV5X/J8N25h3UIbsiQARdnXCGs1AeGqFiueoq8cWm+pdOyPpqiVuqRnnA5osmKG2wovUcBBYumL0Q==", + "version": "1.0.0-dev.2fb6be508170f3b28ca79d738aae71cd48f6d87e", + "resolved": "https://registry.npmjs.org/solidos-toolkit/-/solidos-toolkit-1.0.0-dev.2fb6be508170f3b28ca79d738aae71cd48f6d87e.tgz", + "integrity": "sha512-EQMddD3jV0YiPdPuEfBTRc1+QynYiIBwxMs8e3+F/gEP0lfIvSUjFSXCYsJxQxY8rqsEb4YXR4JW/5b1JRtdTQ==", "dev": true, "dependencies": { "@babel/core": "^7.29.0", @@ -12758,10 +13019,10 @@ "vite-plugin-lit-css": "^3.0.0" }, "peerDependencies": { - "pane-registry": "^3.1.1 || ^3.1.2-0 || ^5.0.0 || ^5.0.0-0", + "pane-registry": "^3.1.1 || ^3.1.2-0 || >=5.0.2-0 <6.0.0", "rdflib": "^2.3.6", - "solid-logic": "^4.0.7 || ^4.0.8-0 || ^6.0.0 || ^6.0.0-0", - "solid-ui": "^3.1.2 || ^3.1.3-0 || ^5.0.0 || ^5.0.0-0", + "solid-logic": "^4.0.7 || ^4.0.8-0 || >=6.0.1-0 <7.0.0", + "solid-ui": "^3.1.2 || ^3.1.3-0 || >=5.0.1-0 <6.0.0 || ^5.0.2-0", "vite": "^8.0.0", "vitest": "^4.0.0" }, @@ -12797,9 +13058,10 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -12819,12 +13081,14 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, "license": "MIT" }, "node_modules/std-env": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, "license": "MIT" }, "node_modules/stop-iteration-iterator": { @@ -12889,9 +13153,9 @@ } }, "node_modules/storybook/node_modules/@testing-library/user-event": { - "version": "14.6.7", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", - "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==", + "version": "14.6.8", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.8.tgz", + "integrity": "sha512-vgSSqnANMnkcYR/vUNDbwS7aM3OErRLNUG8vCVVlj4oc6cKffUrccX7UAS5AmJQQNZTSibi9z5GvvRZ0aYgUCQ==", "dev": true, "license": "MIT", "engines": { @@ -12922,9 +13186,9 @@ } }, "node_modules/storybook/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", "dev": true, "license": "ISC", "bin": { @@ -12934,13 +13198,6 @@ "node": ">=10" } }, - "node_modules/strict-event-emitter": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", - "dev": true, - "license": "MIT" - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -13084,9 +13341,9 @@ } }, "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", - "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.6.0.tgz", + "integrity": "sha512-XZ5apbzreCbXSKwPjJstu8R0zOi247w3XYwWxtwBXgpZrCyc1Ka7p9/xY0A9/diMewmHbEKz+FIraWaEWjlN5g==", "dev": true, "license": "MIT", "engines": { @@ -13168,7 +13425,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/tailwindcss": { @@ -13202,12 +13459,14 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -13217,6 +13476,7 @@ "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -13230,9 +13490,10 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", - "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", + "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" @@ -13249,23 +13510,23 @@ } }, "node_modules/tldts": { - "version": "7.4.16", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.16.tgz", - "integrity": "sha512-QwBER5KMR86IIjpIiO7H/Z3IMJPsZ1A6RKPAqzTTgOyUQUSt9FdnKcqhTaJmkY6HVrgouZHZR0ncK5QxvmnQeg==", - "devOptional": true, + "version": "7.4.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.19.tgz", + "integrity": "sha512-48dH6aSpQnpbzDUc4vIQZ1VjB8tkK+e8c1yx05/SZK+rkssgjEOlxDd/yU8/a4vGYQ9vRgJr9dwMq9TJ0b3X2Q==", + "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.4.16" + "tldts-core": "^7.4.19" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.4.16", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.16.tgz", - "integrity": "sha512-MDolfaSJtlSK5Y0A1xl3277ekubZwobpBjugknDizI9O5Rm60a1m8k4ICK+MRsCDzPygT81mp3BBf5RKDlFRfA==", - "devOptional": true, + "version": "7.4.20", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.20.tgz", + "integrity": "sha512-C630TWXZCZmLq0F2bW4QqwnaaxCQ/svpAqvJwnC4JvgEM5ohhBnGsEzTLSOXcN+xjz+kEaHS5uoWIw2klnqd0A==", + "dev": true, "license": "MIT" }, "node_modules/tmp": { @@ -13292,10 +13553,10 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", - "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", - "devOptional": true, + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.1.0.tgz", + "integrity": "sha512-FTxc8u8Kl2tz0Lat1u8ksvcy8bwHRyEMawwTWybo51JqaC6O7SvfuhAKg+Ld6VqyIe2QRJBzY9HA7y2WyFRT2w==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "tldts": "^7.0.5" @@ -13308,7 +13569,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -13400,9 +13661,9 @@ } }, "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz", + "integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==", "dev": true, "license": "0BSD" }, @@ -13535,16 +13796,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.71.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.0.tgz", - "integrity": "sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.2.tgz", + "integrity": "sha512-c/+AKMW1KB81r28Sp0AAdcyXj5dzNmAsZFcsrxKbZTKmcBK2xIwBR31dcSYSDu3bKmzXe8GKrSxO+eDQ401Uzg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.71.0", - "@typescript-eslint/parser": "8.71.0", - "@typescript-eslint/typescript-estree": "8.71.0", - "@typescript-eslint/utils": "8.71.0" + "@typescript-eslint/eslint-plugin": "8.72.2", + "@typescript-eslint/parser": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2", + "@typescript-eslint/utils": "8.72.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -13592,19 +13853,19 @@ } }, "node_modules/undici": { - "version": "7.30.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", - "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", - "devOptional": true, + "version": "7.30.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.2.tgz", + "integrity": "sha512-mLyPQnHwUxaM/rideoK/+KL8DaAJLRl4aBCfmX444aCSLnIaomqPLtNRe2jdCYcsk5wZzXrDIQC8ZeYNRAYyIA==", + "dev": true, "license": "MIT", "engines": { "node": ">=20.18.1" } }, "node_modules/undici-types": { - "version": "7.30.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.30.0.tgz", - "integrity": "sha512-7QjA5O+GhM27do7Id6hQZpnYEd7WzHbs7/XIS9A1k+Gat/ZVF7Jmpzt68zL/205f65FYwDNLSjQemHPS8ReM/A==", + "version": "7.30.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.30.2.tgz", + "integrity": "sha512-Vx15rZHtDo7X+sZVzcecz08ooRvEa0cjSs8z85v/jRRf81BT1m9eaoMI4gxvaNwgQg2IgkfjJ45dMpKrSl0zEQ==", "dev": true, "license": "MIT" }, @@ -13692,9 +13953,9 @@ } }, "node_modules/unplugin-dts": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unplugin-dts/-/unplugin-dts-1.1.1.tgz", - "integrity": "sha512-Z2VcabC0hjYuzF62oWmZXVoxagkM8bUWgqUy+dotoJTPnooR5mNaxbpv63JCmcaxdGCW2Bb75omK6FOryOYCRg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unplugin-dts/-/unplugin-dts-1.1.2.tgz", + "integrity": "sha512-RDqy90XUmFj6rz4U5YoTMdYdaCJe2ZnQZaWIjkWS4952Gv0zBTQyqWtlGgVMPYHa982dURvyoc+sUOFIcDFv+w==", "dev": true, "license": "MIT", "dependencies": { @@ -13746,6 +14007,16 @@ } } }, + "node_modules/unplugin-dts/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/unplugin-icons": { "version": "23.0.1", "resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-23.0.1.tgz", @@ -13784,47 +14055,45 @@ } }, "node_modules/unrs-resolver": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", - "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.13.0.tgz", + "integrity": "sha512-mcPQo9Vm6kXd84d/WqMiYBi2CcA57QHBX/ev5PoUEb8b2oQzUvJ6xYsfseQblnCncgnVmCZGWiFwK7KkS5JZJA==", "dev": true, - "hasInstallScript": true, "license": "MIT", "dependencies": { - "napi-postinstall": "^0.3.4" + "napi-postinstall": "^0.3.7" }, "funding": { "url": "https://opencollective.com/unrs-resolver" }, "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.12.2", - "@unrs/resolver-binding-android-arm64": "1.12.2", - "@unrs/resolver-binding-darwin-arm64": "1.12.2", - "@unrs/resolver-binding-darwin-x64": "1.12.2", - "@unrs/resolver-binding-freebsd-x64": "1.12.2", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", - "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", - "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", - "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", - "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-x64-musl": "1.12.2", - "@unrs/resolver-binding-openharmony-arm64": "1.12.2", - "@unrs/resolver-binding-wasm32-wasi": "1.12.2", - "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", - "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", - "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + "@unrs/resolver-binding-android-arm-eabi": "1.13.0", + "@unrs/resolver-binding-android-arm64": "1.13.0", + "@unrs/resolver-binding-darwin-arm64": "1.13.0", + "@unrs/resolver-binding-darwin-x64": "1.13.0", + "@unrs/resolver-binding-freebsd-x64": "1.13.0", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.13.0", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.13.0", + "@unrs/resolver-binding-linux-arm64-gnu": "1.13.0", + "@unrs/resolver-binding-linux-arm64-musl": "1.13.0", + "@unrs/resolver-binding-linux-loong64-gnu": "1.13.0", + "@unrs/resolver-binding-linux-loong64-musl": "1.13.0", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.13.0", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.13.0", + "@unrs/resolver-binding-linux-riscv64-musl": "1.13.0", + "@unrs/resolver-binding-linux-s390x-gnu": "1.13.0", + "@unrs/resolver-binding-linux-x64-gnu": "1.13.0", + "@unrs/resolver-binding-linux-x64-musl": "1.13.0", + "@unrs/resolver-binding-openharmony-arm64": "1.13.0", + "@unrs/resolver-binding-win32-arm64-msvc": "1.13.0", + "@unrs/resolver-binding-win32-ia32-msvc": "1.13.0", + "@unrs/resolver-binding-win32-x64-msvc": "1.13.0" } }, "node_modules/update-browserslist-db": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", - "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", "dev": true, "funding": [ { @@ -13873,9 +14142,9 @@ } }, "node_modules/uuid": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz", - "integrity": "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.3.tgz", + "integrity": "sha512-Sc5u1VVln93y1Xnz4R+CunOZc7R9LUcTmhj7Rc7Eg3HVcf7rVn2FOoa812fe0W8p3jD7xeoGSYbfqCGGH2B6xw==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -13886,15 +14155,16 @@ } }, "node_modules/vite": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", - "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "version": "8.3.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.7.tgz", + "integrity": "sha512-oxh0dMxrayBr33ScJvvBJ2HBQ2jc1TBrXKR2NyL/MJ9WPuX19mN2DXWUmywKdyw5RFVQBo8hmtb2FMC0LHrnng==", + "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", - "postcss": "^8.5.28", - "rolldown": "~1.2.9", + "postcss": "^8.5.32", + "rolldown": "~1.2.18", "tinyglobby": "^0.2.17" }, "bin": { @@ -13911,7 +14181,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.7.1", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -13985,280 +14255,337 @@ "vite": "^7.0.0 || ^8.0.0" } }, - "node_modules/vite/node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, + "node_modules/vite/node_modules/@oxc-project/types": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz", + "integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==", + "dev": true, + "license": "MIT", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" + "url": "https://github.com/sponsors/oxc-project" } }, - "node_modules/vite/node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "node_modules/vite/node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.18.tgz", + "integrity": "sha512-ubTG+4KlidvioxjOFdsdEa2F6JVGZb8m2ByJUqQ7fYzUeT1cEn194POcsQ10PKH4p//q0YQn4SB3YpSuzcA1Yg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.18.tgz", + "integrity": "sha512-2Xa1t/JeFGTZn5TbzgPppcCftF8sCVnTNvrIvoSdcZhoFzAuVy1L3xxQUQ4sfWDtwVzg1u6Yi1R3SlJugRNV2Q==", "cpu": [ "arm64" ], - "license": "MPL-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/vite/node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "node_modules/vite/node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.18.tgz", + "integrity": "sha512-iUzw9HshVvrWci9gVXjxWS7/M/IcLvNo67vu2c/w9zKBz4hIvPRixdUpIW/iCfhldLaq9ER2hecsI9UY5z0/yA==", "cpu": [ "arm64" ], - "license": "MPL-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/vite/node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "node_modules/vite/node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.18.tgz", + "integrity": "sha512-XuDHFzbFEtZl0QGJi92+fmG67tfnp1iZ9VOeDkwasGyTq2VG3/5PnJnvAr+MTzFh/rNScJ3VhN+XpZyyGsN7Jw==", "cpu": [ "x64" ], - "license": "MPL-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/vite/node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "node_modules/vite/node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.18.tgz", + "integrity": "sha512-MM8opLfsLK15oEXDz6iBc0t3ahu+ew9jsAmha+ovZ33hSwYgqo7YmpRITqFTfW7xA71BLEQ3jJdTsaCmhB1c9w==", "cpu": [ "x64" ], - "license": "MPL-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "node_modules/vite/node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.18.tgz", + "integrity": "sha512-J6EoCWiy5RnFQzHrKyHXXevZ9lukQfGpnchX/zG/k/WC2nc9O9XyS1JgA8kI9VxMFggMbCPjpv7LcikriXfehw==", "cpu": [ "arm" ], - "license": "MPL-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.18.tgz", + "integrity": "sha512-HIkFuLF7CdDeki+1aCZLLbLq0hk/PHntb+z4/YCTo47JbksTD6jJEELIz92tYROmDmvMrNqUMqBi71BkJLDR+A==", "cpu": [ "arm64" ], + "dev": true, "libc": [ "glibc" ], - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.18.tgz", + "integrity": "sha512-4oqWItLLYt7IceOnElCct8T2ek5p7PmD7LN04TRJRXGo5f3HtKTgGH6PhYRFK4C2ICkq/LMUU0BVPvvlAKhm8g==", "cpu": [ "arm64" ], + "dev": true, "libc": [ "musl" ], - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "node_modules/vite/node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.18.tgz", + "integrity": "sha512-+AkRly5SrCEgnPhGRfub7pucASYEC2fTsxZg7VCcnozpA2+Wn/gCYy1t7LuJS67/xyJu/7noYBJncDUkbwgw2A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.18.tgz", + "integrity": "sha512-Ttg1r9vVykMnQrE/GwYO9icKtXiuiMfRe/w74xJkeoVuvhRG+WtT6LEMOf58Yl63yd77lhHzO8Y55R8CC3XALA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.18.tgz", + "integrity": "sha512-xX1Tt4o5jUMma0vJsXp5eRZA9nOuyigb2dKJMMzZRmzMxLYGE9PaKKSqHPzPvSQgM8ZOEacSTQwDQcwWfh0xmQ==", "cpu": [ "x64" ], + "dev": true, "libc": [ "glibc" ], - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "node_modules/vite/node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.18.tgz", + "integrity": "sha512-4VVlGI5K2kociZlW3lSN8VjG3TW5yQIlAcfCdBXMgGjJxZll15Fa4ocRHXHvIVVnwlibCw/NQajJrNDiqqpiiw==", "cpu": [ "x64" ], + "dev": true, "libc": [ "musl" ], - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "node_modules/vite/node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.18.tgz", + "integrity": "sha512-PApTsgH1jLYuyy3tSCz9De82Gq5qXwi7vImVZhEzTgX1C4150Cp5M2ZfL3pLvbCvbEHL0oJeC6pd6podtv8Y5w==", "cpu": [ "arm64" ], - "license": "MPL-2.0", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.18.tgz", + "integrity": "sha512-J2XjWjx/SP+4x0H4r6dz3Vzb5WVkg7re40Do9K7HJ3sK2/xcmZrWwakXlSwaMvOkvruLJCD4CL/3wqsIR/thgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "node_modules/vite/node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.18.tgz", + "integrity": "sha512-XwRJxRQvkuEtS0hIIt29QrqPJ+JzMJSNYHJw4ae/mc6uEarfRG1ceyg4HmmWiBdL/YCb6g7N5BLbSvmRJRYZHw==", "cpu": [ "x64" ], - "license": "MPL-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 12.0.0" + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/rolldown": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.18.tgz", + "integrity": "sha512-C1uuT3UR60SShuCaPQFAAZyefJCWZlAKB02BV3BuPE2Rs4fevRQltEKoFO2tV5W9/eJhU5RieV6vJj3euulfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.156.0", + "@rolldown/pluginutils": "^1.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.18", + "@rolldown/binding-android-arm64": "1.2.18", + "@rolldown/binding-darwin-arm64": "1.2.18", + "@rolldown/binding-darwin-x64": "1.2.18", + "@rolldown/binding-freebsd-x64": "1.2.18", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.18", + "@rolldown/binding-linux-arm64-gnu": "1.2.18", + "@rolldown/binding-linux-arm64-musl": "1.2.18", + "@rolldown/binding-linux-ppc64-gnu": "1.2.18", + "@rolldown/binding-linux-s390x-gnu": "1.2.18", + "@rolldown/binding-linux-x64-gnu": "1.2.18", + "@rolldown/binding-linux-x64-musl": "1.2.18", + "@rolldown/binding-openharmony-arm64": "1.2.18", + "@rolldown/binding-win32-arm64-msvc": "1.2.18", + "@rolldown/binding-win32-x64-msvc": "1.2.18" } }, "node_modules/vitest": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", - "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.11", - "@vitest/mocker": "4.1.11", - "@vitest/pretty-format": "4.1.11", - "@vitest/runner": "4.1.11", - "@vitest/snapshot": "4.1.11", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.12.tgz", + "integrity": "sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.12", + "@vitest/mocker": "4.1.12", + "@vitest/pretty-format": "4.1.12", + "@vitest/runner": "4.1.12", + "@vitest/snapshot": "4.1.12", + "@vitest/spy": "4.1.12", + "@vitest/utils": "4.1.12", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -14286,12 +14613,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.11", - "@vitest/browser-preview": "4.1.11", - "@vitest/browser-webdriverio": "4.1.11", - "@vitest/coverage-istanbul": "4.1.11", - "@vitest/coverage-v8": "4.1.11", - "@vitest/ui": "4.1.11", + "@vitest/browser-playwright": "4.1.12", + "@vitest/browser-preview": "4.1.12", + "@vitest/browser-webdriverio": "^4.1.11", + "@vitest/coverage-istanbul": "4.1.12", + "@vitest/coverage-v8": "4.1.12", + "@vitest/ui": "4.1.12", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -14336,15 +14663,16 @@ } }, "node_modules/vitest/node_modules/@vitest/expect": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", - "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.12.tgz", + "integrity": "sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ==", + "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", + "@vitest/spy": "4.1.12", + "@vitest/utils": "4.1.12", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -14353,23 +14681,70 @@ } }, "node_modules/vitest/node_modules/@vitest/spy": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", - "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.12.tgz", + "integrity": "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==", + "dev": true, "license": "MIT", "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/vitest/node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, + "node_modules/vitest/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-10.0.0.tgz", + "integrity": "sha512-USSfxg0vDIpTQqlQBGxF3mBARyoS8cU7LHLpbYNh6dOJt32dlskxPWB0Irrh5TllTmyf7oNLAvqDqzklbwwnpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.5.tgz", + "integrity": "sha512-FW4fBMzZ99N8xeob4py0l050wgCoRgTpEq5Xcsb9lM1bc29UKGR/wSV+4knaqFxssFHajy7OzNiKP/m3U1ZEuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "10.0.0", + "vscode-languageserver-types": "3.18.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.16.tgz", + "integrity": "sha512-qeNEuOi9a3qkqRf7Gfez2WL2VwlBTLCNekHBGO9kxPVUp9XuY15yfTJ9jwvOwKI0I2MhUUfZq95NSp+LK1CLQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.5.tgz", + "integrity": "sha512-kPd6+JQNz3HMJvjKlqUxERSlZHAntl/aGpsk7ImwIh0ewDKDcjgAEyVSYciPiOJD3ftbK9IrfiqUeTZLSbCizw==", + "dev": true, + "license": "MIT" + }, "node_modules/vscode-uri": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", @@ -14387,7 +14762,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" @@ -14409,7 +14784,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=20" @@ -14433,7 +14808,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -14443,7 +14818,7 @@ "version": "16.0.1", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@exodus/bytes": "^1.11.0", @@ -14563,6 +14938,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, "license": "MIT", "dependencies": { "siginfo": "^2.0.0", @@ -14631,9 +15007,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", - "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "version": "8.22.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.2.tgz", + "integrity": "sha512-/rEZ2ejown2N5gxVTHUu9bVVC7ttu0/Tgzf5hbTOixR3Fs7mbbXgn2DbdbcKgJOmpQMtrIUWivtYuVMnp/W/yA==", "dev": true, "license": "MIT", "engines": { @@ -14688,7 +15064,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18" @@ -14698,7 +15074,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/y18n": { @@ -14722,7 +15098,7 @@ "version": "2.9.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 7a1b6343d..46345e703 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "solid-ui", - "version": "5.0.1", + "version": "5.0.2-9", "description": "UI library for Solid applications", "main": "dist/index.cjs.js", "types": "dist/index.d.ts", @@ -112,9 +112,9 @@ "escape-html": "^1.0.3", "lit": "^3.3.3", "mime-types": "^3.0.2", - "pane-registry": "5.0.1", + "pane-registry": "5.0.2-3", "rdflib": "2.4.1", - "solid-logic": "6.0.0", + "solid-logic": "6.0.1-6", "solid-namespace": "^0.5.4", "uuid": "^14.0.0" }, diff --git a/src/acl/add-agent-buttons.ts b/src/acl/add-agent-buttons.ts index e31cb85ab..93e147cb5 100644 --- a/src/acl/add-agent-buttons.ts +++ b/src/acl/add-agent-buttons.ts @@ -108,13 +108,17 @@ export class AddAgentButtons { } private renderPublicButton (): HTMLElement { - return widgets.button( + const button = widgets.button( this.groupList.controller.dom, icons.iconBase + widgets.iconForClass['foaf:Agent'], 'Add Everyone', () => this.addAgent(ns.foaf('Agent').uri) .then(() => this.groupList.controller.renderTemporaryStatus('Adding the general public to those who can read. Drag the globe to a different level to give them more access.')) .then(() => this.renderCleanup())) + // Drag the ACL agent class, not the globe image's URL. + button.querySelector('img')?.setAttribute('draggable', 'false') + widgets.makeDraggable(button, ns.foaf('Agent')) + return button } private renderAuthenticatedAgentButton (): HTMLElement { diff --git a/src/components/access-control-modal/AccessControlAddAccess.styles.css b/src/components/access-control-modal/AccessControlAddAccess.styles.css new file mode 100644 index 000000000..0257c9f2b --- /dev/null +++ b/src/components/access-control-modal/AccessControlAddAccess.styles.css @@ -0,0 +1,117 @@ +:host { + .access-grants-form { + display: flex; + flex-direction: column; + padding: 10px; + align-items: flex-start; + gap: 10px; + align-self: stretch; + font-size: var(--solid-ui-font-size-sm, .875rem); + + --access-role-select-width: 129px; + + p { + color: var(--solid-ui-color-gray-600, #4A5565); + line-height: 1.5; + } + + .access-grants-form-main { + display: flex; + align-items: center; + gap: 10px; + align-self: stretch; + + .access-grants-input { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-width: 0; + padding: 10px 15px; + gap: 12px; + align-items: flex-start; + border-radius: 5px; + border: 1px solid var(--solid-ui-color-slate-200, #E2E8F0); + background: var(--solid-ui-color-white, #FFF); + + solid-ui-combobox { + width: 100%; + --solid-ui-input-border-color: var(--solid-ui-color-slate-200, #E2E8F0); + } + + .access-principal-error { + margin: 0; + color: var(--solid-ui-color-red-700, #B42318); + font-size: var(--solid-ui-font-size-xs, .75rem); + line-height: 1.4; + } + } + } + + .access-grants-pending { + display: flex; + flex-wrap: wrap; + gap: 8px; + min-height: 42px; + align-items: center; + } + + .access-grants-pending-item { + display: flex; + padding: 5px 10px; + align-items: center; + gap: 6px; + max-width: 100%; + border-radius: 5px; + border: 1px solid var(--solid-ui-color-gray-300, #D1D5DC); + background: var(--solid-ui-color-gray-50, #F9FAFB); + color: var(--solid-ui-color-gray-700, #374151); + } + + .access-grants-pending-item-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--solid-ui-color-gray-500, #6A7282); + font-size: var(--solid-ui-font-size-xs, .75rem); + line-height: 1; + } + + .access-grants-pending-item-remove { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + border: none; + border-radius: 999px; + background: transparent; + color: var(--solid-ui-color-gray-300, #D1D5DC); + cursor: pointer; + + icon-lucide-circle-x { + width: 14px; + height: 14px; + } + } + } + + .access-grants-directory-entry { + display: inline-flex; + align-items: center; + gap: 8px; + line-height: 1; + } + + .access-role-select { + display: block; + max-width: 100%; + margin-top: 25px; + flex: 0 0 auto; + width: var(--access-role-select-width); + --solid-ui-input-border-color: var(--solid-ui-color-slate-200, #E2E8F0); + } + + .access-role-select--top { + margin-top: 0; + } +} diff --git a/src/components/access-control-modal/AccessControlAddAccess.ts b/src/components/access-control-modal/AccessControlAddAccess.ts new file mode 100644 index 000000000..4d10ccd9e --- /dev/null +++ b/src/components/access-control-modal/AccessControlAddAccess.ts @@ -0,0 +1,190 @@ +import { customElement, WebComponent } from '@/lib/components' +import { html, nothing } from 'lit' +import { property } from 'lit/decorators.js' +import { ACCESS_ROLES, type AccessRole, type DirectoryEntry } from 'solid-logic' +import type { AsyncComboboxOptionsProvider, ComboboxChangeEvent, ComboboxOptionData } from '@/components/combobox' + +import '~icons/lucide/book-user' +import '~icons/lucide/user-round' +import '~icons/lucide/users' +import '~icons/lucide/circle-x' +import '~icons/lucide/globe' +import '@/components/button' +import '@/components/combobox' +import '@/components/combobox-option' + +import styles from './AccessControlAddAccess.styles.css' +import type { PendingAccessGrant, PendingGrantRemovedDetail, RoleChangedDetail, TargetInputDetail, TargetSelectedDetail } from './types' + +export function directoryEntryOptionTemplate (entry: DirectoryEntry) { + return html` + + ${renderDirectoryEntryIcon(entry)} + ${entry.label} + + ` +} + +function renderDirectoryEntryIcon (entry: DirectoryEntry) { + if (entry.sources.includes('contacts') || entry.sources.includes('groups')) { + return html`` + } + + if (entry.sources.includes('friends')) { + return html`` + } + + if (entry.sources.includes('catalog')) { + return html`` + } + + return nothing +} + +@customElement('solid-ui-access-control-add-access') +export default class AccessControlAddAccess extends WebComponent { + static styles = styles + + @property({ attribute: false }) + accessor principalInputValue = '' + + @property({ attribute: false }) + accessor addAccessRoleValue: AccessRole = 'Viewer' + + @property({ attribute: false }) + accessor pendingAccessGrants: PendingAccessGrant[] = [] + + @property({ attribute: false }) + accessor inputError = '' + + @property({ type: Boolean }) + accessor disabled = false + + @property({ attribute: false }) + accessor asyncOptionsProvider: AsyncComboboxOptionsProvider | null = null + + private onPrincipalInput (event: Event) { + const value = this.getEventValue(event) + this.dispatchEvent(new CustomEvent('target-input', { + detail: { value }, + bubbles: true, + composed: true + })) + } + + private onPrincipalSelect (event: ComboboxChangeEvent) { + const option = event.detail.option as ComboboxOptionData & { targetKind?: 'origin' | 'trustedApp' } + if (typeof option.value !== 'string') { + return + } + + this.dispatchEvent(new CustomEvent('target-selected', { + detail: { + value: option.value, + label: option.label, + targetKind: option.targetKind + }, + bubbles: true, + composed: true + })) + } + + private onRoleChange (event: ComboboxChangeEvent) { + const role = event.detail.option.value + const accessRole = typeof role === 'string' + ? ACCESS_ROLES.find(candidate => candidate === role) + : undefined + if (!accessRole) { + return + } + + this.dispatchEvent(new CustomEvent('role-changed', { + detail: { role: accessRole }, + bubbles: true, + composed: true + })) + } + + private onPendingGrantRemove (index: number) { + this.dispatchEvent(new CustomEvent('pending-grant-removed', { + detail: { index }, + bubbles: true, + composed: true + })) + } + + private getEventValue (event: Event): string { + const target = event.currentTarget as { value?: string | null } | null + return typeof target?.value === 'string' ? target.value : '' + } + + protected render () { + return html` +
+

Add person, group or software agent URL.

+
+
+ + ${this.inputError + ? html`` + : nothing} + ${this.renderPendingAccessGrants()} +
+ + ${ACCESS_ROLES + .filter(role => role !== 'No Access') + .map(role => html` + ${role} + `)} + +
+
+ ` + } + + private renderPendingAccessGrants () { + if (!this.pendingAccessGrants.length) { + return nothing + } + + return html` +
+ ${this.pendingAccessGrants.map((grant, index) => html` +
+ + ${grant.targetKind === 'trustedApp' + ? html`` + : nothing} + ${grant.label} + + this.onPendingGrantRemove(index)} + > + Remove ${grant.label} + + +
+ `)} +
+ ` + } +} diff --git a/src/components/access-control-modal/AccessControlErrors.styles.css b/src/components/access-control-modal/AccessControlErrors.styles.css new file mode 100644 index 000000000..9d30cb117 --- /dev/null +++ b/src/components/access-control-modal/AccessControlErrors.styles.css @@ -0,0 +1,48 @@ +:host { + display: block; + width: 100%; + contain: inline-size; + + solid-ui-error-state { + width: 100%; + max-width: 100%; + contain: inline-size; + } + + .access-control-save-errors { + display: grid; + gap: 8px; + width: 100%; + margin: 0; + padding-left: 0; + box-sizing: border-box; + list-style: none; + + li { + display: grid; + grid-template-columns: minmax(0, 18rem) minmax(0, 1fr); + align-items: baseline; + gap: 6px; + min-width: 0; + color: var(--solid-ui-color-red-600, #be2e48); + font-size: var(--solid-ui-font-size-sm, .875rem); + line-height: 1.4; + } + + .access-control-save-error-label { + font-weight: 500; + overflow-wrap: anywhere; + } + + .access-control-save-error-detail { + min-width: 0; + overflow-wrap: anywhere; + } + } + + @media (max-width: 600px) { + .access-control-save-errors li { + grid-template-columns: minmax(0, 40%) minmax(0, 1fr); + } + } +} diff --git a/src/components/access-control-modal/AccessControlErrors.ts b/src/components/access-control-modal/AccessControlErrors.ts new file mode 100644 index 000000000..fa8041153 --- /dev/null +++ b/src/components/access-control-modal/AccessControlErrors.ts @@ -0,0 +1,71 @@ +import { customElement, WebComponent } from '@/lib/components' +import { html } from 'lit' +import { property } from 'lit/decorators.js' + +import '@/components/error-state' + +import styles from './AccessControlErrors.styles.css' +import type { AccessSaveError } from './types' + +@customElement('solid-ui-access-control-errors') +export default class AccessControlErrors extends WebComponent { + static styles = styles + + @property({ attribute: false }) + accessor errors: AccessSaveError[] = [] + + @property({ attribute: false }) + accessor successfulChangesRefreshed = true + + private onClose (event: Event) { + event.stopPropagation() + this.dispatchEvent(new CustomEvent('close', { + bubbles: true, + composed: true + })) + } + + private getScopeLabel (scope: NonNullable['scope']>) { + return scope === 'folder+contents' + ? 'folder and contents' + : scope === 'container' + ? 'folder' + : scope === 'contents' + ? 'contents' + : 'resource' + } + + protected render () { + if (!this.errors.length) { + return html`` + } + + return html` + +
    + ${this.errors.map(error => html` +
  • + ${error.label} + + ${error.action === 'grant' + ? `Could not grant ${error.role ?? 'access'} for ${error.scope ? this.getScopeLabel(error.scope) : 'resource'}` + : error.action === 'revoke' + ? `Could not remove access for ${error.scope ? this.getScopeLabel(error.scope) : 'resource'}` + : error.action === 'reset' + ? 'Could not reset sharing' + : 'Could not save access changes'} + + ${error.message} +
  • + `)} +
+
+ ` + } +} diff --git a/src/components/access-control-modal/AccessControlGeneralAccess.styles.css b/src/components/access-control-modal/AccessControlGeneralAccess.styles.css new file mode 100644 index 000000000..1c07efed5 --- /dev/null +++ b/src/components/access-control-modal/AccessControlGeneralAccess.styles.css @@ -0,0 +1,117 @@ +:host { + h2 { + color: var(--solid-ui-color-gray-900, #101828); + font-size: var(--solid-ui-font-size-md, 1rem); + font-weight: 600; + } + + .access-grants-general { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 10px; + padding: 0 10px; + align-self: stretch; + + solid-ui-combobox, + solid-ui-input { + --solid-ui-input-border-color: var(--solid-ui-color-slate-200, #E2E8F0); + } + + .access-grants-general-header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 15px; + align-self: stretch; + + .access-grants-copy-link-button-label { + font-size: var(--solid-ui-font-size-xs, .75rem); + font-weight: 500; + } + + .access-grants-copy-link-button icon-lucide-link { + width: 12px; + height: 12px; + aspect-ratio: 1/1; + } + } + + .access-grants-general-share { + display: flex; + align-items: center; + gap: 10px; + align-self: stretch; + + .access-grants-general-share-content { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 10px; + flex: 1 0 0; + + .access-grants-general-share-icon { + display: flex; + width: 30px; + height: 30px; + padding: 5px; + justify-content: center; + align-items: center; + aspect-ratio: 1/1; + border-radius: 50px; + background: var(--solid-ui-color-violet-50, #F5F3FF); + } + + .access-grants-general-share-icon-inner { + display: flex; + width: 20px; + height: 20px; + padding: 1.25px; + justify-content: center; + align-items: center; + flex-shrink: 0; + aspect-ratio: 1/1; + + icon-lucide-globe { + width: 17.5px; + height: 17.5px; + flex-shrink: 0; + aspect-ratio: 1/1; + } + } + + .access-grants-general-share-text { + flex: 1 1 auto; + min-width: 0; + + .access-grants-general-share-text-title { + color: var(--solid-ui-color-gray-900, #101828); + font-size: var(--solid-ui-font-size-sm, .875rem); + font-weight: 600; + } + + .access-grants-general-share-text-description { + color: var(--solid-ui-color-gray-700, #364153); + font-size: var(--solid-ui-font-size-sm, .875rem); + } + } + } + } + } + + .access-role-select--general { + width: 120px; + min-width: 120px; + flex: 0 0 120px; + font-size: var(--solid-ui-font-size-xs, .75rem); + font-weight: 500; + } + + .access-role-select--compact { + display: block; + max-width: 100%; + --solid-ui-input-border-color: transparent; + margin-top: 0; + } +} diff --git a/src/components/access-control-modal/AccessControlGeneralAccess.ts b/src/components/access-control-modal/AccessControlGeneralAccess.ts new file mode 100644 index 000000000..e634187ab --- /dev/null +++ b/src/components/access-control-modal/AccessControlGeneralAccess.ts @@ -0,0 +1,169 @@ +import { customElement, WebComponent } from '@/lib/components' +import { html } from 'lit' +import { property } from 'lit/decorators.js' +import { ACCESS_ROLES, PUBLIC_ACCESS_ROLES, type AccessRole } from 'solid-logic' +import * as debug from '@/lib/debug' +import type { ComboboxChangeEvent } from '@/components/combobox' + +import '~icons/lucide/link' +import '~icons/lucide/globe' +import '@/components/button' +import '@/components/combobox' +import '@/components/combobox-option' + +import styles from './AccessControlGeneralAccess.styles.css' +import type { RoleChangedDetail } from './types' + +@customElement('solid-ui-access-control-general-access') +export default class AccessControlGeneralAccess extends WebComponent { + static styles = styles + + @property({ attribute: false }) + accessor subjectUri: string | undefined = undefined + + @property({ attribute: false }) + accessor authenticatedAccessRoleValue: AccessRole = 'No Access' + + @property({ attribute: false }) + accessor publicAccessRoleValue: AccessRole = 'No Access' + + @property({ type: Boolean }) + accessor disabled = false + + private onAuthenticatedRoleChange (event: ComboboxChangeEvent) { + this.dispatchRoleChange('authenticated-role-changed', event, ACCESS_ROLES) + } + + private onPublicRoleChange (event: ComboboxChangeEvent) { + this.dispatchRoleChange('public-role-changed', event, PUBLIC_ACCESS_ROLES) + } + + private dispatchRoleChange ( + eventName: 'authenticated-role-changed' | 'public-role-changed', + event: ComboboxChangeEvent, + roles: readonly string[] + ) { + const value = event.detail.option.value + if (typeof value !== 'string' || !roles.includes(value)) { + return + } + + const detail: RoleChangedDetail = { role: value as AccessRole } + this.dispatchEvent(new CustomEvent(eventName, { + detail, + bubbles: true, + composed: true + })) + } + + private async onCopyLinkClick (event: Event) { + event.preventDefault() + + if (!this.subjectUri) { + return + } + + try { + await navigator.clipboard.writeText(this.subjectUri) + } catch (error) { + debug.error('Failed to copy resource link', error) + } + } + + private renderAuthenticatedRoleOptions () { + return ACCESS_ROLES.map(role => html` + ${role} + `) + } + + private renderPublicRoleOptions () { + return PUBLIC_ACCESS_ROLES.map(role => html` + ${role} + `) + } + + private renderAuthenticatedModeSelector () { + return html` + + ${this.renderAuthenticatedRoleOptions()} + + ` + } + + private renderPublicModeSelector () { + return html` + + ${this.renderPublicRoleOptions()} + + ` + } + + private renderGeneralAccessIcon () { + return html` + + ` + } + + private renderGeneralShareRow (options: { + title: string + description: string + variant: 'authenticated' | 'public' + }) { + return html` + + ` + } + + protected render () { + return html` +
+
+

General Access

+ + + Copy Link + +
+ ${this.renderGeneralShareRow({ + title: 'Only logged in users with the link', + description: 'Users must sign in to SolidOS to access this shared item using the link.', + variant: 'authenticated' + })} + ${this.renderGeneralShareRow({ + title: 'Anyone with the Link', + description: 'Anyone on the internet with the link can view.', + variant: 'public' + })} +
+ ` + } +} diff --git a/src/components/access-control-modal/AccessControlModal.styles.css b/src/components/access-control-modal/AccessControlModal.styles.css new file mode 100644 index 000000000..8c3109b2a --- /dev/null +++ b/src/components/access-control-modal/AccessControlModal.styles.css @@ -0,0 +1,283 @@ +:host { + .access-control-mixed-policy-warning { + display: grid; + gap: 4px; + margin: 0 0 16px; + padding: 12px 14px; + border: 1px solid var(--solid-ui-color-red-300, #FCA5A5); + border-radius: 5px; + background: var(--solid-ui-color-red-50, #FEF2F2); + color: var(--solid-ui-color-red-800, #991B1B); + font-size: var(--solid-ui-font-size-sm, .875rem); + line-height: 1.45; + + strong { + font-weight: 600; + } + } + + .access-control-same-access-toggle { + display: flex; + align-items: flex-start; + gap: 8px; + margin-top: 12px; + color: var(--solid-ui-color-gray-900, #101828); + line-height: 1.4; + border: 0; + background: transparent; + padding: 0; + text-align: left; + font: inherit; + cursor: pointer; + } + + .access-control-switch-track { + display: inline-flex; + flex: 0 0 auto; + width: 34px; + height: 20px; + margin-top: 1px; + border-radius: 999px; + background: var(--solid-ui-color-slate-300, #CBD5E1); + padding: 2px; + transition: background .15s ease; + } + + .access-control-same-access-toggle[aria-checked='true'] .access-control-switch-track { + background: var(--solid-ui-color-purple-600, #9333EA); + } + + .access-control-switch-thumb { + width: 16px; + height: 16px; + border-radius: 50%; + background: white; + box-shadow: 0 1px 2px rgb(0 0 0 / 25%); + transition: transform .15s ease; + } + + .access-control-same-access-toggle[aria-checked='true'] .access-control-switch-thumb { + transform: translateX(14px); + } + + .access-control-same-access-toggle > span:last-child { + min-width: 0; + max-width: 100%; + overflow-wrap: anywhere; + } + + .access-control-sharing-state, + .access-control-contents-prompt { + box-sizing: border-box; + min-width: 0; + max-width: 100%; + padding: 10px; + margin-bottom: 16px; + font-size: var(--solid-ui-font-size-sm, .875rem); + + h2 { + margin: 0 0 10px; + color: var(--solid-ui-color-gray-900, #101828); + font-size: var(--solid-ui-font-size-md, 1rem); + font-weight: 600; + } + + .access-control-advanced-sharing { + box-sizing: border-box; + min-width: 0; + width: 100%; + max-width: 100%; + overflow-wrap: anywhere; + margin-top: 10px; + padding: 10px 15px; + position: relative; + padding-right: 48px; + + h2 { + margin: 0 0 4px; + color: var(--solid-ui-color-gray-900, #101828); + font-size: var(--solid-ui-font-size-md, 1rem); + font-weight: 600; + } + + p { + margin: 0; + min-width: 0; + max-width: 100%; + overflow-wrap: anywhere; + color: var(--solid-ui-color-gray-600, #4A5565); + font-size: var(--solid-ui-font-size-xs, .75rem); + line-height: 1.5; + } + + .access-control-advanced-sharing-description-line { + display: block; + max-width: 100%; + } + } + + .access-control-advanced-sharing-options, + .access-control-advanced-sharing-actions { + display: flex; + flex-wrap: wrap; + min-width: 0; + gap: 8px; + margin-top: 10px; + + solid-ui-button { + --solid-ui-font-size-md: var(--solid-ui-font-size-sm, .875rem); + min-width: 0; + max-width: 100%; + white-space: normal; + } + } + + .access-control-advanced-sharing-actions { + justify-content: flex-end; + position: absolute; + top: 10px; + right: 15px; + margin-top: 0; + + solid-ui-button { + color: var(--solid-ui-color-gray-600, #4A5565); + } + } + + p { + margin: 0; + min-width: 0; + max-width: 100%; + overflow-wrap: anywhere; + color: var(--solid-ui-color-gray-600, #4A5565); + line-height: 1.5; + } + } + + .access-control-sharing-state-panel, + .access-control-contents-prompt { + box-sizing: border-box; + min-width: 0; + max-width: 100%; + border: 1px solid var(--solid-ui-color-slate-200, #E2E8F0); + border-radius: 5px; + background: var(--solid-ui-color-white, #FFF); + padding: 10px 15px; + } + + .access-control-sharing-state-panel { + display: grid; + gap: 10px; + + p { + min-width: 0; + max-width: 100%; + overflow-wrap: anywhere; + } + + .access-control-sharing-state-custom-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + + p { + flex: 1 1 auto; + } + + solid-ui-button { + flex: 0 0 auto; + --solid-ui-font-size-md: var(--solid-ui-font-size-sm, .875rem); + } + } + } + + .access-control-sharing-guidance { + display: flex; + align-items: center; + justify-content: space-between; + min-width: 0; + gap: 12px; + + p { + min-width: 0; + max-width: 100%; + overflow-wrap: anywhere; + } + + solid-ui-button { + flex: 0 0 auto; + --solid-ui-font-size-md: var(--solid-ui-font-size-sm, .875rem); + } + } + + .access-control-sharing-state-actions { + display: flex; + flex-direction: column; + align-items: flex-end; + min-width: 0; + justify-content: flex-end; + margin-top: 10px; + gap: 8px; + + solid-ui-button { + --solid-ui-font-size-md: var(--solid-ui-font-size-sm, .875rem); + max-width: 100%; + } + } + + @media (max-width: 420px) { + .access-control-sharing-guidance { + align-items: flex-start; + flex-direction: column; + gap: 8px; + + solid-ui-button { + align-self: flex-end; + } + } + } + + .access-control-contents-prompt { + .access-control-contents-prompt-actions { + display: flex; + flex-wrap: wrap; + min-width: 0; + gap: 8px; + margin-top: 10px; + } + + solid-ui-button { + --solid-ui-font-size-md: var(--solid-ui-font-size-sm, .875rem); + max-width: 100%; + flex: 0 1 auto; + } + } + + .access-control-contents-prompt h2 { + margin-bottom: 4px; + } + + .access-control-footer-actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 15px; + width: 100%; + + > solid-ui-button { + min-width: 132px; + } + } + + @media (max-width: 380px) { + .access-control-footer-actions { + gap: 8px; + + > solid-ui-button { + min-width: 0; + flex: 1 1 0; + } + } + } +} diff --git a/src/components/access-control-modal/AccessControlModal.ts b/src/components/access-control-modal/AccessControlModal.ts new file mode 100644 index 000000000..bf0c157f8 --- /dev/null +++ b/src/components/access-control-modal/AccessControlModal.ts @@ -0,0 +1,912 @@ +import { customElement, DialogComponent } from '@/lib/components' +import { html } from 'lit' +import type { PropertyValues } from 'lit' +import { property, state } from 'lit/decorators.js' +import { label } from '@/utils/label' +import * as debug from '@/lib/debug' +import { Authenticated, Public, analyzeContainerAccessScope, filterAuthorizationsByContainerScope, solidLogicSingleton, type ACLContext, type AccessRole, type Authorization, type ContainerAccessScope, type ContainerAccessScopeResult } from 'solid-logic' +import { sym } from 'rdflib' + +import '~icons/lucide/circle-x' +import '~icons/lucide/folder' +import '~icons/lucide/folder-open' +import '~icons/lucide/x' +import '@/components/dialog' +import '@/components/dialog-content' +import '@/components/dialog-footer' +import '@/components/button' +import '@/components/combobox' +import '@/components/combobox-option' +import './AccessControlSharedAccess' +import './AccessControlGeneralAccess' +import './AccessControlErrors' + +import styles from './AccessControlModal.styles.css' +import type { AccessChange, AccessControlError, AccessGrantDraft, AccessGrantRoleChangedEvent, AccessSaveError, AccessSaveScope, InputAccessControlError, PendingAccessGrant, PendingGrantRemovedEvent, RoleChangedEvent, TargetInputEvent, TargetSelectedEvent } from './types' +import { createAccessGrantDrafts, getAccessGrantIds, getAccessGrantViewModels, getAuthenticatedAccessRole, getAuthorizationSubjectLabel, getChangedAccessGrants, getPublicAccessRole, getSharedAuthorization } from './accessGrantDrafts' +import { accessPrincipalOptionsProvider, resolveAccessTarget } from './accessTargetResolver' +import { applyAccessChanges } from './applyAccessChanges' +import { buildAccessChanges } from './buildAccessChanges' + +function isDefined (value: T | undefined | null): value is T { + return value !== undefined && value !== null +} + +type PendingAccessGrantCreationResult = + | { grant: PendingAccessGrant } + | { error: InputAccessControlError } + +@customElement('solid-ui-access-control-modal') +export default class AccessControlModal extends DialogComponent { + static styles = styles + + @property({ attribute: false }) + accessor subjectUri: string | undefined = undefined + + @property({ attribute: false }) + accessor accessGrants: Authorization[] | undefined = undefined + + @state() + private accessor principalInputValue: string = '' + + @state() + private accessor addAccessRoleValue: AccessRole = 'Viewer' + + @state() + private accessor authenticatedAccessRoleValue: AccessRole = 'No Access' + + @state() + private accessor publicAccessRoleValue: AccessRole = 'No Access' + + @state() + private accessor submitting: boolean = false + + @state() + private accessor pendingAccessGrants: PendingAccessGrant[] = [] + + @state() + private accessor accessGrantDrafts: AccessGrantDraft[] = [] + + @state() + private accessor errors: AccessControlError[] = [] + + @state() + private accessor successfulChangesRefreshed = true + + @state() + private accessor accessContext: ACLContext | undefined + + @state() + private accessor accessContextLoading = false + + @state() + private accessor accessContextError = '' + + // Intentionally has no opener this release: scope selectors and reset are deferred. + @state() + private accessor advancedSharingOpen = false + + @state() + private accessor containerScope: ContainerAccessScope = 'folder+contents' + + @state() + private accessor containerScopeAnalysis: ContainerAccessScopeResult | undefined + + @state() + private accessor protectedTarget = false + + private accessGrantLabelRequestVersion = 0 + private accessContextRequestVersion = 0 + private accessContextSubjectUri: string | undefined + + disconnectedCallback (): void { + this.accessGrantLabelRequestVersion++ + this.accessContextRequestVersion++ + this.errors = [] + super.disconnectedCallback() + } + + private get inputError (): string { + return this.errors.find(error => error.action === 'input')?.message ?? '' + } + + private get saveErrors (): AccessSaveError[] { + return this.errors.filter((error): error is AccessSaveError => error.action !== 'input') + } + + private setInputError (error: InputAccessControlError) { + this.errors = [ + ...this.errors.filter(item => item.action !== 'input'), + error + ] + } + + private clearInputError () { + this.errors = this.errors.filter(error => error.action !== 'input') + } + + protected willUpdate (changedProperties: PropertyValues) { + super.willUpdate(changedProperties) + + if (changedProperties.has('accessGrants')) { + const scopedAccessGrants = this.getScopedAccessGrants() + this.accessGrantDrafts = createAccessGrantDrafts(scopedAccessGrants) + this.authenticatedAccessRoleValue = getAuthenticatedAccessRole(scopedAccessGrants) + this.publicAccessRoleValue = getPublicAccessRole(scopedAccessGrants) + } + } + + protected updated (changedProperties: PropertyValues) { + super.updated(changedProperties) + + if (changedProperties.has('subjectUri')) { + this.loadAccessContext().catch(error => { + debug.error('Failed to load access control context', error) + }) + } + + if (changedProperties.has('accessGrants')) { + this.refreshAccessGrantLabels().catch((error: unknown) => { + debug.error('Failed to refresh access grant labels', error) + }) + } + + if (changedProperties.has('accessContext' as keyof AccessControlModal) && !changedProperties.has('subjectUri')) { + this.refreshContainerScopeAnalysis() + } + } + + private refreshContainerScopeAnalysis () { + if (this.isContainer() && this.accessContext) { + this.containerScopeAnalysis = analyzeContainerAccessScope(this.accessContext) + this.containerScope = this.containerScopeAnalysis.primaryScope + this.refreshScopedAccessGrantDrafts() + } else { + this.containerScopeAnalysis = undefined + } + } + + private async refreshAccessGrantLabels () { + const requestVersion = ++this.accessGrantLabelRequestVersion + const sourceAccessGrants = this.accessGrants + const subjectUri = this.subjectUri + const scope = this.getSelectedContainerScope() + const grants = this.getScopedAccessGrants(scope) + if (!grants.length) { + this.accessGrantDrafts = [] + return + } + + const labels = await Promise.all(grants.map(async authorization => { + await Promise.all(authorization.agentGroup.map(async groupUri => { + try { + await solidLogicSingleton.store.fetcher.load(sym(groupUri).doc()) + } catch { + // Keep the fallback label if the group document cannot be loaded. + } + })) + + return getAuthorizationSubjectLabel(getSharedAuthorization(authorization)) + })) + + if ( + requestVersion === this.accessGrantLabelRequestVersion && + this.accessGrants === sourceAccessGrants && + this.subjectUri === subjectUri && + this.getSelectedContainerScope() === scope + ) { + const labelsById = new Map(getAccessGrantIds(grants).map((id, index) => [id, labels[index]])) + this.accessGrantDrafts = this.accessGrantDrafts.map(draft => ({ + ...draft, + subjectLabel: labelsById.get(draft.id) ?? draft.subjectLabel + })) + } + } + + private getDialogTitle (): string { + const subject = this.subjectUri ? sym(this.subjectUri) : undefined + const subjectLabel = subject ? label(subject).trim() : '' + + if (!subjectLabel || subjectLabel === 'this resource') { + return 'Manage Access of "this resource"' + } + return `Manage Access of "${subjectLabel}"` + } + + private isContainer (): boolean { + return Boolean(this.subjectUri && new URL(this.subjectUri).pathname.endsWith('/')) + } + + private async loadAccessContext (): Promise { + const requestVersion = ++this.accessContextRequestVersion + const subjectUri = this.subjectUri + this.accessContext = undefined + this.accessContextSubjectUri = undefined + this.accessContextError = '' + this.accessContextLoading = Boolean(subjectUri) + + if (!subjectUri) { + this.accessContextLoading = false + return + } + + try { + const accessContext = await solidLogicSingleton.acl.findEffectiveAcl(subjectUri) + if (requestVersion !== this.accessContextRequestVersion || this.subjectUri !== subjectUri) { + return + } + + this.accessContext = accessContext + this.accessContextSubjectUri = subjectUri + if (this.accessContext?.inherited && !(this.accessGrants?.length)) { + // findAccessGrants only reports ACLs attached directly to the resource. + // Inherited permissions are the effective grants users need to review. + this.accessGrants = this.accessContext.authorizations + } + this.refreshContainerScopeAnalysis() + this.protectedTarget = solidLogicSingleton.acl.isProtectedResource(subjectUri) + if (this.protectedTarget) { + this.refreshScopedAccessGrantDrafts() + } + } catch (error) { + if (requestVersion !== this.accessContextRequestVersion || this.subjectUri !== subjectUri) { + return + } + + this.accessContextError = error instanceof Error && error.message.trim() + ? error.message + : 'Unable to load access settings for this resource.' + debug.error('Failed to load access control context', error) + } finally { + if (requestVersion === this.accessContextRequestVersion && this.subjectUri === subjectUri) { + this.accessContextLoading = false + } + } + } + + private async ensureAccessContext (): Promise { + if (!this.subjectUri) { + return false + } + + // Direct method calls are used by consumers and unit tests before the + // element is connected; the rendered controls are always guarded below. + if (!this.isConnected) { + return true + } + + if (this.accessContextSubjectUri === this.subjectUri && this.accessContext && !this.accessContextError) { + return true + } + + await this.loadAccessContext() + return this.accessContextSubjectUri === this.subjectUri && Boolean(this.accessContext) && !this.accessContextError + } + + private canMutate (): boolean { + if (!this.isConnected) { + return true + } + + if (!this.subjectUri) { + return true + } + + return Boolean( + this.subjectUri && + this.accessContextSubjectUri === this.subjectUri && + this.accessContext && + !this.accessContextLoading && + !this.accessContextError + ) + } + + private async retryAccessContext () { + await this.loadAccessContext() + } + + private renderSharingInformation () { + if (!this.accessContext) { + return html`` + } + + const isCustomSharing = !this.accessContext.inherited + const inheritedContainer = this.accessContext?.effectiveContainer + const inheritedContainerLabel = inheritedContainer + ? label(sym(inheritedContainer)).trim() + : '' + const defaultContainer = inheritedContainerLabel && inheritedContainerLabel !== inheritedContainer + ? html`${inheritedContainerLabel} (${inheritedContainer})` + : html`${inheritedContainer ?? 'the parent container'}` + return html` + + ` + } + + private renderSharingState () { + if (!this.accessContext || !this.advancedSharingOpen) { + return html`` + } + + return html` + + ` + } + + private getSelectedContainerScope (): ContainerAccessScope { + return this.containerScope + } + + private hasMixedContainerPolicy (): boolean { + return this.isContainer() && this.containerScopeAnalysis?.mixed === true + } + + private renderAccessContextState () { + if (this.accessContextLoading) { + return html`

Loading access settings...

` + } + + if (this.accessContextError) { + return html` + + + Retry + + + ` + } + + if (!this.accessContext) { + return html`

Access settings are not ready yet.

` + } + + return html`` + } + + private renderMixedContainerWarning () { + if (!this.hasMixedContainerPolicy()) { + return html`` + } + + return html` + + ` + } + + private getScopedAccessGrants (scope = this.getSelectedContainerScope()): Authorization[] { + const grants = this.accessGrants ?? [] + if (!this.isContainer() || !this.subjectUri || this.protectedTarget) { + return grants + } + return filterAuthorizationsByContainerScope(this.subjectUri, grants, scope, this.accessContext?.inherited ?? false) + } + + private refreshScopedAccessGrantDrafts () { + const scopedAccessGrants = this.getScopedAccessGrants() + this.accessGrantDrafts = createAccessGrantDrafts(scopedAccessGrants) + this.authenticatedAccessRoleValue = getAuthenticatedAccessRole(scopedAccessGrants) + this.publicAccessRoleValue = getPublicAccessRole(scopedAccessGrants) + } + + private selectContainerScope (scope: 'folder+contents' | 'contents' | 'folder') { + this.containerScope = scope + this.refreshScopedAccessGrantDrafts() + } + + private renderAdvancedSharing () { + if (!this.advancedSharingOpen) return html`` + + return html` +
+

Advanced sharing

+

+ Use this option when the folder and its contents have different access requirements. + Choose which access settings to edit. + Save Changes applies your current edits only to that scope. +

+ ${this.isContainer() + ? html` + +
+ { this.selectContainerScope('folder') }} + > + Folder Only + + { this.selectContainerScope('contents') }} + > + Contents Only + +
+ ` + : html``} +
+ + + Close + +
+
+ ` + } + + protected render () { + const dialogTitle = this.getDialogTitle() + + return html` + +
+ + ${this.renderAccessContextState()} + ${this.renderMixedContainerWarning()} + + + + + ${this.renderSharingState()} + + + + + +
+
+ ` + } + + private async onSubmit (e: Event) { + e.preventDefault() + + if (this.submitting || this.hasMixedContainerPolicy()) { + return + } + + if (this.isConnected && !await this.ensureAccessContext()) { + return + } + + await this.commitPrincipalFromInputSafely() + } + + private async onSaveClick () { + if (this.submitting || this.hasMixedContainerPolicy()) { + return + } + + if (this.isConnected && !await this.ensureAccessContext()) { + return + } + + if (this.principalInputValue.trim()) { + const committed = await this.commitPrincipalFromInputSafely() + + if (!committed) { + return + } + } + + const scope = this.isContainer() && !this.protectedTarget + ? this.getSelectedContainerScope() + : 'file' + await this.saveAccessChanges(scope === 'folder' ? 'container' : scope) + } + + private onCancelClick () { + this.close() + } + + private closeAdvancedSharing () { + this.advancedSharingOpen = false + } + + private async onResetToDefault () { + if (!this.subjectUri || !this.canMutate() || !this.accessContext || this.accessContext.inherited || this.submitting || this.hasMixedContainerPolicy()) return + + this.submitting = true + this.errors = this.errors.filter(error => error.action === 'input') + try { + await solidLogicSingleton.acl.resetToDefault(this.subjectUri) + await this.loadAccessContext() + const directAccessGrants = await solidLogicSingleton.acl.findAccessGrants(this.subjectUri) + this.accessGrants = this.accessContext?.inherited + ? this.accessContext.authorizations + : directAccessGrants + } catch (error) { + this.errors = [...this.errors, { + action: 'reset', + targetUri: this.subjectUri, + label: 'Custom sharing', + message: error instanceof Error ? error.message : 'Unable to reset sharing.' + }] + debug.error('Failed to reset access control', error) + } finally { + this.submitting = false + } + } + + private async saveAccessChanges (scope: AccessSaveScope = 'file') { + if (this.submitting || !this.canMutate() || this.hasMixedContainerPolicy()) { + return + } + + const changedAccessGrants = getChangedAccessGrants(this.accessGrantDrafts) + const generalAccessChanges = this.getGeneralAccessChanges() + + if (!this.pendingAccessGrants.length && !changedAccessGrants.length && !generalAccessChanges.length) { + return + } + + if (!this.subjectUri) { + return + } + + this.submitting = true + this.errors = this.errors.filter(error => error.action === 'input') + this.successfulChangesRefreshed = true + + try { + const accessChanges = buildAccessChanges({ + pendingAccessGrants: this.pendingAccessGrants, + generalAccessChanges, + changedAccessGrants + }) + + const saveErrors = await applyAccessChanges(this.subjectUri, accessChanges, { + containerScope: scope + }) + if (saveErrors.length) { + this.errors = [...this.errors, ...saveErrors] + this.principalInputValue = '' + this.pendingAccessGrants = [] + + try { + this.accessGrants = await solidLogicSingleton.acl.findAccessGrants(this.subjectUri) + } catch (error) { + this.successfulChangesRefreshed = false + this.errors = [ + ...this.errors, + { + action: 'save', + targetUri: this.subjectUri, + label: 'Shared with list', + message: error instanceof Error + ? error.message + : 'Unable to refresh the access list.' + } + ] + debug.error('Failed to refresh access grants after save errors', error) + } + + debug.error('Failed to save some access changes', saveErrors) + return + } + + this.principalInputValue = '' + this.pendingAccessGrants = [] + this.close() + } catch (error) { + this.errors = [ + ...this.errors, + { + action: 'save', + targetUri: this.subjectUri ?? '', + label: 'Access changes', + message: error instanceof Error + ? error.message + : 'Unexpected error while saving access changes.' + } + ] + } finally { + this.submitting = false + } + } + + private onPrincipalInput (event: TargetInputEvent) { + if (!this.canMutate() || this.hasMixedContainerPolicy()) return + this.principalInputValue = event.detail.value + this.clearInputError() + } + + private onPrincipalSelect (event: TargetSelectedEvent) { + if (!this.canMutate() || this.hasMixedContainerPolicy()) return + this.queuePendingPrincipal(event.detail.value, this.addAccessRoleValue, event.detail.label, undefined, event.detail.targetKind) + } + + private getGeneralAccessChanges (): AccessChange[] { + if (!this.subjectUri) { + return [] + } + + const accessGrants = this.getScopedAccessGrants() + const currentAuthenticatedRole = getAuthenticatedAccessRole(accessGrants) + const currentPublicRole = getPublicAccessRole(accessGrants) + + const authenticatedChange = this.authenticatedAccessRoleValue === currentAuthenticatedRole + ? undefined + : { + subject: Authenticated, + role: this.authenticatedAccessRoleValue, + label: 'Authenticated users' + } + + const publicChange = this.publicAccessRoleValue === currentPublicRole + ? undefined + : { + subject: Public, + role: this.publicAccessRoleValue, + label: 'Public' + } + + return [authenticatedChange, publicChange].filter(isDefined) + } + + private onAddAccessRoleInput (event: RoleChangedEvent) { + if (!this.canMutate() || this.hasMixedContainerPolicy()) return + this.addAccessRoleValue = event.detail.role + this.pendingAccessGrants = this.pendingAccessGrants.map(grant => ({ + ...grant, + role: event.detail.role + })) + } + + private onAuthenticatedAccessRoleInput (event: RoleChangedEvent) { + if (!this.canMutate() || this.hasMixedContainerPolicy()) return + this.authenticatedAccessRoleValue = event.detail.role + } + + private onPublicAccessRoleInput (event: RoleChangedEvent) { + if (!this.canMutate() || this.hasMixedContainerPolicy()) return + this.publicAccessRoleValue = event.detail.role + } + + private onAccessGrantRoleInput (event: AccessGrantRoleChangedEvent) { + if (!this.canMutate() || this.hasMixedContainerPolicy()) return + this.accessGrantDrafts = this.accessGrantDrafts.map(draft => + draft.id === event.detail.id + ? { ...draft, role: event.detail.role } + : draft + ) + } + + private onPendingGrantRemoved (event: PendingGrantRemovedEvent) { + if (!this.canMutate() || this.hasMixedContainerPolicy()) return + this.pendingAccessGrants = this.pendingAccessGrants.filter((_, pendingIndex) => pendingIndex !== event.detail.index) + } + + private hasUnsavedChanges (): boolean { + return Boolean( + this.pendingAccessGrants.length || + this.principalInputValue.trim() || + getChangedAccessGrants(this.accessGrantDrafts).length || + this.getGeneralAccessChanges().length + ) + } + + private async commitPrincipalFromInput (): Promise { + if (!this.canMutate() || this.hasMixedContainerPolicy()) { + return false + } + + const rawValue = this.principalInputValue.trim() + if (!rawValue) { + return false + } + + return this.queuePendingPrincipal(rawValue, this.addAccessRoleValue, undefined, rawValue) + } + + private async commitPrincipalFromInputSafely (): Promise { + const inputSnapshot = this.principalInputValue.trim() + + try { + return await this.commitPrincipalFromInput() + } catch (error) { + if (this.principalInputValue.trim() !== inputSnapshot) { + debug.error('Ignored stale access-target validation error', error) + return false + } + + this.setInputError({ + action: 'input', + label: 'Access target', + targetUri: this.principalInputValue.trim(), + message: 'We could not verify this access target. Check the value and try again.' + }) + debug.error('Failed to commit pending access grants', error) + return false + } + } + + private async queuePendingPrincipal ( + principal: string, + role: AccessRole = this.addAccessRoleValue, + preferredLabel?: string, + inputSnapshot?: string, + targetKind?: 'origin' | 'trustedApp' + ): Promise { + const snapshot = inputSnapshot ?? this.principalInputValue.trim() + const result = await this.createPendingAccessGrant(principal, role, preferredLabel, targetKind) + + if ('error' in result) { + if (this.principalInputValue.trim() === snapshot) { + this.setInputError(result.error) + } + debug.error('Failed to add access grant:', result.error.message) + return false + } + + const merged = [...this.pendingAccessGrants, result.grant] + this.pendingAccessGrants = this.dedupePendingAccessGrants(merged) + + if (this.principalInputValue.trim() === snapshot) { + this.clearInputError() + this.principalInputValue = '' + } + + return true + } + + private async createPendingAccessGrant ( + principal: string, + role: AccessRole = this.addAccessRoleValue, + preferredLabel?: string, + targetKind?: 'origin' | 'trustedApp' + ): Promise { + try { + const normalizedPrincipal = principal.trim() + const resolvedTarget = await resolveAccessTarget(normalizedPrincipal, preferredLabel) + if (!resolvedTarget) { + return { + error: { + action: 'input', + label: 'Access target', + targetUri: normalizedPrincipal, + message: 'Could not verify this access target' + } + } + } + + return { + grant: { + subjectType: resolvedTarget.subjectType, + subjectValue: resolvedTarget.subjectValue, + role, + label: resolvedTarget.label, + targetKind: targetKind ?? resolvedTarget.targetKind + } + } + } catch (error) { + return { + error: { + action: 'input', + label: 'Access target', + targetUri: principal, + message: error instanceof Error && error.message.trim() + ? error.message + : 'Could not verify this access target' + } + } + } + } + + private dedupePendingAccessGrants (draftGrants: PendingAccessGrant[]): PendingAccessGrant[] { + const grantsBySubject = new Map() + for (const draftGrant of draftGrants) { + grantsBySubject.set(`${draftGrant.subjectType}:${draftGrant.subjectValue}`, draftGrant) + } + return [...grantsBySubject.values()] + } +} diff --git a/src/components/access-control-modal/AccessControlSharedAccess.styles.css b/src/components/access-control-modal/AccessControlSharedAccess.styles.css new file mode 100644 index 000000000..5c4198cec --- /dev/null +++ b/src/components/access-control-modal/AccessControlSharedAccess.styles.css @@ -0,0 +1,153 @@ +:host { + h2 { + color: var(--solid-ui-color-gray-900, #101828); + font-size: var(--solid-ui-font-size-md, 1rem); + font-weight: 600; + } + + .access-grants-header, + .access-grants-list { + padding: 0 10px; + align-self: stretch; + } + + .access-grants-header { + display: flex; + align-items: center; + gap: 15px; + justify-content: space-between; + + .access-grants-search-input { + flex: 0 0 auto; + width: 12ch; + min-width: 12ch; + --solid-ui-input-border-color: transparent; + color: var(--solid-ui-color-zinc-500, #71717B); + font-size: var(--solid-ui-font-size-sm, .875rem); + font-weight: 500; + } + } + + .access-grants-list { + display: flex; + align-items: flex-start; + gap: 20px; + + ul { + display: flex; + flex-direction: column; + height: 195px; + padding: 15px; + align-items: flex-start; + gap: 15px; + flex: 1 0 0; + min-height: 0; + box-sizing: border-box; + overflow-y: auto; + overflow-x: hidden; + border-radius: 10px; + border: 1px solid var(--solid-ui-color-gray-200, #E5E7EB); + } + + li { + display: flex; + align-items: center; + gap: 10px; + flex: 0 0 auto; + width: 100%; + + .access-grants-image { + display: flex; + justify-content: center; + align-items: center; + aspect-ratio: 1/1; + width: 30px; + height: 30px; + flex: 0 0 30px; + border-radius: 999px; + overflow: hidden; + color: var(--solid-ui-color-white, #FFF); + text-align: center; + font-size: var(--solid-ui-font-size-xs, .75rem); + font-weight: 500; + + img { + display: flex; + width: 100%; + height: 100%; + border-radius: 999px; + object-fit: cover; + } + + span { + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + line-height: 1; + } + } + + .access-grants-image--agentGroup { + background: var(--solid-ui-color-blue-900, #083575); + } + + .access-grants-image--agent, + .access-grants-image--agentClass { + background: var(--solid-ui-color-lavender-300, #e6dcff); + } + + .access-grants-image--origin { + background: var(--solid-ui-color-green-700, #15803D); + } + + .access-grants-image--unknown { + background: var(--solid-ui-color-gray-300, #D1D5DB); + } + + h3 { + flex: 1 1 auto; + min-width: 0; + color: var(--solid-ui-color-gray-800, #1E2939); + font-size: var(--solid-ui-font-size-xs, .75rem); + font-weight: 600; + } + + span { + margin-left: auto; + flex: 0 0 auto; + text-align: right; + } + + .access-grants-role { + margin-left: auto; + flex: 0 0 auto; + font-size: var(--solid-ui-font-size-xs, .75rem); + } + + .access-grants-role--owner { + font-size: var(--solid-ui-font-size-xs, .75rem); + } + + .access-grants-role--editable { + max-width: 100%; + } + } + } + + .access-role-select--compact { + width: 110px; + min-width: 110px; + flex: 0 0 110px; + max-width: 100%; + --solid-ui-input-border-color: transparent; + margin-top: 0; + } + + .access-grants-list li .access-role-select--compact { + width: 110px; + min-width: 110px; + flex: 0 0 110px; + } +} diff --git a/src/components/access-control-modal/AccessControlSharedAccess.ts b/src/components/access-control-modal/AccessControlSharedAccess.ts new file mode 100644 index 000000000..1217f0938 --- /dev/null +++ b/src/components/access-control-modal/AccessControlSharedAccess.ts @@ -0,0 +1,206 @@ +import { customElement, WebComponent } from '@/lib/components' +import { html } from 'lit' +import { property, state } from 'lit/decorators.js' +import { ACCESS_ROLES, type AccessRole } from 'solid-logic' +import { findImage } from '@/widgets' +import type { ComboboxChangeEvent } from '@/components/combobox' +import { sym } from 'rdflib' + +import '@/components/combobox' +import '@/components/combobox-option' +import '~icons/lucide/globe' + +import styles from './AccessControlSharedAccess.styles.css' +import type { AccessGrantRoleChangedDetail, AccessGrantViewModel } from './types' + +@customElement('solid-ui-access-control-shared-access') +export default class AccessControlSharedAccess extends WebComponent { + static styles = styles + + @property({ attribute: false }) + accessor accessGrantViewModels: AccessGrantViewModel[] = [] + + @property({ type: Boolean }) + accessor disabled = false + + @state() + private accessor searchValue = '' + + private onSearchInput (event: Event) { + const target = event.currentTarget as { value?: string | null } | null + this.searchValue = typeof target?.value === 'string' ? target.value : '' + } + + private onSearchSelect (event: ComboboxChangeEvent) { + const option = event.detail.option + if (typeof option.label !== 'string') { + return + } + + this.searchValue = option.label + } + + private onAccessGrantRoleChange (id: string, event: ComboboxChangeEvent) { + const value = event.detail.option.value + const accessRole = typeof value === 'string' + ? ACCESS_ROLES.find(candidate => candidate === value) + : undefined + if (!accessRole) { + return + } + + const detail: AccessGrantRoleChangedDetail = { + id, + role: accessRole + } + + this.dispatchEvent(new CustomEvent('access-grant-role-changed', { + detail, + bubbles: true, + composed: true + })) + } + + private getFilteredAccessGrants () { + const query = this.searchValue.trim().toLowerCase() + + return this.accessGrantViewModels.filter(({ subjectLabel, subjects }) => { + if (!subjects.length) { + return false + } + + return !query || subjectLabel.toLowerCase().includes(query) + }) + } + + private renderAccessGrant (entry: AccessGrantViewModel) { + return html` +
  • + ${this.renderAuthorizationBadge(entry.authorization, entry.subjectLabel)} +

    ${entry.subjectLabel}

    + ${this.renderAuthorizationRole(entry.role, entry.id)} +
  • + ` + } + + private renderAuthorizationBadge (authorization: AccessGrantViewModel['authorization'], subjectLabel: string) { + const badge = this.getAuthorizationBadge(authorization, subjectLabel) + + return html` +
    + ${badge.image + ? html`` + : badge.kind === 'origin' + ? html`` + : html``} +
    + ` + } + + private getAuthorizationBadge ( + authorization: AccessGrantViewModel['authorization'], + subjectLabel: string + ) { + const agent = authorization.agent[0] + if (agent) { + const image = findImage(sym(agent)) + return { + kind: 'agent' as const, + image, + text: image ? '' : this.getInitials(subjectLabel, 2) + } + } + + const agentGroup = authorization.agentGroup[0] + if (agentGroup) { + return { + kind: 'agentGroup' as const, + text: this.getInitials(subjectLabel, 1) + } + } + + const agentClass = authorization.agentClass[0] + if (agentClass) { + const image = findImage(sym(agentClass)) + return { + kind: 'agentClass' as const, + image, + text: image ? '' : this.getInitials(subjectLabel, 2) + } + } + + if (authorization.origin[0]) { + return { + kind: 'origin' as const, + text: 'O', + image: '' + } + } + + return { + kind: 'unknown' as const, + text: '?' + } + } + + private getInitials (value: string, maxWords = 2): string { + const parts = value.split(/\s+/).filter(Boolean).slice(0, maxWords) + const initials = parts.map(part => part[0]).join('') + return (initials || value.slice(0, maxWords)).toUpperCase() + } + + private renderAuthorizationRole (role: AccessRole, id: string) { + return html` + this.onAccessGrantRoleChange(id, event)} + > + ${this.renderGrantRoleOptions()} + + ` + } + + private renderGrantRoleOptions () { + return ACCESS_ROLES.map(role => html` + ${role === 'No Access' ? 'Remove' : role} + `) + } + + protected render () { + const accessGrants = this.getFilteredAccessGrants() + + return html` +
    +
    +

    Shared with

    + + ${this.accessGrantViewModels + .filter(({ subjects }) => subjects.length > 0) + .map(({ subjectLabel }) => html` + + ${subjectLabel} + + `)} + +
    +
    +
      + ${accessGrants.length > 0 + ? accessGrants.map(entry => this.renderAccessGrant(entry)) + : html`
    • No access grants
    • `} +
    +
    +
    + ` + } +} diff --git a/src/components/access-control-modal/accessGrantDrafts.ts b/src/components/access-control-modal/accessGrantDrafts.ts new file mode 100644 index 000000000..89d4eaa31 --- /dev/null +++ b/src/components/access-control-modal/accessGrantDrafts.ts @@ -0,0 +1,123 @@ +import { label } from '@/utils/label' +import { Authenticated, Public, solidLogicSingleton, type AccessRole, type AccessSubject } from 'solid-logic' +import { sym } from 'rdflib' +import type { AccessAuthorization, AccessGrantDraft, AccessGrantViewModel, AuthorizationSubjectSet, ChangedAccessGrant } from './types' + +export function getSharedAuthorization (authorization: AccessAuthorization): AccessAuthorization { + return { + ...authorization, + agentClass: authorization.agentClass.filter(iri => iri !== Authenticated.iri && iri !== Public.iri) + } +} + +export function getAuthorizationSubjectEntries (authorization: AccessAuthorization): AccessSubject[] { + const subjectSets: AuthorizationSubjectSet[] = [ + { type: 'agent', iris: authorization.agent }, + { type: 'agentGroup', iris: authorization.agentGroup }, + { type: 'agentClass', iris: authorization.agentClass }, + { type: 'origin', iris: authorization.origin } + ] + + return subjectSets.flatMap(({ type, iris }) => iris.map(iri => ({ type, iri }))) +} + +export function getAccessGrantId (authorization: AccessAuthorization): string { + if (authorization.id) { + return authorization.id + } + + return getAuthorizationSubjectEntries(authorization) + .map(subject => `${subject.type}:${subject.iri}`) + .sort() + .join('|') +} + +export function getAccessGrantIds (authorizations: AccessAuthorization[]): string[] { + const occurrences = new Map() + + return authorizations.map(authorization => { + const sharedAuthorization = getSharedAuthorization(authorization) + const baseId = getAccessGrantId(sharedAuthorization) + const occurrence = occurrences.get(baseId) ?? 0 + occurrences.set(baseId, occurrence + 1) + return occurrence === 0 ? baseId : `${baseId}#${occurrence}` + }) +} + +export function getAuthorizationSubjectLabel (authorization: AccessAuthorization): string { + const subjects = [ + ...authorization.agent, + ...authorization.agentGroup, + ...authorization.agentClass, + ...authorization.origin + ] + return subjects.length ? subjects.map(subject => label(sym(subject))).join(', ') : 'Unknown access holder' +} + +export function getAuthorizationRole (authorization: AccessAuthorization): AccessRole { + return solidLogicSingleton.acl.roleFromModes(authorization.mode) +} + +export function createAccessGrantDraft (authorization: AccessAuthorization): AccessGrantDraft { + const sharedAuthorization = getSharedAuthorization(authorization) + return { + id: getAccessGrantId(sharedAuthorization), + authorization: sharedAuthorization, + role: getAuthorizationRole(sharedAuthorization), + initialRole: getAuthorizationRole(authorization), + subjectLabel: getAuthorizationSubjectLabel(sharedAuthorization), + subjects: getAuthorizationSubjectEntries(sharedAuthorization) + } +} + +export function createAccessGrantDrafts (authorizations: AccessAuthorization[]): AccessGrantDraft[] { + const ids = getAccessGrantIds(authorizations) + return authorizations.map((authorization, index) => ({ + ...createAccessGrantDraft(authorization), + id: ids[index] + })) +} + +export function getAccessGrantViewModels (drafts: AccessGrantDraft[]): AccessGrantViewModel[] { + return [...drafts] + .sort((left, right) => { + const leftIsOwner = left.role === 'Owner' + const rightIsOwner = right.role === 'Owner' + + if (leftIsOwner && !rightIsOwner) return -1 + if (!leftIsOwner && rightIsOwner) return 1 + return drafts.indexOf(left) - drafts.indexOf(right) + }) +} + +export function getChangedAccessGrants (drafts: AccessGrantDraft[]): ChangedAccessGrant[] { + return getAccessGrantViewModels(drafts) + .filter(({ role, initialRole, subjects }) => role !== initialRole && subjects.length > 0) + .map(({ authorization, subjects, role }) => ({ authorization, subjects, role })) +} + +export function getAuthenticatedAccessRole (accessGrants: AccessAuthorization[]): AccessRole { + const modes = accessGrants + .filter(item => item.agentClass.includes(Authenticated.iri)) + .flatMap(item => item.mode) + return modes.length ? solidLogicSingleton.acl.roleFromModes(modes) : 'No Access' +} + +export function getPublicAccessRole (accessGrants: AccessAuthorization[]): AccessRole { + const modes = accessGrants + .filter(item => item.agentClass.includes(Public.iri)) + .flatMap(item => item.mode) + return modes.length ? solidLogicSingleton.acl.publicRoleFromModes(modes) : 'No Access' +} + +export function getAccessSubjectLabel (subject: AccessSubject): string { + if (subject.iri === Authenticated.iri) { + return 'Authenticated users' + } + + if (subject.iri === Public.iri) { + return 'Public' + } + + return label(sym(subject.iri)).trim() || subject.iri +} diff --git a/src/components/access-control-modal/accessTargetResolver.ts b/src/components/access-control-modal/accessTargetResolver.ts new file mode 100644 index 000000000..2ad4a7382 --- /dev/null +++ b/src/components/access-control-modal/accessTargetResolver.ts @@ -0,0 +1,205 @@ +import { defineAsyncComboboxOptionsProvider, type AsyncComboboxOptionsProvider, type ComboboxOptionData } from '@/components/combobox' +import { directoryEntryOptionTemplate } from './AccessControlAddAccess' +import { label } from '@/utils/label' +import { DEFAULT_DIRECTORY_SOURCES, solidLogicSingleton, type AccessControlSubjectKind, type DirectoryEntry } from 'solid-logic' +import { sym } from 'rdflib' +import { html } from 'lit' +import ns from '@/lib/ns' +import '~icons/lucide/globe' +import { dedupeByKey, dedupeComboboxOptions, isAbsoluteHttpUri, isOriginUrl, normalizeOriginInput, type StringComboboxOptionData } from './helpers' + +export type ResolvedAccessTarget = { + subjectType: AccessControlSubjectKind + subjectValue: string + label: string + targetKind?: 'origin' | 'trustedApp' +} + +type AccessTargetOption = ComboboxOptionData & { + targetKind?: 'origin' | 'trustedApp' +} + +async function classifyAccessTarget (principal: string) { + const normalizedOrigin = normalizeOriginInput(principal) + if (normalizedOrigin) { + return { kind: 'origin' as const, subjectValue: normalizedOrigin } + } + + if (isOriginUrl(principal)) { + return { kind: 'origin' as const, subjectValue: new URL(principal).origin } + } + + return await solidLogicSingleton.acl.classifyAccessControlSubject(principal) +} + +async function resolveAccessTargetLabel ( + resolvedPrincipal: Awaited>, + preferredLabel?: string +): Promise { + if (!resolvedPrincipal || resolvedPrincipal.kind === 'origin') { + return resolvedPrincipal?.subjectValue + } + + const subject = sym(resolvedPrincipal.subjectValue) + await solidLogicSingleton.store.fetcher.load(subject.doc()) + const resolvedLabel = preferredLabel?.trim() || label(subject).trim() + if (!resolvedLabel || resolvedLabel === resolvedPrincipal.subjectValue) { + throw new Error('The access target has no usable display label.') + } + + return resolvedLabel +} + +export async function resolveAccessTarget ( + principal: string, + preferredLabel?: string +): Promise { + const resolvedPrincipal = await classifyAccessTarget(principal) + if (!resolvedPrincipal) { + return undefined + } + + const resolvedLabel = await resolveAccessTargetLabel(resolvedPrincipal, preferredLabel) + if (!resolvedLabel) { + return undefined + } + + return { + subjectType: resolvedPrincipal.kind, + subjectValue: resolvedPrincipal.subjectValue, + label: resolvedLabel, + targetKind: resolvedPrincipal.kind === 'origin' ? 'origin' : undefined + } +} + +async function createUrlOption (uri: string): Promise { + const resolved = await resolveAccessTarget(uri) + return resolved + ? { label: resolved.label, value: uri } + : undefined +} + +function directoryEntryToOption (entry: DirectoryEntry, labelValue: string): ComboboxOptionData { + return { + label: labelValue, + value: entry.uri, + template: directoryEntryOptionTemplate({ ...entry, label: labelValue }) + } +} + +async function getPreferredAccessPrincipalOptions (query: string): Promise { + const normalizedOrigin = normalizeOriginInput(query) + if (normalizedOrigin) { + return [{ + label: `Use origin ${normalizedOrigin}`, + value: normalizedOrigin, + targetKind: 'origin', + template: html`Use origin ${normalizedOrigin}` + }] + } + + if (!isAbsoluteHttpUri(query)) { + return [] + } + + if (isOriginUrl(query)) { + return [{ + label: `Use origin ${new URL(query).origin}`, + value: new URL(query).origin, + targetKind: 'origin', + template: html`Use origin ${new URL(query).origin}` + }] + } + + const urlOption = await createUrlOption(query) + return dedupeComboboxOptions(urlOption ? [urlOption] : []) +} + +async function getTrustedAppOptions (query: string): Promise { + try { + const me = solidLogicSingleton.authn.currentUser() + if (!me) return [] + await solidLogicSingleton.profile.loadProfile(me) + const store = solidLogicSingleton.store + const apps = store.each(me, ns.acl('trustedApp')) as Array> + return apps.flatMap(app => { + const origin = store.any(app, ns.acl('origin')) + if (!origin || !isOriginUrl(origin.value)) return [] + const normalizedOrigin = new URL(origin.value).origin + if (query && !`${normalizedOrigin} trusted app`.toLowerCase().includes(query.toLowerCase())) return [] + return [{ + label: `Trusted app: ${normalizedOrigin}`, + value: normalizedOrigin, + targetKind: 'trustedApp' as const, + template: html`Trusted app: ${normalizedOrigin}` + }] + }) + } catch { + return [] + } +} + +function getShortQueryOptions (preferredOptions: ComboboxOptionData[]): ComboboxOptionData[] { + if (preferredOptions.length) { + return preferredOptions + } + + return [{ + label: 'Type at least 2 characters to search', + value: '', + selectable: false + }] +} + +export const accessPrincipalOptionsProvider: AsyncComboboxOptionsProvider = + defineAsyncComboboxOptionsProvider(async (filter: string) => { + const query = filter.trim() + const preferredOptions = await getPreferredAccessPrincipalOptions(query) + const trustedAppOptions = await getTrustedAppOptions(query) + + if (query.length < 2) { + return dedupeByKey( + [...preferredOptions, ...trustedAppOptions, ...getShortQueryOptions(preferredOptions)], + option => { + const targetOption = option as AccessTargetOption + return typeof targetOption.value === 'string' + ? `${targetOption.value}:${targetOption.targetKind ?? 'default'}` + : undefined + } + ) + } + + let entries: DirectoryEntry[] = [] + + try { + entries = await solidLogicSingleton.directory.search({ + query, + sources: DEFAULT_DIRECTORY_SOURCES + }) + } catch (error) { + if (!preferredOptions.length && !trustedAppOptions.length) { + throw error + } + } + + const verifiedEntries = await Promise.all(entries.map(async entry => { + try { + const resolved = await resolveAccessTarget(entry.uri, entry.label) + return resolved ? { entry, label: resolved.label } : undefined + } catch { + return undefined + } + })) + const options = verifiedEntries + .filter((entry): entry is { entry: DirectoryEntry, label: string } => entry !== undefined) + .map(({ entry, label: entryLabel }) => directoryEntryToOption(entry, entryLabel)) + return dedupeByKey( + [...preferredOptions, ...trustedAppOptions, ...options], + option => { + const targetOption = option as AccessTargetOption + return typeof targetOption.value === 'string' + ? `${targetOption.value}:${targetOption.targetKind ?? 'default'}` + : undefined + } + ) + }) diff --git a/src/components/access-control-modal/applyAccessChanges.ts b/src/components/access-control-modal/applyAccessChanges.ts new file mode 100644 index 000000000..685303ba2 --- /dev/null +++ b/src/components/access-control-modal/applyAccessChanges.ts @@ -0,0 +1,55 @@ +import { PUBLIC_ACCESS_ROLES, Public, solidLogicSingleton } from 'solid-logic' +import type { AccessChange, AccessSaveError, AccessSaveScope } from './types' + +export async function applyAccessChanges ( + subjectUri: string, + accessChanges: AccessChange[], + options: { containerScope?: AccessSaveScope } = {} +): Promise { + const errors: AccessSaveError[] = [] + const scope = options.containerScope ?? 'file' + + for (const { subject, label, role } of accessChanges) { + try { + const grantPlanner = scope === 'folder+contents' + ? solidLogicSingleton.acl.planGrantContainerAndContents + : scope === 'container' + ? solidLogicSingleton.acl.planGrantContainer + : scope === 'contents' + ? solidLogicSingleton.acl.planGrantContainerContents + : solidLogicSingleton.acl.planGrant + const plan = role === 'No Access' + ? await (scope === 'contents' + ? solidLogicSingleton.acl.planRevokeContainerContents(subjectUri, subject) + : solidLogicSingleton.acl.planRevoke(subjectUri, subject)) + : await grantPlanner(subjectUri, subject, getModes(subject, role)) + + await solidLogicSingleton.acl.applyPlan(plan) + } catch (error) { + errors.push({ + action: role === 'No Access' ? 'revoke' : 'grant', + targetUri: subject.iri, + label, + role, + scope, + message: error instanceof Error + ? error.message + : 'Unable to apply access change.' + }) + } + } + + return errors +} + +function getModes (subject: AccessChange['subject'], role: AccessChange['role']) { + if (subject.type === 'agentClass' && subject.iri === Public.iri) { + const publicRole = PUBLIC_ACCESS_ROLES.find(candidate => candidate === role) + if (!publicRole) { + throw new Error(`Public access does not support the ${role} role.`) + } + return solidLogicSingleton.acl.modesFromPublicRole(publicRole) + } + + return solidLogicSingleton.acl.modesFromRole(role) +} diff --git a/src/components/access-control-modal/buildAccessChanges.ts b/src/components/access-control-modal/buildAccessChanges.ts new file mode 100644 index 000000000..e482e9a28 --- /dev/null +++ b/src/components/access-control-modal/buildAccessChanges.ts @@ -0,0 +1,30 @@ +import { getAccessSubjectLabel } from './accessGrantDrafts' +import type { AccessChange, ChangedAccessGrant, PendingAccessGrant } from './types' + +export type AccessChangeDrafts = { + pendingAccessGrants: PendingAccessGrant[] + generalAccessChanges: AccessChange[] + changedAccessGrants: ChangedAccessGrant[] +} + +export function buildAccessChanges ({ + pendingAccessGrants, + generalAccessChanges, + changedAccessGrants +}: AccessChangeDrafts): AccessChange[] { + return [ + ...pendingAccessGrants.map(grant => ({ + subject: { type: grant.subjectType, iri: grant.subjectValue }, + role: grant.role, + label: grant.label + })), + ...generalAccessChanges, + ...changedAccessGrants.flatMap(({ subjects, role }) => + subjects.map(subject => ({ + subject, + role, + label: getAccessSubjectLabel(subject) + })) + ) + ] +} diff --git a/src/components/access-control-modal/helpers.ts b/src/components/access-control-modal/helpers.ts new file mode 100644 index 000000000..330f01758 --- /dev/null +++ b/src/components/access-control-modal/helpers.ts @@ -0,0 +1,63 @@ +import type { ComboboxOptionData } from '@/components/combobox' + +export type StringComboboxOptionData = ComboboxOptionData & { value: string } + +export function isAbsoluteHttpUri (value: string): boolean { + try { + const url = new URL(value) + return (url.protocol === 'http:' || url.protocol === 'https:') && Boolean(url.hostname) + } catch { + return false + } +} + +export function isOriginUrl (value: string): boolean { + if (!isAbsoluteHttpUri(value)) { + return false + } + + const url = new URL(value) + return url.pathname === '/' && + !url.search && + !url.hash && + !url.username && + !url.password +} + +export function normalizeOriginInput (value: string): string | undefined { + const trimmedValue = value.trim() + if (!trimmedValue || isAbsoluteHttpUri(trimmedValue)) { + return undefined + } + + try { + const parsed = new URL(`https://${trimmedValue}`) + const isHttpOrigin = parsed.protocol === 'http:' || parsed.protocol === 'https:' + const hasOnlyOriginParts = parsed.pathname === '/' && !parsed.search && !parsed.hash + const isRecognizedHost = parsed.hostname.includes('.') || + parsed.hostname.includes(':') + + return isHttpOrigin && hasOnlyOriginParts && isRecognizedHost && !parsed.username && !parsed.password + ? parsed.origin + : undefined + } catch { + return undefined + } +} + +export function dedupeByKey (items: T[], getKey: (item: T) => string | undefined): T[] { + const uniqueItems = new Map() + + for (const item of items) { + const key = getKey(item) + if (key && !uniqueItems.has(key)) { + uniqueItems.set(key, item) + } + } + + return [...uniqueItems.values()] +} + +export function dedupeComboboxOptions (options: StringComboboxOptionData[]): StringComboboxOptionData[] { + return dedupeByKey(options, option => option.value) +} diff --git a/src/components/access-control-modal/index.ts b/src/components/access-control-modal/index.ts new file mode 100644 index 000000000..8b0b7eacd --- /dev/null +++ b/src/components/access-control-modal/index.ts @@ -0,0 +1,4 @@ +import AccessControlModal from './AccessControlModal' + +export { AccessControlModal } +export default AccessControlModal diff --git a/src/components/access-control-modal/types.ts b/src/components/access-control-modal/types.ts new file mode 100644 index 000000000..def7c1835 --- /dev/null +++ b/src/components/access-control-modal/types.ts @@ -0,0 +1,103 @@ +import type { AccessControlSubjectKind, AccessMode, AccessRole, AccessSubject, Authorization } from 'solid-logic' + +export type AccessAuthorization = Omit< + Pick, + 'mode' +> & { + id?: string + mode: readonly AccessMode[] +} + +export type TargetInputDetail = { + value: string +} + +export type TargetSelectedDetail = { + value: string + label: string + targetKind?: 'origin' | 'trustedApp' +} + +export type RoleChangedDetail = { + role: AccessRole +} + +export type PendingGrantRemovedDetail = { + index: number +} + +export type TargetInputEvent = CustomEvent +export type TargetSelectedEvent = CustomEvent +export type RoleChangedEvent = CustomEvent +export type PendingGrantRemovedEvent = CustomEvent + +export type AccessGrantRoleChangedDetail = { + id: string + role: AccessRole +} + +export type AccessGrantRoleChangedEvent = CustomEvent + +export type PendingAccessGrant = { + subjectType: AccessControlSubjectKind + subjectValue: string + role: AccessRole + label: string + targetKind?: 'origin' | 'trustedApp' +} + +export type AccessGrantEntry = { + id: string + authorization: AccessAuthorization + role: AccessRole + subjectLabel: string +} + +export type AccessGrantViewModel = AccessGrantEntry & { + initialRole: AccessRole + subjects: AccessSubject[] +} + +export type AccessGrantDraft = AccessGrantViewModel + +export type ChangedAccessGrant = { + authorization: AccessAuthorization + subjects: AccessSubject[] + role: AccessRole +} + +export type AuthorizationSubjectSet = { + type: AccessControlSubjectKind + iris: string[] +} + +export type AccessChange = { + subject: AccessSubject + role: AccessRole + label: string +} + +export type AccessSaveScope = 'folder+contents' | 'container' | 'contents' | 'file' + +export type InputAccessControlError = { + action: 'input' + targetUri: string + label: string + message: string +} + +export type AccessSaveError = { + action: 'grant' | 'revoke' + targetUri: string + label: string + role: AccessRole + scope: AccessSaveScope + message: string +} | { + action: 'save' | 'reset' + targetUri: string + label: string + message: string +} + +export type AccessControlError = InputAccessControlError | AccessSaveError diff --git a/src/components/combobox/Combobox.stories.ts b/src/components/combobox/Combobox.stories.ts index 7e5a8fc16..f7049a73e 100644 --- a/src/components/combobox/Combobox.stories.ts +++ b/src/components/combobox/Combobox.stories.ts @@ -37,22 +37,25 @@ const meta = { options: 'Pizza, Ramen, Tacos', asyncJSOptions: false, asyncHtmlOptions: false, + srOnlyLabel: false, }, argTypes: { label: { control: 'text' }, options: { control: 'text' }, asyncJSOptions: { control: 'boolean' }, asyncHtmlOptions: { control: 'boolean' }, + srOnlyLabel: { control: 'boolean' }, }, - render ({ label, options, asyncJSOptions, asyncHtmlOptions }) { + render ({ label, options, asyncJSOptions, asyncHtmlOptions, srOnlyLabel }) { if (asyncJSOptions) { - return html`` + return html`` } if (asyncHtmlOptions) { return html` option.trim()) return html` - + ${parsedOptions.map((option, index) => { const indent = index === 0 ? '' : ' ' diff --git a/src/components/combobox/Combobox.styles.css b/src/components/combobox/Combobox.styles.css index 2308ce043..1efdc1897 100644 --- a/src/components/combobox/Combobox.styles.css +++ b/src/components/combobox/Combobox.styles.css @@ -1,4 +1,6 @@ :host { + --solid-ui-input-border-color: var(--solid-ui-color-gray-400); + display: inline-flex; flex-direction: column; align-items: flex-start; @@ -29,11 +31,19 @@ width: 100%; input { - border: 1px solid var(--solid-ui-color-gray-400); + border: 1px solid var(--solid-ui-input-border-color); border-radius: 5px; padding: 10px; padding-right: 40px; width: 100%; + + /* Global focus styles don't reach into the shadow DOM, so mirror them here. */ + &:focus-visible { + outline: var(--focus-indicator-width, 2px) solid var(--color-primary, var(--solid-ui-color-primary, #7C4DFF)); + outline-offset: 2px; + box-shadow: 0 0 0 1px var(--color-background, #F8F9FB), 0 0 0 4px rgba(124, 77, 255, 0.2); + transition: none; + } } icon-lucide-chevron-down { diff --git a/src/components/combobox/Combobox.ts b/src/components/combobox/Combobox.ts index e4ea5593c..3793e4bdc 100644 --- a/src/components/combobox/Combobox.ts +++ b/src/components/combobox/Combobox.ts @@ -154,10 +154,12 @@ export default class Combobox extends FormControlComponent { aria-required=${this.required ? 'true' : nothing} autocomplete="off" spellcheck="false" + ?disabled=${this.disabled} ?required=${this.required} .value=${this.displayValue} @keydown=${this.onInputKeyDown} @focus=${this.onInputFocus} + @click=${this.onInputClick} @input=${() => this.selectOnly ? this.updateDisplayValue(this.controlElement?.value ?? '') : this.controlTrait.onInput()} /> @@ -445,6 +447,10 @@ export default class Combobox extends FormControlComponent { this.show() } + private onInputClick () { + this.show() + } + private openPopupFromKey (focusLast: boolean) { if (!this.open) { this.show({ focusLast }) diff --git a/src/components/error-state/ErrorState.stories.ts b/src/components/error-state/ErrorState.stories.ts new file mode 100644 index 000000000..e58b86a68 --- /dev/null +++ b/src/components/error-state/ErrorState.stories.ts @@ -0,0 +1,64 @@ +import { html, nothing } from 'lit' + +import './ErrorState' + +const args = { + title: 'Something went wrong', + message: 'We could not load this content. Please try again.', + details: '', + linkText: '', + linkHref: '' +} as const + +const meta = { + title: 'Basic UI/Error State', + args, + argTypes: { + title: { control: 'text' }, + message: { control: 'text' }, + details: { control: 'text' }, + linkText: { control: 'text' }, + linkHref: { control: 'text' } + }, + render ({ title, message, details, linkText, linkHref }: typeof args) { + return html` + + ${details ? html`

    ${details}

    ` : nothing} + ${linkText && linkHref + ? html` + { window.location.href = linkHref }} + > + ${linkText} + + ` + : nothing} +
    + ` + } +} as const + +export const Primary = {} + +export const Detailed = { + args: { + title: 'Some people could not be added', + message: 'Your existing sharing settings were not changed. Review these WebIDs, then try saving again.', + details: 'The profile could not be found or is not publicly readable.' + } +} + +export const WithRecoveryLink = { + args: { + title: 'This page is unavailable', + message: 'The requested resource could not be loaded.', + linkText: 'Open help', + linkHref: 'https://solidproject.org/' + } +} + +export default meta diff --git a/src/components/error-state/ErrorState.styles.css b/src/components/error-state/ErrorState.styles.css new file mode 100644 index 000000000..04a60d642 --- /dev/null +++ b/src/components/error-state/ErrorState.styles.css @@ -0,0 +1,79 @@ +:host { + display: block; + color: var(--solid-ui-color-gray-800, #334155); +} + +section { + box-sizing: border-box; + position: relative; + width: 100%; + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px 18px; + border: 1px solid var(--solid-ui-color-pink-200, #f0d8dc); + border-radius: 10px; + background: var(--solid-ui-color-pink-50, #fff7f8); + box-shadow: 0 1px 2px rgb(15 23 42 / 8%); +} + +.close-button { + --solid-ui-font-size-2xl: var(--solid-ui-font-size-md, 1rem); + + position: absolute; + top: 10px; + right: 10px; + color: var(--solid-ui-color-gray-500); +} + +.icon { + display: grid; + flex: 0 0 28px; + height: 28px; + place-items: center; + border-radius: 7px; + color: var(--solid-ui-color-red-500, #fb2c36); + background: var(--solid-ui-color-pink-100, #ffe7eb); +} + +icon-lucide-triangle-alert { + width: 16px; + height: 16px; +} + +.content { + min-width: 0; + flex: 1; + max-width: 100%; + padding-right: 28px; +} + +h2 { + margin: 1px 0 4px; + color: var(--solid-ui-color-red-700, #b00020); + font-size: var(--solid-ui-font-size-lg, 1.125rem); + font-weight: 600; + line-height: 1.3; +} + +p { + width: 100%; + max-width: 70ch; + margin: 0 0 8px; + color: var(--solid-ui-color-gray-600, #64748b); + font-size: var(--solid-ui-font-size-sm, 0.875rem); + line-height: 1.5; + overflow-wrap: anywhere; +} + +slot:not(:empty) { + display: block; + margin-top: 8px; +} + +::slotted(*) { + max-width: 100%; + color: var(--solid-ui-error-state-slot-color, var(--solid-ui-color-gray-600, #64748b)); + font-size: var(--solid-ui-error-state-slot-font-size, var(--solid-ui-font-size-xs, 0.75rem)); + line-height: 1.5; +} diff --git a/src/components/error-state/ErrorState.ts b/src/components/error-state/ErrorState.ts new file mode 100644 index 000000000..7da030747 --- /dev/null +++ b/src/components/error-state/ErrorState.ts @@ -0,0 +1,46 @@ +import { customElement, WebComponent } from '@/lib/components' +import { html, nothing } from 'lit' +import { property } from 'lit/decorators.js' + +import '@/components/button' +import '~icons/lucide/triangle-alert' +import '~icons/lucide/x' + +import styles from './ErrorState.styles.css' + +@customElement('solid-ui-error-state') +export default class ErrorState extends WebComponent { + static styles = styles + + @property({ type: String }) + accessor title = '' + + @property({ type: String }) + accessor message = '' + + render () { + return html` +
    + + + Close + +
    + +
    +
    + ${this.title ? html`

    ${this.title}

    ` : nothing} + ${this.message ? html`

    ${this.message}

    ` : nothing} + +
    +
    + ` + } + + private onClose () { + this.dispatchEvent(new CustomEvent('close', { + bubbles: true, + composed: true + })) + } +} diff --git a/src/components/error-state/index.ts b/src/components/error-state/index.ts new file mode 100644 index 000000000..d32ed35ab --- /dev/null +++ b/src/components/error-state/index.ts @@ -0,0 +1 @@ +export { default } from './ErrorState' diff --git a/src/components/file-explorer-header/FileExplorerHeader.styles.css b/src/components/file-explorer-header/FileExplorerHeader.styles.css new file mode 100644 index 000000000..5902ad65d --- /dev/null +++ b/src/components/file-explorer-header/FileExplorerHeader.styles.css @@ -0,0 +1,14 @@ +:host { + header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 6px; + flex-wrap: nowrap; + box-sizing: border-box; + width: 100%; + text-align: right; + padding: 10px; + border-bottom: 1px solid var(--solid-ui-color-gray-200, #cbd5e1); + } +} diff --git a/src/components/file-explorer-header/FileExplorerHeader.ts b/src/components/file-explorer-header/FileExplorerHeader.ts new file mode 100644 index 000000000..8b0a322c2 --- /dev/null +++ b/src/components/file-explorer-header/FileExplorerHeader.ts @@ -0,0 +1,113 @@ +import { sym } from 'rdflib' +import { solidLogicSingleton } from 'solid-logic' +import { customElement, WebComponent } from '@/lib/components' +import { DEFAULT_FILE_EXPLORER_CONTEXT, fileExplorerContext } from '@/lib/file-explorer/context' +import { FILE_EXPLORER_BACK_EVENT } from '@/lib/file-explorer/events' +import { property, state } from 'lit/decorators.js' +import { consume } from '@lit/context' +import { html } from 'lit' +import '~icons/lucide/share-2' +import '~icons/lucide/pencil' +import '~icons/lucide/ellipsis-vertical' +import styles from './FileExplorerHeader.styles.css' +import './FileExplorerHeaderSummary' +import './FileExplorerHeaderControls' +import { PaneIcon } from './types' +import { type FileExplorerResourceMetadata } from './types' + +@customElement('solid-ui-file-explorer-header') +export default class FileExplorerHeader extends WebComponent { + static styles = styles + + private _loadedMetadataForUri: string | undefined + private _metadataGeneration = 0 + + @consume({ context: fileExplorerContext, subscribe: true }) + accessor fileExplorerContext = DEFAULT_FILE_EXPLORER_CONTEXT + + @property({ attribute: false }) + accessor menuItems: Array<{ label: string, action: (event: Event) => void }> = [] + + @property({ attribute: false }) + accessor paneIcon: PaneIcon = undefined as unknown as PaneIcon + + @property({ type: Boolean }) + accessor showBack = true + + @state() + accessor responseMetadata: Pick = { + modified: undefined, + isPublic: false, + canEdit: false, + canManageAccess: false, + aclUri: undefined + } + + private handleBack = () => { + this.dispatchEvent(new CustomEvent(FILE_EXPLORER_BACK_EVENT, { + bubbles: true, + composed: true, + detail: { + subjectUri: this.fileExplorerContext.subjectUri + } + })) + } + + private getDefaultResponseMetadata (): Pick { + return { + modified: undefined, + isPublic: false, + canEdit: false, + canManageAccess: false, + aclUri: undefined + } + } + + protected willUpdate () { + const subjectUri = this.fileExplorerContext?.subjectUri + if (this.fileExplorerContext?.store && subjectUri && this._loadedMetadataForUri !== subjectUri) { + this._loadedMetadataForUri = subjectUri + const generation = ++this._metadataGeneration + this.responseMetadata = this.getDefaultResponseMetadata() + this.loadResponseMetadata(subjectUri, generation) + } + } + + private async loadResponseMetadata (subjectUri: string, generation: number) { + try { + const metadata = await solidLogicSingleton.resource.fetchMetadata(sym(subjectUri)) + if (generation === this._metadataGeneration && this.fileExplorerContext.subjectUri === subjectUri) { + this.responseMetadata = { + modified: metadata.modified, + isPublic: metadata.access.isPublic, + canEdit: metadata.access.canEdit, + canManageAccess: metadata.access.canManageAccess, + aclUri: metadata.aclUri + } + } + } catch (error) { + if (generation === this._metadataGeneration && this.fileExplorerContext.subjectUri === subjectUri) { + this.responseMetadata = this.getDefaultResponseMetadata() + console.warn('Failed to load response metadata', error) + } + } + } + + render () { + return html` +
    + + +
    + ` + } +} diff --git a/src/components/file-explorer-header/FileExplorerHeaderControls.styles.css b/src/components/file-explorer-header/FileExplorerHeaderControls.styles.css new file mode 100644 index 000000000..dc411a3ca --- /dev/null +++ b/src/components/file-explorer-header/FileExplorerHeaderControls.styles.css @@ -0,0 +1,69 @@ +:host { + flex: 0 1 auto; + min-width: 0; + + div { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 6px; + flex: 0 0 auto; + text-align: right; + } + + /* generative ai */ + div > * { + flex: 0 1 auto; + } + + .file-explorer-header-action-button { + width: var(--solid-ui-clickable-area, 42px); + height: var(--solid-ui-clickable-area, 42px); + padding: 0; + } + + /* TODO: look into what colors to use this is just to show */ + .dirtyIndicator { + display: inline-flex; + align-items: center; + border: 1px solid #F59E0B; + color: #92400E; + background: #FFFBEB; + border-radius: 999px; + font-size: 0.72rem; + font-weight: 600; + line-height: 1; + padding: 4px 8px; + white-space: nowrap; + } + + icon-lucide-user-round-cog, + icon-lucide-pencil { + width: 16px; + height: 16px; + } + + @media (max-width: 900px) { + div { + padding-right: 3px; + } + } + + @media (max-width: 600px) { + div { + justify-content: flex-start; + gap: 5px; + } + + .file-explorer-header-action-button { + display: none; + } + + icon-lucide-ellipsis-vertical { + width: 15.36px; + height: 15.36px; + flex-shrink: 0; + aspect-ratio: 1 / 1; + } + } +} diff --git a/src/components/file-explorer-header/FileExplorerHeaderControls.ts b/src/components/file-explorer-header/FileExplorerHeaderControls.ts new file mode 100644 index 000000000..6fa2e39f8 --- /dev/null +++ b/src/components/file-explorer-header/FileExplorerHeaderControls.ts @@ -0,0 +1,134 @@ +import { customElement, WebComponent } from '@/lib/components' +import { property, state } from 'lit/decorators.js' +import { consume } from '@lit/context' +import { html, nothing } from 'lit' +import '@/components/button' +import '~icons/lucide/user-round-cog' +import '~icons/lucide/pencil' +import styles from './FileExplorerHeaderControls.styles.css' +import '@/components/resource-actions-menu/ResourceActionsMenu' +import { DEFAULT_FILE_EXPLORER_CONTEXT, fileExplorerContext } from '@/lib/file-explorer/context' +import { FILE_EXPLORER_OPEN_PANE_EVENT } from '@/lib/file-explorer/events' +import { isContainerSubject } from '@/utils/podUtils' + +@customElement('solid-ui-file-explorer-header-controls') +export default class FileExplorerHeaderControls extends WebComponent { + static styles = styles + + private mobileMediaQuery: MediaQueryList | undefined + private readonly mobileQuery = '(max-width: 600px)' + private readonly handleMobileMediaChange = (event: MediaQueryListEvent) => { + this.isMobile = event.matches + } + + @consume({ context: fileExplorerContext, subscribe: true }) + accessor fileExplorerContext = DEFAULT_FILE_EXPLORER_CONTEXT + + @property({ attribute: false }) + accessor menuItems: Array<{ label: string, action: (event: Event) => void, icon?: HTMLElement }> = [] + + @property({ type: Boolean }) + accessor canEdit: boolean = false + + @property({ type: Boolean }) + accessor canManageAccess = false + + @state() + accessor isMobile = typeof window !== 'undefined' && typeof window.matchMedia === 'function' + ? window.matchMedia('(max-width: 600px)').matches + : false + + connectedCallback () { + super.connectedCallback() + + if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { + return + } + + this.mobileMediaQuery = window.matchMedia(this.mobileQuery) + this.isMobile = this.mobileMediaQuery.matches + this.mobileMediaQuery.addEventListener('change', this.handleMobileMediaChange) + } + + disconnectedCallback () { + this.mobileMediaQuery?.removeEventListener('change', this.handleMobileMediaChange) + this.mobileMediaQuery = undefined + super.disconnectedCallback() + } + + // TODO: Add broken then use this function to set tooltip and disable edit button + /* private setEditable() { + const sourcePaneState = this.sourceContext?.sourcePaneState + const { canEdit } = this.sourceContext?.headerMetadata ?? { canEdit: false } + const subject = this.sourceContext?.subject + if (!sourcePaneState || !canEdit || sourcePaneState.broken || (subject && subject.endsWith('/'))) return + + this.sourceContext?.setEditing?.() + } */ + + private getEditTooltip () { + if (!this.canEdit) return 'No Access' + return 'Edit' + } + + private handleManageAccess = () => { + const subjectUri = this.fileExplorerContext.subjectUri + if (!subjectUri) return + + this.dispatchEvent(new CustomEvent(FILE_EXPLORER_OPEN_PANE_EVENT, { + bubbles: true, + composed: true, + detail: { subjectUri, paneName: 'sharing' } + })) + } + + private renderDirtyIndicator () { + if (!this.fileExplorerContext.isDirty) return nothing + + return html`Unsaved` + } + + render () { + const isContainerResource = isContainerSubject(this.fileExplorerContext.store, this.fileExplorerContext.subjectUri) + const supportsEditing = this.fileExplorerContext.canEdit + + return html` +
    + ${this.renderDirtyIndicator()} + ${!isContainerResource && !this.isMobile && this.canManageAccess + ? html` + + + Manage access + + ${supportsEditing + ? html` + + + Edit + + ` + : nothing} + ` + : nothing} + +
    + ` + } +} diff --git a/src/components/file-explorer-header/FileExplorerHeaderSummary.styles.css b/src/components/file-explorer-header/FileExplorerHeaderSummary.styles.css new file mode 100644 index 000000000..db9952b73 --- /dev/null +++ b/src/components/file-explorer-header/FileExplorerHeaderSummary.styles.css @@ -0,0 +1,148 @@ + :host { + .file-explorer-header-summary { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 6px; + flex-wrap: wrap; + flex: 1 1 auto; + min-width: 0; + text-align: left; + } + + h1 { + color: var(--solid-ui-color-gray-700, #364153); + font-size: var(--solid-ui-font-size-xl, 1.25rem); /* while in expand; will go to 2xl when new design is complete */ + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; + margin: 0; + } + + .resource-info { + display: flex; + flex-direction: column; + min-width: 0; + } + + .container-info { + display: flex; + flex-direction: row; + gap: 10px; + min-width: 0; + } + + .pane-icon { + display: flex; + width: 40px; + height: 40px; + padding: 7.5px 7px 6.5px 7px; + justify-content: center; + align-items: center; + aspect-ratio: 1/1; + /* border-radius: 100px; */ + /* background: var(--slate-600, #45556C); */ + } + + .pane-icon img { + width: 100%; + height: 100%; + object-fit: contain; + display: block; + } + + p { + display: flex; + align-items: center; + gap: 5px; + color: var(--solid-ui-gray-500, #6A7282); + font-size: var(--solid-ui-font-size-xs, .75rem); + font-weight: 500; + } + + .public { + display: flex; + padding: 2px 5px; + justify-content: center; + align-items: center; + gap: 3px; + color: var(--blue-400, #51A2FF); + border-radius: 5px; + background: var(--blue-50, #EFF6FF); + } + + icon-lucide-globe { + stroke-width: 1.021px; + stroke: var(--blue-400, #51A2FF); + } + + .private { + display: flex; + padding: 2px 5px; + justify-content: center; + align-items: center; + gap: 3px; + color: var(--yellow-600, #D08700); + border-radius: 5px; + background: var(--yellow-100, #FEF9C2); + } + + icon-lucide-lock-keyhole { + stroke-width: 1.021px; + stroke: var(--yellow-600, #D08700); + } + + icon-lucide-globe, + icon-lucide-lock-keyhole { + width: 0.875rem; + height: 0.875rem; + } + + .resource-date { + font-size: inherit; + } + + icon-lucide-arrow-left { + width: 1.125rem; + height: 1.125rem; + } + + @media (max-width: 600px) { + .file-explorer-header-summary { + gap: 7px; + } + + h1 { + font-size: 14px; + } + + .pane-icon { + width: 18.2px; + height: 18.2px; + flex-shrink: 0; + aspect-ratio: 1 / 1; + padding: 0; + } + + p { + font-size: 14px; + } + + .resource-date { + font-size: 10px; + } + + .public, + .private { + font-size: 9px; + } + + icon-lucide-arrow-left { + width: 15px; + height: 18px; + flex-shrink: 0; + aspect-ratio: 5 / 6; + } + } +} diff --git a/src/components/file-explorer-header/FileExplorerHeaderSummary.ts b/src/components/file-explorer-header/FileExplorerHeaderSummary.ts new file mode 100644 index 000000000..4903178f3 --- /dev/null +++ b/src/components/file-explorer-header/FileExplorerHeaderSummary.ts @@ -0,0 +1,167 @@ +import { sym } from 'rdflib' +import { customElement, WebComponent } from '@/lib/components' +import * as utils from '@/utils' +import * as widgets from '@/widgets/index' +import { DEFAULT_FILE_EXPLORER_CONTEXT, fileExplorerContext } from '@/lib/file-explorer/context' +import '@/components/button' +import { property, query, state } from 'lit/decorators.js' +import type { PropertyValues } from 'lit' +import { html } from 'lit' +import { consume } from '@lit/context' +import { PaneIcon } from './types' +import '~icons/lucide/globe' +import '~icons/lucide/lock-keyhole' +import '~icons/lucide/arrow-left' +import '~icons/lucide/folder' +import styles from './FileExplorerHeaderSummary.styles.css' +import { getContainerItemCount, isContainerSubject } from '@/utils/podUtils' +import type { FileExplorerResourceMetadata } from './types' + +@customElement('solid-ui-file-explorer-header-summary') +export default class FileExplorerHeaderSummary extends WebComponent { + static styles = styles + + private _draggableSubjectUri: string | undefined + private _paneIconGeneration = 0 + + @consume({ context: fileExplorerContext, subscribe: true }) + accessor fileExplorerContext = DEFAULT_FILE_EXPLORER_CONTEXT + + @property({ attribute: false }) + accessor paneIcon: PaneIcon | undefined + + @property({ attribute: false }) + accessor onBackClick: (() => void) | undefined + + @property({ type: Boolean }) + accessor showBack = true + + @property({ attribute: false }) + accessor responseMetadata: Pick = { + modified: undefined, + isPublic: false + } + + @state() + accessor resolvedPaneIcon: string | undefined = undefined + + @query('h1') + private accessor titleHeading: HTMLHeadingElement | null = null + + private formatModifiedDate (modified: string | undefined) { + if (!modified) return '' + + const date = new Date(modified) + if (Number.isNaN(date.getTime())) return modified + + const parts = new Intl.DateTimeFormat('en-GB', { + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: true + }).formatToParts(date) + + const getPart = (type: Intl.DateTimeFormatPartTypes) => parts.find(part => part.type === type)?.value ?? '' + const day = getPart('day') + const month = getPart('month') + const year = getPart('year') + const hour = getPart('hour') + const minute = getPart('minute') + const dayPeriod = getPart('dayPeriod').toUpperCase() + + return `${day} ${month}, ${year} at ${hour}:${minute} ${dayPeriod}` + } + + protected willUpdate (changedProperties: PropertyValues) { + if (changedProperties.has('paneIcon')) { + const generation = ++this._paneIconGeneration + this.resolvedPaneIcon = undefined + this.resolvePaneIcon(this.paneIcon, generation) + } + } + + protected updated () { + if (!this.titleHeading || !this.fileExplorerContext?.subjectUri || this._draggableSubjectUri === this.fileExplorerContext.subjectUri) return + + widgets.makeDraggable(this.titleHeading, sym(this.fileExplorerContext.subjectUri)) // doing it this way for now, just to keep the same functionality + this._draggableSubjectUri = this.fileExplorerContext.subjectUri + } + + // Needed because humanReadable pane can return a promise for an icon. + private async resolvePaneIcon (paneIcon: PaneIcon | undefined, generation: number) { + try { + if (paneIcon == null) return + + const icon = await paneIcon + if (generation === this._paneIconGeneration) { + this.resolvedPaneIcon = icon ?? undefined + } + } catch (error) { + if (generation === this._paneIconGeneration) { + this.resolvedPaneIcon = undefined + console.warn('file-explorer-header-summary: failed to resolve pane icon', error) + } + } + } + + private renderContainerResourceHeader (label: string, isPublic: boolean) { + const itemCount = getContainerItemCount(this.fileExplorerContext?.store, this.fileExplorerContext?.subjectUri) ?? 0 + return html` +
    +

    + ${label} +

    +

    + ${itemCount} items + ${isPublic + ? html`Public` + : html`Private`} +

    +
    + ` + } + + private renderResourceHeader (label: string, isPublic: boolean) { + const modified = this.formatModifiedDate(this.responseMetadata.modified) + + return html` +
    +

    + ${label} +

    +

    ${modified} ${isPublic ? html` Public` : html` Private`}

    +
    + ` + } + + render () { + const subject = this.fileExplorerContext?.subjectUri ? sym(this.fileExplorerContext.subjectUri) : undefined + const label = subject ? utils.label(subject) : '' + const isPublic = this.responseMetadata.isPublic + const isContainerResource = isContainerSubject(this.fileExplorerContext?.store, this.fileExplorerContext?.subjectUri) + + return html` +
    + ${this.showBack + ? html` + + + Back + + ` + : ''} + + ${this.resolvedPaneIcon ? html`` : ''} + + ${isContainerResource ? this.renderContainerResourceHeader(label, isPublic) : this.renderResourceHeader(label, isPublic)} +
    + + ` + } +} diff --git a/src/components/file-explorer-header/index.ts b/src/components/file-explorer-header/index.ts new file mode 100644 index 000000000..ad71f8812 --- /dev/null +++ b/src/components/file-explorer-header/index.ts @@ -0,0 +1,4 @@ +import FileExplorerHeader from './FileExplorerHeader' + +export { FileExplorerHeader } +export default FileExplorerHeader diff --git a/src/components/file-explorer-header/types.ts b/src/components/file-explorer-header/types.ts new file mode 100644 index 000000000..01305d80b --- /dev/null +++ b/src/components/file-explorer-header/types.ts @@ -0,0 +1,11 @@ +export type PaneIcon = string | Promise | null | undefined + +export type FileExplorerResourceMetadata = { + contentType: string | undefined + canEdit: boolean + canManageAccess: boolean + isPublic: boolean + aclUri: string | undefined + eTag: string | undefined + modified: string | undefined +} diff --git a/src/components/file-explorer-provider/FileExplorerProvider.ts b/src/components/file-explorer-provider/FileExplorerProvider.ts new file mode 100644 index 000000000..3a15a6a83 --- /dev/null +++ b/src/components/file-explorer-provider/FileExplorerProvider.ts @@ -0,0 +1,52 @@ +import { html } from 'lit-html' +import { provide } from '@lit/context' +import { customElement, WebComponent } from '@/lib/components' +import { property, state } from 'lit/decorators.js' +import { DataBrowserContext, PaneDefinition } from 'pane-registry' +import { FileExplorerContext, FileExplorerEditor, fileExplorerContext } from '@/lib/file-explorer/context' + +@customElement('solid-ui-file-explorer-provider') +export default class FileExplorerProvider extends WebComponent { + @property({ attribute: false }) + accessor context: DataBrowserContext | undefined + + @property({ attribute: false }) + accessor subjectUri: string | undefined + + @property({ attribute: false }) + accessor pane: PaneDefinition | undefined + + @property({ attribute: false }) + accessor relevantPanes: PaneDefinition[] = [] + + @property({ attribute: false }) + accessor soloPane: boolean | undefined + + @state() private accessor editor: FileExplorerEditor | undefined + @state() private accessor isDirty = false + + @provide({ context: fileExplorerContext }) + accessor value!: FileExplorerContext + + private registerEditor = (editor: FileExplorerEditor) => { + this.editor = editor + return () => { if (this.editor === editor) this.editor = undefined } + } + + protected willUpdate () { + this.value = { + store: this.context?.session.store, + subjectUri: this.subjectUri, + pane: this.pane, + isDirty: this.isDirty, + canEdit: !!this.editor, + registerEditor: this.registerEditor, + beginEditing: () => { this.editor?.beginEditing() }, + setDirty: (dirty) => { this.isDirty = dirty } + } + } + + render () { + return html`` + } +} diff --git a/src/components/file-explorer-provider/index.ts b/src/components/file-explorer-provider/index.ts new file mode 100644 index 000000000..10e75175e --- /dev/null +++ b/src/components/file-explorer-provider/index.ts @@ -0,0 +1,4 @@ +import FileExplorerProvider from './FileExplorerProvider' + +export { FileExplorerProvider } +export default FileExplorerProvider diff --git a/src/components/index.ts b/src/components/index.ts index 916e49f9f..e47751a43 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,4 +1,5 @@ // Make sure that is registered first, so that contexts // are available to nested components when they mount. import './provider' +import './file-explorer-provider' import './all' diff --git a/src/components/input/Input.stories.ts b/src/components/input/Input.stories.ts index d07128e39..1ec4149a2 100644 --- a/src/components/input/Input.stories.ts +++ b/src/components/input/Input.stories.ts @@ -7,6 +7,7 @@ const args = { value: '', placeholder: 'Enter your name', type: 'text', + srOnlyLabel: false, } const meta = { @@ -16,15 +17,17 @@ const meta = { label: { control: 'text' }, value: { control: 'text' }, placeholder: { control: 'text' }, + srOnlyLabel: { control: 'boolean' }, type: { control: 'select', options: ['text', 'email', 'password', 'search', 'url'], }, }, - render ({ label, value, placeholder, type }: typeof args) { + render ({ label, value, placeholder, type, srOnlyLabel }: typeof args) { return html` + 0) + } + private onKeyDown (e: KeyboardEvent) { if (e.key === 'Enter') { e.preventDefault() diff --git a/src/components/menu-item/MenuItem.styles.css b/src/components/menu-item/MenuItem.styles.css index 594b8b28e..8d72ecd5d 100644 --- a/src/components/menu-item/MenuItem.styles.css +++ b/src/components/menu-item/MenuItem.styles.css @@ -19,8 +19,9 @@ } ::slotted([slot="left-icon"]), ::slotted([slot="right-icon"]) { - width: var(--solid-ui-font-size-xl); - height: var(--solid-ui-font-size-xl); + width: var(--solid-ui-font-size-xl) !important; + height: var(--solid-ui-font-size-xl) !important; + object-fit: contain; } a::after { diff --git a/src/components/resource-actions-menu/DeleteResourceDialog.ts b/src/components/resource-actions-menu/DeleteResourceDialog.ts new file mode 100644 index 000000000..1b5c427b1 --- /dev/null +++ b/src/components/resource-actions-menu/DeleteResourceDialog.ts @@ -0,0 +1,36 @@ +import { customElement, DialogComponent } from '@/lib/components' +import { html } from 'lit' +import { property } from 'lit/decorators.js' + +import '@/components/button' +import '@/components/dialog' +import '@/components/dialog-content' +import '@/components/dialog-footer' + +@customElement('resource-delete-dialog') +export default class DeleteResourceDialog extends DialogComponent { + @property({ type: String }) + accessor resourceName = 'this resource' + + private cancel = () => this.close(false) + + private confirm = () => this.close(true) + + protected render () { + return html` + + +

    Are you sure you want to permanently delete ${this.resourceName}?

    +
    + + + Go back + + + Yes + + +
    + ` + } +} diff --git a/src/components/resource-actions-menu/ResourceActionsMenu.styles.css b/src/components/resource-actions-menu/ResourceActionsMenu.styles.css new file mode 100644 index 000000000..9b9e7b4d1 --- /dev/null +++ b/src/components/resource-actions-menu/ResourceActionsMenu.styles.css @@ -0,0 +1,12 @@ +:host { + .file-explorer-header-more-button { + width: var(--solid-ui-clickable-area, 42px); + height: var(--solid-ui-clickable-area, 42px); + padding: 0; + } + + .ellipsisIcon { + width: 16px; + height: 16px; + } +} diff --git a/src/components/resource-actions-menu/ResourceActionsMenu.test.ts b/src/components/resource-actions-menu/ResourceActionsMenu.test.ts new file mode 100644 index 000000000..8523fb7d8 --- /dev/null +++ b/src/components/resource-actions-menu/ResourceActionsMenu.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it, vi } from 'vitest' +import './ResourceActionsMenu.ts' + +describe('solid-ui-resource-actions-menu', () => { + it('does not duplicate the desktop file header access action in the menu', async () => { + const menu = document.createElement('solid-ui-resource-actions-menu') as HTMLElement & { + handleAccessClick?: () => void + canManageAccess?: boolean + subjectUri?: string + updateComplete: Promise + } + const handleAccessClick = vi.fn() + menu.subjectUri = 'https://example.test/resource.ttl' + menu.handleAccessClick = handleAccessClick + menu.canManageAccess = true + + document.body.appendChild(menu) + await menu.updateComplete + + const accessItem = Array.from(menu.shadowRoot?.querySelectorAll('solid-ui-menu-item') || []) + .find(item => item.textContent?.includes('Manage Access')) + expect(accessItem).toBeUndefined() + + menu.remove() + }) + + it('does not render Manage Access without the capability', async () => { + const menu = document.createElement('solid-ui-resource-actions-menu') as HTMLElement & { + handleAccessClick?: () => void + subjectUri?: string + showAccessInMenu?: boolean + updateComplete: Promise + } + menu.subjectUri = 'https://example.test/resource.ttl' + menu.handleAccessClick = vi.fn() + menu.showAccessInMenu = true + + document.body.appendChild(menu) + await menu.updateComplete + + expect(menu.shadowRoot?.textContent).not.toContain('Manage Access') + menu.remove() + }) + + it('shows Manage Access when the containing view explicitly enables it', async () => { + const menu = document.createElement('solid-ui-resource-actions-menu') as HTMLElement & { + handleAccessClick?: () => void + subjectUri?: string + showAccessInMenu?: boolean + canManageAccess?: boolean + updateComplete: Promise + } + const handleAccessClick = vi.fn() + menu.subjectUri = 'https://example.test/resource.ttl' + menu.handleAccessClick = handleAccessClick + menu.showAccessInMenu = true + menu.canManageAccess = true + + document.body.appendChild(menu) + await menu.updateComplete + + const accessItem = Array.from(menu.shadowRoot?.querySelectorAll('solid-ui-menu-item') || []) + .find(item => item.textContent?.includes('Manage Access')) + expect(accessItem).toBeTruthy() + + accessItem?.dispatchEvent(new CustomEvent('solid-ui-select', { bubbles: true, composed: true })) + expect(handleAccessClick).toHaveBeenCalledOnce() + + menu.remove() + }) + + it('opens the resource URI, including its fragment, in a new tab', async () => { + const menu = document.createElement('solid-ui-resource-actions-menu') as HTMLElement & { + subjectUri?: string + updateComplete: Promise + } + const subjectUri = 'https://example.test/resource.ttl#subject' + const open = vi.spyOn(window, 'open').mockReturnValue(null) + menu.subjectUri = subjectUri + + document.body.appendChild(menu) + await menu.updateComplete + + const item = Array.from(menu.shadowRoot?.querySelectorAll('solid-ui-menu-item') || []) + .find(item => item.textContent?.includes('Open in New Tab')) + expect(item).toBeTruthy() + + item?.dispatchEvent(new CustomEvent('solid-ui-select', { bubbles: true, composed: true })) + expect(open).toHaveBeenCalledOnce() + expect(open).toHaveBeenCalledWith(subjectUri, '_blank', 'noopener,noreferrer') + + open.mockRestore() + menu.remove() + }) + + it('renders supplied pane actions and dispatches their event', async () => { + const menu = document.createElement('solid-ui-resource-actions-menu') as HTMLElement & { + subjectUri?: string + menuItems?: Array<{ label: string, action: (event: Event) => void }> + updateComplete: Promise + } + const action = vi.fn() + menu.subjectUri = 'https://example.test/resource.ttl' + menu.menuItems = [{ label: 'View as Source', action }] + + document.body.appendChild(menu) + await menu.updateComplete + + const item = Array.from(menu.shadowRoot?.querySelectorAll('solid-ui-menu-item') || []) + .find(candidate => candidate.textContent?.includes('View as Source')) + expect(item).toBeTruthy() + + const event = new CustomEvent('solid-ui-select', { bubbles: true, composed: true }) + item?.dispatchEvent(event) + expect(action).toHaveBeenCalledOnce() + expect(action).toHaveBeenCalledWith(event) + + menu.remove() + }) +}) diff --git a/src/components/resource-actions-menu/ResourceActionsMenu.ts b/src/components/resource-actions-menu/ResourceActionsMenu.ts new file mode 100644 index 000000000..d024cf834 --- /dev/null +++ b/src/components/resource-actions-menu/ResourceActionsMenu.ts @@ -0,0 +1,112 @@ +import { customElement, WebComponent } from '@/lib/components' +import { property } from 'lit/decorators.js' +import { html, nothing } from 'lit' +import '@/components/button' +import '@/components/menu' +import '@/components/menu-item' +import '~icons/lucide/ellipsis-vertical' +import '~icons/lucide/user-round-cog' +import '~icons/lucide/pencil' +import '~icons/lucide/file' +import '~icons/lucide/square-arrow-out-up-right' +import { LiveStore } from 'rdflib' +import styles from './ResourceActionsMenu.styles.css' +import { isContainerSubject } from '@/utils/podUtils' + +type ResourceActionMenuItem = { + label: string + icon?: HTMLElement | string + action?: (event: Event) => void +} + +type ActionableResourceActionMenuItem = ResourceActionMenuItem & { + action: (event: Event) => void +} + +@customElement('solid-ui-resource-actions-menu') +export default class ResourceActionsMenu extends WebComponent { + static styles = styles + + @property({ attribute: false }) + accessor store: LiveStore | undefined + + @property({ type: String }) + accessor subjectUri: string | undefined + + @property({ attribute: false }) + accessor handleAccessClick: (() => void) | undefined = undefined + + @property({ attribute: false }) + accessor handleEditingClick: (() => void) | undefined = undefined + + @property({ type: Boolean }) + accessor paneSupportsEditing = false + + @property({ type: Boolean }) + accessor canEdit = false + + @property({ type: Boolean }) + accessor canManageAccess = false + + @property({ type: Boolean }) + accessor isMobile = false + + @property({ type: Boolean, attribute: 'show-access-in-menu' }) + accessor showAccessInMenu = false + + @property({ attribute: false }) + accessor menuItems: ResourceActionMenuItem[] = [] + + private handleOpenInNewTab = () => { + if (!this.subjectUri) return + window.open(this.subjectUri, '_blank', 'noopener,noreferrer') + } + + render () { + const isContainerResource = isContainerSubject(this.store, this.subjectUri) + const canEdit = !isContainerResource && this.isMobile && this.paneSupportsEditing && this.canEdit && !!this.handleEditingClick + const canManageAccess = this.canManageAccess && !!this.handleAccessClick && + (this.showAccessInMenu || this.isMobile) + const actionableMenuItems = this.menuItems.filter((item): item is ActionableResourceActionMenuItem => typeof item.action === 'function') + return html` + + + + More options + + ${actionableMenuItems.map(item => html` + item.action(event)}> + ${typeof item.icon === 'string' + ? html`` + : item.icon ?? html``} + ${item.label} + + `)} + ${this.subjectUri + ? html` + + + Open in New Tab + + ` + : nothing} + ${canEdit + ? html` + + + Edit + + ` + : nothing} + ${canManageAccess + ? html` + + + Manage Access + + ` + : nothing} + + ` + } +} diff --git a/src/components/resource-actions-menu/index.ts b/src/components/resource-actions-menu/index.ts new file mode 100644 index 000000000..a00c20ed1 --- /dev/null +++ b/src/components/resource-actions-menu/index.ts @@ -0,0 +1 @@ +export { default } from './ResourceActionsMenu' diff --git a/src/components/select/Select.stories.ts b/src/components/select/Select.stories.ts index 2d5114409..75756e670 100644 --- a/src/components/select/Select.stories.ts +++ b/src/components/select/Select.stories.ts @@ -7,6 +7,7 @@ import './Select' const args = { label: 'What is the best food?', options: 'Pizza, Ramen, Tacos', + srOnlyLabel: false, } const meta = { @@ -15,12 +16,13 @@ const meta = { argTypes: { label: { control: 'text' }, options: { control: 'text' }, + srOnlyLabel: { control: 'boolean' }, }, - render ({ label, options }: typeof args) { + render ({ label, options, srOnlyLabel }: typeof args) { const parsedOptions = options.split(',').map(option => option.trim()) return html` - + ${parsedOptions.map((option, index) => { const indent = index === 0 ? '' : ' ' diff --git a/src/index.ts b/src/index.ts index 10b6aac46..fab8d4808 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,5 +66,5 @@ export type { CreateContext, NewAppInstanceOptions } from './create/types' export * from './lib/auth' export * from './lib/components' export * from './lib/dialogs' -export * from './lib/file-explorer/context' +export * from './lib/file-explorer' export * from './lib/code-editor' diff --git a/src/lib/code-editor/CodeEditor.ts b/src/lib/code-editor/CodeEditor.ts index 8d1cab6a7..a464d442a 100644 --- a/src/lib/code-editor/CodeEditor.ts +++ b/src/lib/code-editor/CodeEditor.ts @@ -2,7 +2,7 @@ import type { ThemeMode } from 'pane-registry' import { Compartment, EditorState } from '@codemirror/state' import { EditorView, drawSelection, keymap, lineNumbers } from '@codemirror/view' import { defaultHighlightStyle, syntaxHighlighting, StreamLanguage } from '@codemirror/language' -import { defaultKeymap, history, historyKeymap } from '@codemirror/commands' +import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands' import { css } from '@codemirror/lang-css' import { html } from '@codemirror/lang-html' import { javascript } from '@codemirror/lang-javascript' @@ -18,16 +18,27 @@ export class CodeEditor { private _view: EditorView | null = null private _languageCompartment: Compartment | null = null private _editableCompartment: Compartment | null = null + private _readOnlyCompartment: Compartment | null = null + private _accessibilityCompartment: Compartment | null = null private _onDirtyChange?: (dirty: boolean) => void private _isDirty = false - async initialize (container: HTMLElement, initialDoc = '', contentType: string = 'text/turtle', theme: ThemeMode = 'dark', onDirtyChange?: (dirty: boolean) => void) { + async initialize ( + container: HTMLElement, + initialDoc = '', + contentType: string = 'text/turtle', + theme: ThemeMode = 'dark', + onDirtyChange?: (dirty: boolean) => void, + accessibility?: { label?: string, description?: string } + ) { if (this._view) { this._view.destroy() } this._languageCompartment = new Compartment() this._editableCompartment = new Compartment() + this._readOnlyCompartment = new Compartment() + this._accessibilityCompartment = new Compartment() this._onDirtyChange = onDirtyChange this._isDirty = false const languageExtension = await this._getLanguageExtension(contentType) @@ -43,6 +54,11 @@ export class CodeEditor { }), this._languageCompartment.of(languageExtension), this._editableCompartment.of(EditorView.editable.of(true)), + this._readOnlyCompartment.of(EditorState.readOnly.of(false)), + this._accessibilityCompartment.of(EditorView.contentAttributes.of({ + ...(accessibility?.label ? { 'aria-label': accessibility.label } : {}), + ...(accessibility?.description ? { 'aria-description': accessibility.description } : {}) + })), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), lineNumbers(), history(), @@ -55,6 +71,7 @@ export class CodeEditor { } }), keymap.of([ + indentWithTab, ...defaultKeymap, ...historyKeymap ]) @@ -93,9 +110,23 @@ export class CodeEditor { setReadOnly (readOnly: boolean) { if (!this._view) return const editableCompartment = this._editableCompartment - if (!editableCompartment) return + const readOnlyCompartment = this._readOnlyCompartment + if (!editableCompartment || !readOnlyCompartment) return this._view.dispatch({ - effects: editableCompartment.reconfigure(EditorView.editable.of(!readOnly)) + effects: [ + editableCompartment.reconfigure(EditorView.editable.of(!readOnly)), + readOnlyCompartment.reconfigure(EditorState.readOnly.of(readOnly)) + ] + }) + } + + setAccessibility (accessibility: { label?: string, description?: string }) { + if (!this._view || !this._accessibilityCompartment) return + this._view.dispatch({ + effects: this._accessibilityCompartment.reconfigure(EditorView.contentAttributes.of({ + ...(accessibility.label ? { 'aria-label': accessibility.label } : {}), + ...(accessibility.description ? { 'aria-description': accessibility.description } : {}) + })) }) } diff --git a/src/lib/components/form-control-component/FormControlComponent.ts b/src/lib/components/form-control-component/FormControlComponent.ts index 9a5e0b4da..5cacef32d 100644 --- a/src/lib/components/form-control-component/FormControlComponent.ts +++ b/src/lib/components/form-control-component/FormControlComponent.ts @@ -8,6 +8,9 @@ export default abstract class FormControlComponent${this.target.label}` + ? html`` : nothing } diff --git a/src/lib/context/noopContext.ts b/src/lib/context/noopContext.ts new file mode 100644 index 000000000..dd9e04eba --- /dev/null +++ b/src/lib/context/noopContext.ts @@ -0,0 +1,13 @@ +export function noopContext (name: string): T { + const fail = () => { throw new Error(`Context '${name}' is missing. Is the component inside its provider?`) } + + return new Proxy({}, { + get: fail, + set: fail, + has: fail, + ownKeys: fail, + getOwnPropertyDescriptor: fail, + defineProperty: fail, + deleteProperty: fail + }) as T +} diff --git a/src/lib/file-explorer/compose-explorer.ts b/src/lib/file-explorer/compose-explorer.ts new file mode 100644 index 000000000..1ddc750bd --- /dev/null +++ b/src/lib/file-explorer/compose-explorer.ts @@ -0,0 +1,51 @@ +import type FileExplorerProvider from '@/components/file-explorer-provider/FileExplorerProvider' +import type FileExplorerHeader from '@/components/file-explorer-header/FileExplorerHeader' +import type { DataBrowserContext, PaneDefinition } from 'pane-registry' + +export type ExplorerComposition = { + context: DataBrowserContext + subjectUri: string + pane: PaneDefinition | undefined + relevantPanes: PaneDefinition[] + header?: Pick & Partial> + paneElement?: HTMLElement +} + +/** + * Composes one explorer without touching nested explorers or owning navigation. + * Callers register the provider/header components and render the pane content. + */ +export function composeExplorer ( + provider: FileExplorerProvider, + composition: ExplorerComposition +): void { + provider.context = composition.context + provider.subjectUri = composition.subjectUri + provider.pane = composition.pane + provider.relevantPanes = composition.relevantPanes + + const currentHeader = provider.querySelector( + ':scope > solid-ui-file-explorer-header' + ) + if (composition.header) { + const header = currentHeader || + provider.ownerDocument.createElement('solid-ui-file-explorer-header') + header.paneIcon = composition.header.paneIcon + header.menuItems = composition.header.menuItems + header.showBack = composition.header.showBack ?? true + if (provider.firstElementChild !== header) provider.prepend(header) + } else { + currentHeader?.remove() + } + + if (composition.paneElement) { + const paneElement = composition.paneElement + paneElement.classList.add('paneDiv') + const currentPane = provider.querySelector(':scope > .paneDiv') + if (currentPane) { + if (currentPane !== paneElement) currentPane.replaceWith(paneElement) + } else { + provider.appendChild(paneElement) + } + } +} diff --git a/src/lib/file-explorer/context.ts b/src/lib/file-explorer/context.ts index 9d291174f..1e349fe37 100644 --- a/src/lib/file-explorer/context.ts +++ b/src/lib/file-explorer/context.ts @@ -1,26 +1,22 @@ -import { createContext } from '@lit/context' -import { LiveStore } from 'rdflib' -import type { NamedNode } from 'rdflib' +import type { LiveStore } from 'rdflib' import type { PaneDefinition } from 'pane-registry' +import { createContext } from '@lit/context' +import { noopContext } from '@/lib/context/noopContext' -export interface FileExplorerEdit { - onEdit?: () => void - isDirty?: boolean - updateDirtyState?: (dirty: boolean) => void +export interface FileExplorerEditor { + beginEditing (): void } export interface FileExplorerContext { - store: LiveStore | undefined - subjectUri: string | undefined - pane?: PaneDefinition - soloPane?: boolean - - onBack?: () => void - openPane?: (subject: NamedNode, paneName: string) => void - handleAccessClick?: () => void - - paneSupportsEditing?: boolean - edit?: FileExplorerEdit + readonly store: LiveStore | undefined + readonly subjectUri: string | undefined + readonly pane: PaneDefinition | undefined + readonly isDirty: boolean + readonly canEdit: boolean // true while an editor is registered + registerEditor (editor: FileExplorerEditor): () => void // returns unregister + beginEditing (): void + setDirty (dirty: boolean): void } export const fileExplorerContext = createContext(Symbol('file-explorer')) +export const DEFAULT_FILE_EXPLORER_CONTEXT = noopContext('file-explorer') diff --git a/src/lib/file-explorer/events.ts b/src/lib/file-explorer/events.ts new file mode 100644 index 000000000..b8621925e --- /dev/null +++ b/src/lib/file-explorer/events.ts @@ -0,0 +1,25 @@ +export const FILE_EXPLORER_BACK_EVENT = 'solid-ui-file-explorer-back' + +export interface FileExplorerBackDetail { + subjectUri?: string +} + +export const FILE_EXPLORER_MANAGE_ACCESS_EVENT = 'solid-ui-file-explorer-manage-access' + +export interface FileExplorerManageAccessDetail { + subjectUri: string +} + +export const FILE_EXPLORER_OPEN_PANE_EVENT = 'solid-ui-file-explorer-open-pane' + +export interface FileExplorerOpenPaneDetail { + subjectUri: string + paneName: string +} + +/** Whether an event belongs to this explorer rather than a nested one. */ +export function isExplorerEvent (event: Event, provider: HTMLElement): boolean { + return event.composedPath().find(target => + 'localName' in target && target.localName === 'solid-ui-file-explorer-provider' + ) === provider +} diff --git a/src/lib/file-explorer/index.ts b/src/lib/file-explorer/index.ts new file mode 100644 index 000000000..da7c8c7e3 --- /dev/null +++ b/src/lib/file-explorer/index.ts @@ -0,0 +1,5 @@ +export * from './context' +export * from './events' +export * from './relevant-panes' +export * from './compose-explorer' +export * from './pane-menu-items' diff --git a/src/lib/file-explorer/pane-menu-items.ts b/src/lib/file-explorer/pane-menu-items.ts new file mode 100644 index 000000000..f95185464 --- /dev/null +++ b/src/lib/file-explorer/pane-menu-items.ts @@ -0,0 +1,66 @@ +import type { DataBrowserContext, PaneDefinition } from 'pane-registry' +import type { NamedNode } from 'rdflib' + +export type PaneMenuIcon = string + +export type PaneMenuItem = { + key?: string + label: string + icon?: PaneMenuIcon + action: (event: Event) => void +} + +export type PaneMenuPane = Omit & { + icon: string | PaneIconResolver +} + +export type PaneIconResolver = ( + subject: NamedNode, + context: DataBrowserContext +) => PaneMenuIcon | undefined | Promise + +export type PaneMenuPreparation = { + subject: NamedNode + context: DataBrowserContext + panes: PaneMenuPane[] + openPane: (subject: NamedNode, paneName: string, event: Event) => void + iconOverrides?: Record +} + +async function resolvePaneIcon ( + pane: PaneMenuPane, + subject: NamedNode, + context: DataBrowserContext, + override: PaneIconResolver | undefined +) { + if (override) return override(subject, context) + return typeof pane.icon === 'function' + ? pane.icon(subject, context) + : pane.icon +} + +export async function preparePaneMenuItems ( + input: PaneMenuPreparation +): Promise { + const items: Array = await Promise.all(input.panes.map(async pane => { + const label = pane.label(input.subject, input.context) + if (!label) return undefined + + const icon = await resolvePaneIcon( + pane, + input.subject, + input.context, + input.iconOverrides?.[pane.name] + ) + + const item: PaneMenuItem = { + key: pane.name, + label, + ...(icon ? { icon } : {}), + action: (event: Event) => input.openPane(input.subject, pane.name, event) + } + return item + })) + + return items.filter((item): item is PaneMenuItem => item !== undefined) +} diff --git a/src/lib/file-explorer/relevant-panes.ts b/src/lib/file-explorer/relevant-panes.ts new file mode 100644 index 000000000..375aaaa02 --- /dev/null +++ b/src/lib/file-explorer/relevant-panes.ts @@ -0,0 +1,68 @@ +import type { DataBrowserContext, PaneDefinition } from 'pane-registry' +import type { NamedNode } from 'rdflib' +import { filterAvailablePanes } from '@/login/login' +import ns from '@/lib/ns' + +export type RelevantPaneOptions = { + excludedPaneNames?: readonly string[] + fallbackPaneName?: string +} + +/** + * Returns panes that can represent a subject, preserving legacy outliner ordering. + * + * Audience filtering narrows the list, but the first relevant pane is retained + * even when its audience does not match. The legacy outliner deliberately kept + * this most relevant view available, including when no panes match the audience. + */ +export async function getRelevantPanes ( + subject: NamedNode, + context: DataBrowserContext, + options: RelevantPaneOptions = {} +): Promise { + const excludedPaneNames = new Set(options.excludedPaneNames || []) + const paneRegistry = context.session.paneRegistry + const relevantPanes = paneRegistry.list.filter( + pane => !pane.global && + !excludedPaneNames.has(pane.name) && + pane.label(subject, context) + ) + + if (relevantPanes.length === 0) { + const fallbackPane = options.fallbackPaneName + ? paneRegistry.byName(options.fallbackPaneName) + : undefined + return fallbackPane ? [fallbackPane] : [] + } + + const filteredPanes = await filterAvailablePanes(relevantPanes) + if (filteredPanes.length === 0) return [relevantPanes[0]] + + const firstRelevantPaneIndex = paneRegistry.list.indexOf(relevantPanes[0]) + const firstFilteredPaneIndex = paneRegistry.list.indexOf(filteredPanes[0]) + return firstRelevantPaneIndex < firstFilteredPaneIndex + ? [relevantPanes[0]].concat(filteredPanes) + : filteredPanes +} + +/** + * Chooses the default pane from the relevant panes for a resource. + * + * A resource's `ui:mintClass` is an explicit declaration of the pane that + * created it, so it takes precedence over panes that merely request focus. + */ +export function getPreferredPane ( + subject: NamedNode, + context: DataBrowserContext, + relevantPanes: PaneDefinition[] +): PaneDefinition | undefined { + const store = context.session.store + const mintClass = store.any(subject, ns.ui('mintClass'), undefined, subject.doc()) || + store.any(subject.doc(), ns.ui('mintClass'), undefined, subject.doc()) + if (mintClass) { + const mintClassPane = relevantPanes.find(pane => pane.mintClass?.sameTerm(mintClass)) + if (mintClassPane) return mintClassPane + } + + return relevantPanes.find(pane => pane.shouldGetFocus?.(subject)) || relevantPanes[0] +} diff --git a/src/styles/theme.css b/src/styles/theme.css index 7a8fe5a17..1bf6c06c4 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -8,27 +8,43 @@ --solid-ui-color-primary: #7c4dff; --solid-ui-color-primary-hover: var(--solid-ui-color-lavender-300); --solid-ui-color-primary-selected: var(--solid-ui-color-lavender-400); + --solid-ui-color-blue-900: #083575; --solid-ui-color-tertiary: #083575; --solid-ui-color-white: #ffffff; --solid-ui-color-error: #b00020; + --solid-ui-color-pink-50: #fff7f8; + --solid-ui-color-pink-100: #ffe7eb; + --solid-ui-color-pink-200: #f0d8dc; --solid-ui-color-gray-100: #f3f4f6; + --solid-ui-color-gray-150: #f0f0f0; --solid-ui-color-gray-200: #cbd5e1; + --solid-ui-color-gray-300: #d1d5dc; --solid-ui-color-gray-400: #99a1af; --solid-ui-color-gray-500: #6a7282; --solid-ui-color-gray-600: #4a5565; --solid-ui-color-gray-700: #364153; --solid-ui-color-gray-800: #101828; + --solid-ui-color-gray-900: #101828; --solid-ui-color-slate-50: #f8fafc; + --solid-ui-color-neutral-50: #fafafa; + --solid-ui-color-gray-50: #f9fafb; --solid-ui-color-slate-200: #e2e8f0; --solid-ui-color-slate-400: #90a1b9; --solid-ui-color-slate-800: #1d293d; + --solid-ui-color-zinc-300: #d4d4d8; + --solid-ui-color-zinc-500: #71717b; + --solid-ui-color-lavender-300: #e6dcff; --solid-ui-color-lavender-400: #cbb9ff; + --solid-ui-color-violet-50: #F5F3FF; + --solid-ui-color-red-500: #fb2c36; + --solid-ui-color-red-600: #be2e48; + --solid-ui-color-red-700: #b00020; --solid-ui-color-body-grey: #666; diff --git a/src/types/custom-elements.d.ts b/src/types/custom-elements.d.ts index 62587eafe..b5df7d19f 100644 --- a/src/types/custom-elements.d.ts +++ b/src/types/custom-elements.d.ts @@ -1,3 +1,4 @@ +import type AccessControlModal from '../components/access-control-modal/AccessControlModal' import type Account from '../components/account/Account' import type Avatar from '../components/avatar/Avatar' import type Button from '../components/button/Button' @@ -9,6 +10,9 @@ import type DialogFooter from '../components/dialog-footer/DialogFooter' import type DialogHeader from '../components/dialog-header/DialogHeader' import type DialogProvider from '../components/dialog-provider/DialogProvider' import type DialogsRoot from '../components/dialogs-root/DialogsRoot' +import type ErrorState from '../components/error-state/ErrorState' +import type FileExplorerHeader from '../components/file-explorer-header/FileExplorerHeader' +import type FileExplorerProvider from '../components/file-explorer-provider/FileExplorerProvider' import type Guard from '../components/guard/Guard' import type Input from '../components/input/Input' import type LoginButton from '../components/login-button/LoginButton' @@ -21,6 +25,7 @@ import type PhotoCaptureModal from '../components/photo-capture-modal/PhotoCaptu import type Provider from '../components/provider/Provider' import type RDFForm from '../components/rdf-form/RDFForm' import type RDFInput from '../components/rdf-input/RDFInput' +import type ResourceActionsMenu from '../components/resource-actions-menu/ResourceActionsMenu' import type Select from '../components/select/Select' import type SelectOption from '../components/select-option/SelectOption' import type SignupButton from '../components/signup-button/SignupButton' @@ -28,6 +33,7 @@ import type SolidEmblem from '../components/solid-emblem/SolidEmblem' declare global { interface HTMLElementTagNameMap { + 'solid-ui-access-control-modal': AccessControlModal 'solid-ui-account': Account 'solid-ui-avatar': Avatar 'solid-ui-button': Button @@ -39,6 +45,9 @@ declare global { 'solid-ui-dialog-header': DialogHeader 'solid-ui-dialog-provider': DialogProvider 'solid-ui-dialogs-root': DialogsRoot + 'solid-ui-error-state': ErrorState + 'solid-ui-file-explorer-header': FileExplorerHeader + 'solid-ui-file-explorer-provider': FileExplorerProvider 'solid-ui-guard': Guard 'solid-ui-input': Input 'solid-ui-login-button': LoginButton @@ -51,6 +60,7 @@ declare global { 'solid-ui-provider': Provider 'solid-ui-rdf-form': RDFForm 'solid-ui-rdf-input': RDFInput + 'solid-ui-resource-actions-menu': ResourceActionsMenu 'solid-ui-select': Select 'solid-ui-select-option': SelectOption 'solid-ui-signup-button': SignupButton diff --git a/src/utils/podUtils.ts b/src/utils/podUtils.ts new file mode 100644 index 000000000..db25cd393 --- /dev/null +++ b/src/utils/podUtils.ts @@ -0,0 +1,90 @@ +import { solidLogicSingleton, store } from 'solid-logic' +import ns from '@/lib/ns' +import { LiveStore, NamedNode, parse } from 'rdflib' +import { isWebIdUri } from './webIdUtils' + +export async function getPodStorages (url: NamedNode): Promise { + if (isWebIdUri(url)) { + try { + await store.fetcher.load(url.doc()) + } catch (err) { + console.warn('Unable to load profile document for WebID', url.doc().uri, err) + } + const podStorages = store.each(url, ns.space('storage')) + const results = await Promise.all(podStorages.map(async pod => await isPodStorage(pod as NamedNode) ? pod as NamedNode : null)) + return results.filter(pod => pod !== null) as NamedNode[] + } + try { + const storage = await findPodStorageFromUrl(url) + return storage ? [storage] : [] + } catch (err) { + console.error('cannot load container', err) + return [] + } +} + +async function findPodStorageFromUrl (url: NamedNode): Promise { + const podStorage = new URL(url.value || url.uri) + let pathStorage = podStorage.pathname + + while (pathStorage.length) { + pathStorage = pathStorage.substring(0, pathStorage.lastIndexOf('/')) + const candidate = store.sym(`${podStorage.origin}${pathStorage}/`) + if (await isPodStorage(candidate)) return candidate + } + + // TODO should url.origin be added to pods list when there are no pim:Storage ??? + return null +} + +async function isPodStorage (pod: NamedNode): Promise { + await loadContainerRepresentation(pod) + return store.holds(pod, ns.rdf('type'), ns.space('Storage'), pod.doc()) +} + +export async function loadContainerRepresentation (subject) { + // force reload for index.html with RDFa + if (!store.any(subject, ns.ldp('contains'), undefined, subject.doc())) { + try { + const response = await store.fetcher.webOperation('GET', subject.uri, store.fetcher.initFetchOptions(subject.uri, { headers: { accept: 'text/turtle' } })) + const containerTurtle = response.responseText + if (subject.uri && containerTurtle) { + parse(containerTurtle, store, subject.uri, 'text/turtle') + } + } catch (err) { + console.warn('Unable to load container representation for', subject.uri, err) + } + } +} + +export function isContainerSubject (store: LiveStore | undefined, subjectUri: string | undefined): boolean { + if (!store || !subjectUri) return false + + const subject = store.sym(subjectUri) + const typeUris = store.findTypeURIs(subject) + return Boolean( + typeUris[ns.ldp('Container').uri] || + typeUris[ns.ldp('BasicContainer').uri] || + subject.uri.endsWith('/') + ) +} + +// The shared rdflib store can hold duplicate ldp:contains statements for the +// same child resource when the container and companion metadata are both loaded. +// Count unique visible children here so the header summary matches the list UI. +export function getContainerItemCount (store: LiveStore | undefined, subjectUri: string | undefined): number { + if (!store || !subjectUri) return 0 + + const subject = store.sym(subjectUri) + const seen = new Set() + + for (const item of store.each(subject, ns.ldp('contains'))) { + if (item.termType === 'NamedNode') { + const resource = item as NamedNode + if (!solidLogicSingleton.resource.isHiddenFile(resource)) { + seen.add(resource.uri) + } + } + } + return seen.size +} diff --git a/src/utils/webIdUtils.ts b/src/utils/webIdUtils.ts new file mode 100644 index 000000000..5eb9cac32 --- /dev/null +++ b/src/utils/webIdUtils.ts @@ -0,0 +1,122 @@ +/* +* Utility functions to help load the profile +* especially when I am not logged in +*/ + +import { IndexedFormula, NamedNode, sym } from 'rdflib' +import ns from '@/lib/ns' +import { authn, store } from 'solid-logic' + +const DEFAULT_PROFILE_PATH = 'profile/card#me' + +export function isOwnWebIdProfile (subject: NamedNode): boolean { + const viewer = authn.currentUser() + return Boolean(viewer && (viewer.sameTerm(subject) || viewer.doc().sameTerm(subject))) +} + +export async function loadProfileFromURI ( + uri: NamedNode +): Promise { + try { + const pod = uri.site().uri + // TODO: This is a hack - we cannot assume that the profile is at this document, but we will live with it for now + const webId = sym(`${pod}${DEFAULT_PROFILE_PATH}`) + try { + await store.fetcher.load(webId) + return webId + } catch (err) { + // ignore any failure and continue fallback lookup + } + + // we try a prefixed pod structure + try { + const uriUrl = new URL(uri.uri) + const pathSegments = uriUrl.pathname.split('/').filter(Boolean) + if (pathSegments.length > 0) { + const derivedPod = `${uriUrl.origin}/${pathSegments[0]}/` + const derivedWebId = sym(`${derivedPod}${DEFAULT_PROFILE_PATH}`) + await store.fetcher.load(derivedWebId) + return derivedWebId + } + } catch (err) { + // ignore any failure and continue fallback lookup + } + + try { + await store.fetcher.load(uri) + } catch (err) { + return uri + } + + const primaryTopic = store.any(uri, ns.foaf('primaryTopic'), null, uri.doc()) + if (primaryTopic && primaryTopic.termType === 'NamedNode') { + try { + await store.fetcher.load(primaryTopic as NamedNode) + return primaryTopic as NamedNode + } catch (err) { + return uri + } + } + + return uri + } catch (err) { + return uri + } +} + +export function isWebIdUri (uri: NamedNode): boolean { + try { + const parsedURL = new URL(uri.uri) + return parsedURL.hash === '#me' + } catch (err) { + return false + } +} + +export async function getNameOfPodOwner ( + pod: NamedNode +): Promise { + // TODO: This is a hack - we cannot assume that the profile is at this document, but we will live with it for now + const webId = sym(`${pod.uri}${DEFAULT_PROFILE_PATH}`) + try { + await store.fetcher.load(webId) + return getName(store, webId) + } catch (err) { + if (!isFetchErrorStatus(err, 403)) { + console.error('getNameOfPodOwner failed on default profile:', err) + } + } + + // we try a prefixed pod structure + try { + const uriUrl = new URL(pod.uri) + const pathSegments = uriUrl.pathname.split('/').filter(Boolean) + if (pathSegments.length > 0) { + const derivedPod = `${uriUrl.origin}/${pathSegments[0]}/` + const derivedWebId = sym(`${derivedPod}${DEFAULT_PROFILE_PATH}`) + await store.fetcher.load(derivedWebId) + return getName(store, derivedWebId) + } + } catch (err) { + if (!isFetchErrorStatus(err, 403)) { + console.error('getNameOfPodOwner failed on derived profile:', err) + } + } + + return '' +} + +export function getName (store: IndexedFormula, ownersProfile: NamedNode): string { + return ( + store.anyValue(ownersProfile, ns.vcard('fn'), null, ownersProfile.doc()) || + store.anyValue(ownersProfile, ns.foaf('name'), null, ownersProfile.doc()) || + new URL(ownersProfile.uri).host.split('.')[0] + ) +} + +function isFetchErrorStatus (err: unknown, status: number): boolean { + if (!err || typeof err !== 'object') return false + const statusCode = (err as any)?.response?.status ?? (err as any)?.status + const parsedStatus = Number(statusCode) + return Number.isFinite(parsedStatus) && parsedStatus === status +} diff --git a/src/widgets/buttons.ts b/src/widgets/buttons.ts index 8a0e18e2b..a6b3779c4 100644 --- a/src/widgets/buttons.ts +++ b/src/widgets/buttons.ts @@ -42,6 +42,7 @@ export type RenderAsDivOptions = { image?: HTMLImageElement, title?: string, deleteFunction?: () => void, + confirmationContainer?: HTMLElement, link?: boolean, noun?: string, draggable?: boolean, @@ -464,16 +465,21 @@ export function faviconOrDefault (dom: HTMLDocument, x: NamedNode) { /* Two-option dialog pop-up */ -function renderDeleteConfirmPopup (dom, refererenceElement, prompt, deleteFunction) { +function renderDeleteConfirmPopup (dom: HTMLDocument, refererenceElement: HTMLElement, prompt: string, deleteFunction: () => any, restoreFocus: () => void) { function removePopup () { - refererenceElement.parentElement.removeChild(refererenceElement) + refererenceElement.remove() + restoreFocus() } function removePopupAndDoDeletion () { removePopup() - deleteFunction() + const result = deleteFunction() + if (result && typeof result.then === 'function') { + return result.then(restoreFocus) + } + restoreFocus() } const popup = dom.createElement('div') - popup.style = style.confirmPopupStyle + popup.style.cssText = style.confirmPopupStyle popup.style.position = 'absolute' popup.style.top = '-1em' // try leaving original button clear @@ -492,7 +498,7 @@ function renderDeleteConfirmPopup (dom, refererenceElement, prompt, deleteFuncti xButton.style.gridRow = '2' const cancelPrompt = popup.appendChild(dom.createElement('button')) - cancelPrompt.style = style.buttonStyle + cancelPrompt.style.cssText = style.buttonStyle cancelPrompt.style.gridRow = '2' cancelPrompt.style.gridColumn = '2' cancelPrompt.textContent = 'Cancel' // @@ I18n @@ -503,7 +509,7 @@ function renderDeleteConfirmPopup (dom, refererenceElement, prompt, deleteFuncti affirmIcon.style.gridColumn = '1' const sureButtonElt = popup.appendChild(dom.createElement('button')) - sureButtonElt.style = style.buttonStyle + sureButtonElt.style.cssText = style.buttonStyle sureButtonElt.style.gridRow = '1' sureButtonElt.style.gridColumn = '2' sureButtonElt.textContent = prompt @@ -519,33 +525,87 @@ function renderDeleteConfirmPopup (dom, refererenceElement, prompt, deleteFuncti } /** * Delete button with a check you really mean it + * Return a promise from deleteFunction for focus restoration after asynchronous deletion. * @@ Supress check if command key held down? */ export function deleteButtonWithCheck ( dom: HTMLDocument, container: HTMLElement, noun: string, - deleteFunction: () => any + deleteFunction: () => any, + confirmationContainer?: HTMLElement ) { function createPopup () { + const focusableSelector = 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' + const focusScopes: { element: HTMLElement, index: number }[] = [] + if (confirmationContainer) { + for (let ancestor: HTMLElement | null = container; ancestor; ancestor = ancestor.parentElement) { + const controls = Array.from(ancestor.querySelectorAll(focusableSelector)) + focusScopes.push({ element: ancestor, index: controls.indexOf(deleteButton) }) + } + } + function restoreFocus () { + if (!confirmationContainer) return + if (deleteButton.isConnected) { + deleteButton.focus() + return + } + for (const scope of focusScopes) { + if (!scope.element.isConnected) continue + const controls = scope.element.querySelectorAll(focusableSelector) + if (controls.length) { + controls[Math.min(Math.max(scope.index, 0), controls.length - 1)].focus() + return + } + } + const survivingScope = focusScopes.find(scope => scope.element.isConnected) + if (survivingScope) { + survivingScope.element.tabIndex = -1 + survivingScope.element.focus() + } + } const refererenceElement = dom.createElement('div') - container.insertBefore(refererenceElement, deleteButton) - refererenceElement.style.position = 'relative' // Needed as reference for popup - refererenceElement.appendChild(renderDeleteConfirmPopup(dom, refererenceElement, prompt, deleteFunction)) + if (confirmationContainer) { + confirmationContainer.querySelector(':scope > [data-delete-confirmation]')?.remove() + refererenceElement.setAttribute('data-delete-confirmation', '') + refererenceElement.setAttribute('role', 'dialog') + refererenceElement.setAttribute('aria-label', prompt) + confirmationContainer.appendChild(refererenceElement) + } else { + container.insertBefore(refererenceElement, deleteButton) + refererenceElement.style.position = 'relative' // Needed as reference for popup + } + const popup = renderDeleteConfirmPopup(dom, refererenceElement, prompt, deleteFunction, restoreFocus) + refererenceElement.appendChild(popup) + if (confirmationContainer) { + popup.style.position = 'static' + popup.style.top = '' + Array.from(popup.querySelectorAll('button')) + .find(button => button.textContent === 'Cancel')?.focus() + } } const minusIconURI = iconBase + 'noun_2188_red.svg' // white minus in red #cc0000 circle const deleteButton = dom.createElement('img') deleteButton.setAttribute('src', minusIconURI) + deleteButton.tabIndex = 0 + deleteButton.setAttribute('role', 'button') deleteButton.setAttribute('style', style.smallButtonStyle) // @@tsc - would set deleteButton.style deleteButton.style.float = 'right' // Historically this has alwaus floated right const prompt = 'Remove this ' + noun deleteButton.title = prompt + deleteButton.setAttribute('aria-label', prompt) // @@ In an ideal world, make use of hover an accessibility option deleteButton.classList.add('hoverControlHide') deleteButton.addEventListener('click', createPopup) + deleteButton.addEventListener('keydown', event => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + deleteButton.click() + } + }) container.classList.add('hoverControl') container.appendChild(deleteButton) @@ -769,7 +829,7 @@ export function renderAsRow (dom: HTMLDocument, pred: NamedNode, obj: NamedNode, } if (options.deleteFunction) { - deleteButtonWithCheck(dom, td3, options.noun || 'one', options.deleteFunction) + deleteButtonWithCheck(dom, td3, options.noun || 'one', options.deleteFunction, options.confirmationContainer) } if (obj.uri) { // blank nodes need not apply @@ -813,7 +873,7 @@ export function createLinkDiv (dom: HTMLDocument, div: HTMLDivElement, obj: Name linkDiv.setAttribute('style', style.linkDivStyle) if (options.deleteFunction) { - deleteButtonWithCheck(dom, linkDiv, options.noun || 'one', options.deleteFunction) + deleteButtonWithCheck(dom, linkDiv, options.noun || 'one', options.deleteFunction, options.confirmationContainer) } if (obj.uri) { @@ -878,6 +938,8 @@ export type attachmentListOptions = { predicate?: NamedNode uploadFolder?: NamedNode noun?: string + /** Render delete confirmations here in normal flow, restoring focus to the list when closed. */ + confirmationContainer?: HTMLElement renderSupportingInfo?: RenderSupportingInfo renderNameSuffix?: RenderNameSuffix refreshOnDocumentLoad?: boolean @@ -900,11 +962,12 @@ export function attachmentList (dom: HTMLDocument, subject: NamedNode, div: HTML const refreshOnDocumentLoad = options.refreshOnDocumentLoad ?? true const deleteAttachment = function (target) { - if (!kb.updater) { + const updater = kb.updater + if (!updater) { throw new Error('kb has no updater') } - kb.updater.update(st(subject, predicate, target, doc) as any, [], function ( + return new Promise(resolve => updater.update(st(subject, predicate, target, doc) as any, [], function ( uri, ok, errorBody, @@ -915,7 +978,8 @@ export function attachmentList (dom: HTMLDocument, subject: NamedNode, div: HTML } else { complain(undefined, 'Error deleting one: ' + errorBody) } - }) + resolve() + })) } function createNewRow (target) { @@ -923,6 +987,7 @@ export function attachmentList (dom: HTMLDocument, subject: NamedNode, div: HTML const opt: any = { noun } opt.renderSupportingInfo = options.renderSupportingInfo opt.renderNameSuffix = options.renderNameSuffix + opt.confirmationContainer = options.confirmationContainer if (hasAsyncEnrichedRowOptions && refreshOnDocumentLoad && target?.uri && kb.fetcher) { const targetDoc = target.doc() @@ -943,7 +1008,7 @@ export function attachmentList (dom: HTMLDocument, subject: NamedNode, div: HTML if (modify) { opt.deleteFunction = function () { - deleteAttachment(theTarget) + return deleteAttachment(theTarget) } } return personTR(dom, predicate, target, opt) diff --git a/src/widgets/buttons/iconLinks.ts b/src/widgets/buttons/iconLinks.ts index d773987d5..91d4d160b 100644 --- a/src/widgets/buttons/iconLinks.ts +++ b/src/widgets/buttons/iconLinks.ts @@ -1,5 +1,5 @@ import { NamedNode } from 'rdflib' -import { originalIconBase } from '../../lib/iconBase' +import '~icons/lucide/square-arrow-out-up-right' /** * Creates an anchor tag for a NamedNode @@ -21,12 +21,16 @@ export function linkIcon (dom: HTMLDocument, subject: NamedNode, iconURI?: strin // If diff web page anchor.setAttribute('target', '_blank') // open in a new tab or window } // as mailboxes and mail messages do not need new browser window - const img = anchor.appendChild(dom.createElement('img')) - img.setAttribute( - 'src', - iconURI || originalIconBase + 'go-to-this.png' - ) - img.setAttribute('style', 'margin: 0.3em;') + if (iconURI) { + const img = anchor.appendChild(dom.createElement('img')) + img.setAttribute('src', iconURI) + img.setAttribute('style', 'margin: 0.3em;') + } else { + anchor.setAttribute('aria-label', 'Open resource') + const icon = anchor.appendChild(dom.createElement('icon-lucide-square-arrow-out-up-right')) + icon.setAttribute('aria-hidden', 'true') + icon.setAttribute('style', 'display: inline-block; width: 1em; height: 1em; margin: 0.2em; color: var(--color-text-secondary, #4A5565);') + } return anchor } /** diff --git a/src/widgets/forms/autocomplete/autocompletePicker.ts b/src/widgets/forms/autocomplete/autocompletePicker.ts index 8e5f723cd..932d0508c 100644 --- a/src/widgets/forms/autocomplete/autocompletePicker.ts +++ b/src/widgets/forms/autocomplete/autocompletePicker.ts @@ -157,11 +157,11 @@ export async function renderAutoComplete (dom: HTMLDocument, } clearList() const slimmed = filterByLanguage(bindings, languagePrefs) - return slimmed + return slimmed || [] } function filterByName (filter, bindings) { - return bindings.filter(binding => nameMatch(filter, binding.name.value)) + return (bindings || []).filter(binding => nameMatch(filter, binding.name.value)) } async function refreshList () { @@ -205,7 +205,7 @@ export async function renderAutoComplete (dom: HTMLDocument, } else { if (!allDisplayed || !lastFilter || !filter.startsWith(lastFilter)) { debug.log(` Querying database at "${filter}" cf last "${lastFilter}".`) - lastBindings = await loadBindingsAndFilterByLanguage(filter, languagePrefs) // freesh query + lastBindings = await loadBindingsAndFilterByLanguage(filter, languagePrefs) || [] // freesh query } // Trim table as search gets tighter: const slimmed = filterByName(filter, lastBindings) diff --git a/test/components/access-control-modal.test.ts b/test/components/access-control-modal.test.ts new file mode 100644 index 000000000..46ef67c96 --- /dev/null +++ b/test/components/access-control-modal.test.ts @@ -0,0 +1,1770 @@ +// @vitest-environment jsdom + +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { sym } from 'rdflib' +import type { AccessMode } from 'solid-logic' +import { isAbsoluteHttpUri, isOriginUrl } from '../../src/components/access-control-modal/helpers' + +const planGrant = vi.fn() +const planGrantContainer = vi.fn() +const planGrantContainerContents = vi.fn() +const planGrantContainerAndContents = vi.fn() +const planRevoke = vi.fn() +const planPublicRead = vi.fn() +const applyPlan = vi.fn() +const classifyAccessControlSubject = vi.fn() +const findAccessGrants = vi.fn() +const findEffectiveAcl = vi.fn() +const resetToDefault = vi.fn() +const isProtectedResource = vi.fn() +const load = vi.fn() + +vi.mock('@/components/combobox', () => ({ + defineAsyncComboboxOptionsProvider: (provider: T) => provider +})) + +vi.mock('@/components/combobox-option', () => ({})) + +vi.mock('solid-logic', async (importOriginal) => { + const actual = await importOriginal() + + return { + ...actual, + solidLogicSingleton: { + ...actual.solidLogicSingleton, + acl: { + ...actual.solidLogicSingleton.acl, + classifyAccessControlSubject, + planGrant, + planGrantContainer, + planGrantContainerContents, + planGrantContainerAndContents, + planPublicRead, + planRevoke, + applyPlan, + findAccessGrants, + findEffectiveAcl, + resetToDefault, + isProtectedResource + }, + store: { + ...actual.solidLogicSingleton.store, + fetcher: { + ...actual.solidLogicSingleton.store.fetcher, + load + } + }, + resource: { + ...actual.solidLogicSingleton.resource, + isWebId: vi.fn(() => true) + } + } + } +}) + +describe('AccessControlModal submit', () => { + beforeEach(async () => { + planGrant.mockReset() + planGrantContainer.mockReset() + planGrantContainerContents.mockReset() + planGrantContainerAndContents.mockReset() + planRevoke.mockReset() + planPublicRead.mockReset() + applyPlan.mockReset() + classifyAccessControlSubject.mockReset() + findAccessGrants.mockReset() + findEffectiveAcl.mockReset() + resetToDefault.mockReset() + isProtectedResource.mockReset() + load.mockReset() + classifyAccessControlSubject.mockImplementation(async (principal: string) => ({ + kind: 'agent' as const, + subjectValue: principal + })) + planGrant.mockResolvedValue({ target: 'https://example.com/resource.ttl.acl', deletes: [], inserts: [] }) + planGrantContainer.mockResolvedValue({ target: 'https://example.com/container/.acl', deletes: [], inserts: [] }) + planGrantContainerContents.mockResolvedValue({ target: 'https://example.com/container/.acl', deletes: [], inserts: [] }) + planGrantContainerAndContents.mockResolvedValue({ target: 'https://example.com/container/.acl', deletes: [], inserts: [] }) + planPublicRead.mockResolvedValue({ target: 'https://example.com/resource.ttl.acl', deletes: [], inserts: [] }) + applyPlan.mockResolvedValue(new Response('ok', { status: 200 })) + findAccessGrants.mockResolvedValue([]) + findEffectiveAcl.mockResolvedValue({ inherited: true, authorizations: [] }) + resetToDefault.mockResolvedValue(new Response(null, { status: 204 })) + isProtectedResource.mockReturnValue(false) + load.mockResolvedValue(undefined) + await import('../../src/components/access-control-modal/AccessControlModal') + await import('../../src/components/access-control-modal/AccessControlErrors') + }) + + describe('access-control URL predicates', () => { + it.each([ + 'http://example.com', + 'https://example.com/path', + 'https://example.com:8443/resource' + ])('accepts an absolute HTTP URI: %s', value => { + expect(isAbsoluteHttpUri(value)).toBe(true) + }) + + it.each([ + 'example.com', + '/relative/path', + 'mailto:alice@example.com', + 'not a URL' + ])('rejects a non-absolute HTTP URI: %s', value => { + expect(isAbsoluteHttpUri(value)).toBe(false) + }) + + it.each([ + 'http://example.com', + 'https://example.com/', + 'https://example.com:8443' + ])('recognizes an origin URL: %s', value => { + expect(isOriginUrl(value)).toBe(true) + }) + + it.each([ + 'https://example.com/path', + 'https://example.com?query=value', + 'https://user:password@example.com' + ])('rejects a URL with non-origin components: %s', value => { + expect(isOriginUrl(value)).toBe(false) + }) + }) + + it('forwards the error-state close event only once', async () => { + const element = document.createElement('solid-ui-access-control-errors') as any + element.errors = [{ + action: 'grant', + targetUri: 'https://example.com/alice#me', + label: 'Alice', + role: 'Editor', + message: 'Grant failed' + }] + const close = vi.fn() + element.addEventListener('close', close) + document.body.appendChild(element) + await element.updateComplete + + const errorState = element.shadowRoot.querySelector('solid-ui-error-state') + await errorState.updateComplete + const closeButton = errorState.shadowRoot.querySelector('solid-ui-button.close-button') + const innerButton = closeButton.shadowRoot.querySelector('button') + innerButton.click() + + expect(close).toHaveBeenCalledTimes(1) + document.body.removeChild(element) + }) + + it('queues a grant on submit without saving it', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.principalInputValue = 'https://alice.example.com/profile/card.ttl#me' + element.addAccessRoleValue = 'Editor' + + await element.onSubmit(new Event('submit')) + + expect(planGrant).not.toHaveBeenCalled() + expect(applyPlan).not.toHaveBeenCalled() + expect(element.pendingAccessGrants).toHaveLength(1) + expect(element.principalInputValue).toBe('') + }) + + it('rejects an HTTP URL that cannot be classified', async () => { + classifyAccessControlSubject.mockResolvedValueOnce(undefined) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.principalInputValue = 'https://unknown.example/resource' + + await element.onSubmit(new Event('submit')) + + expect(element.pendingAccessGrants).toEqual([]) + expect(element.inputError).toContain('Could not verify this access target') + }) + + it.each([ + ['app.example', 'https://app.example'], + ['www.sharonstratsianis.com', 'https://www.sharonstratsianis.com'] + ])('queues a bare origin as %s', async (input, origin) => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.principalInputValue = input + + await element.onSubmit(new Event('submit')) + + expect(classifyAccessControlSubject).not.toHaveBeenCalled() + expect(element.pendingAccessGrants).toEqual([ + expect.objectContaining({ + subjectType: 'origin', + subjectValue: origin + }) + ]) + }) + + it('keeps newer input when an async lookup resolves later', async () => { + let resolveClassification!: (value: { kind: 'agent', subjectValue: string }) => void + classifyAccessControlSubject.mockImplementationOnce(() => new Promise(resolve => { + resolveClassification = resolve + })) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.principalInputValue = 'https://alice.example.com/profile/card.ttl#me' + element.addAccessRoleValue = 'Editor' + + const submitPromise = element.onSubmit(new Event('submit')) + element.principalInputValue = 'https://bob.example.com/profile/card.ttl#me' + resolveClassification({ kind: 'agent', subjectValue: 'https://alice.example.com/profile/card.ttl#me' }) + await submitPromise + + expect(element.principalInputValue).toBe('https://bob.example.com/profile/card.ttl#me') + expect(element.pendingAccessGrants).toHaveLength(1) + }) + + it('does not show a stale validation error after newer input', async () => { + let resolveClassification!: (value: undefined) => void + classifyAccessControlSubject.mockImplementationOnce(() => new Promise(resolve => { + resolveClassification = resolve + })) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.principalInputValue = 'https://old.example.com/profile/card#me' + + const submitPromise = element.onSubmit(new Event('submit')) + element.principalInputValue = 'https://new.example.com/profile/card#me' + resolveClassification(undefined) + await submitPromise + + expect(element.principalInputValue).toBe('https://new.example.com/profile/card#me') + expect(element.inputError).toBe('') + }) + + it('does not clear a current validation error when an older lookup succeeds', async () => { + let resolveOldLookup!: (value: { kind: 'agent', subjectValue: string }) => void + classifyAccessControlSubject + .mockImplementationOnce(() => new Promise(resolve => { + resolveOldLookup = resolve + })) + .mockResolvedValueOnce(undefined) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.principalInputValue = 'https://old.example.com/profile/card#me' + const oldSubmitPromise = element.onSubmit(new Event('submit')) + + element.principalInputValue = 'not-a-principal' + await element.onSubmit(new Event('submit')) + expect(element.inputError).toContain('Could not verify this access target') + + resolveOldLookup({ + kind: 'agent', + subjectValue: 'https://old.example.com/profile/card#me' + }) + await oldSubmitPromise + + expect(element.inputError).toContain('Could not verify this access target') + }) + + it('uses the fallback message when a related lookup throws without a message', async () => { + load.mockRejectedValueOnce(new Error('')) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.principalInputValue = 'https://alice.example.com/profile/card.ttl#me' + + await element.onSubmit(new Event('submit')) + + expect(element.inputError).toBe('Could not verify this access target') + }) + + it('shows a related lookup failure when adding an access target directly', async () => { + load.mockRejectedValueOnce(new Error('related document failed')) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.principalInputValue = 'https://alice.example.com/profile/card.ttl#me' + + await element.onSubmit(new Event('submit')) + + expect(element.inputError).toBe('related document failed') + }) + + it('keeps the selected role for a queued grant even if the mode changes later', async () => { + let resolveClassification!: (value: { kind: 'agent', subjectValue: string }) => void + classifyAccessControlSubject.mockImplementationOnce(() => new Promise(resolve => { + resolveClassification = resolve + })) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.principalInputValue = 'https://alice.example.com/profile/card.ttl#me' + element.addAccessRoleValue = 'Editor' + + const submitPromise = element.onSubmit(new Event('submit')) + element.addAccessRoleValue = 'Viewer' + resolveClassification({ kind: 'agent', subjectValue: 'https://alice.example.com/profile/card.ttl#me' }) + await submitPromise + + expect(element.pendingAccessGrants).toEqual([ + expect.objectContaining({ + role: 'Editor' + }) + ]) + }) + + it('applies a grant plan and clears the loading state after save', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.principalInputValue = 'https://alice.example.com/profile/card.ttl#me' + element.addAccessRoleValue = 'Editor' + + await element.onSaveClick() + + expect(planGrant).toHaveBeenCalledTimes(1) + expect(planGrant).toHaveBeenCalledWith( + 'https://example.com/resource.ttl', + { + type: 'agent', + iri: 'https://alice.example.com/profile/card.ttl#me' + }, + ['Read', 'Write'] + ) + expect(applyPlan).toHaveBeenCalledTimes(1) + expect(element.submitting).toBe(false) + expect(element.pendingAccessGrants).toEqual([]) + }) + + it('does not apply changes while the effective ACL context is loading', async () => { + let resolveContext!: (context: { inherited: boolean, authorizations: [] }) => void + findEffectiveAcl.mockImplementationOnce(() => new Promise(resolve => { + resolveContext = resolve + })) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.pendingAccessGrants = [{ + subjectType: 'agent', + subjectValue: 'https://alice.example.com/profile/card.ttl#me', + role: 'Editor', + label: 'Alice' + }] + document.body.appendChild(element) + await element.updateComplete + + const saveButton = [...element.shadowRoot.querySelectorAll('solid-ui-button')] + .find((button: Element) => button.textContent?.includes('Save Changes')) as HTMLElement + expect(saveButton.hasAttribute('disabled')).toBe(true) + + const savePromise = element.onSaveClick() + await Promise.resolve() + expect(applyPlan).not.toHaveBeenCalled() + + resolveContext({ inherited: true, authorizations: [] }) + await savePromise + + expect(applyPlan).toHaveBeenCalledTimes(1) + document.body.removeChild(element) + }) + + it('shows a retry action when the effective ACL context fails to load', async () => { + findEffectiveAcl.mockRejectedValueOnce(new Error('ACL service unavailable')) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.pendingAccessGrants = [{ + subjectType: 'agent', + subjectValue: 'https://alice.example.com/profile/card.ttl#me', + role: 'Editor', + label: 'Alice' + }] + document.body.appendChild(element) + await element.updateComplete + await vi.waitFor(() => { + expect(element.accessContextError).toBe('ACL service unavailable') + }) + + const errorState = element.shadowRoot.querySelector('solid-ui-error-state') as any + expect(errorState.message).toBe('ACL service unavailable') + const retryButton = [...element.shadowRoot.querySelectorAll('solid-ui-button')] + .find((button: Element) => button.textContent?.includes('Retry')) as HTMLElement + expect(retryButton).not.toBeUndefined() + expect(applyPlan).not.toHaveBeenCalled() + + findEffectiveAcl.mockResolvedValueOnce({ inherited: true, authorizations: [] }) + retryButton.click() + await vi.waitFor(() => { + expect(element.accessContext).toBeDefined() + }) + + expect(element.accessContextError).toBe('') + expect(element.accessContext).toBeDefined() + document.body.removeChild(element) + }) + + it('passes a queued origin grant to planGrant on save', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.principalInputValue = 'www.sharonstratsianis.com' + element.addAccessRoleValue = 'Editor' + + await element.onSaveClick() + + expect(planGrant).toHaveBeenCalledWith( + 'https://example.com/resource.ttl', + { + type: 'origin', + iri: 'https://www.sharonstratsianis.com' + }, + ['Read', 'Write'] + ) + expect(applyPlan).toHaveBeenCalledTimes(1) + expect(element.pendingAccessGrants).toEqual([]) + }) + + it.each([ + { role: 'Owner', modes: ['Read', 'Write', 'Control'] }, + { role: 'Editor', modes: ['Read', 'Write'] }, + { role: 'Viewer', modes: ['Read'] }, + { role: 'Poster', modes: ['Append', 'Read'] }, + { role: 'Submitter', modes: ['Append'] } + ])('saves $role using the modes supplied by solid-logic', async ({ role, modes }) => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.principalInputValue = 'https://alice.example.com/profile/card.ttl#me' + element.addAccessRoleValue = role + + await element.onSaveClick() + + expect(planGrant).toHaveBeenCalledWith( + element.subjectUri, + { type: 'agent', iri: 'https://alice.example.com/profile/card.ttl#me' }, + modes + ) + expect(applyPlan).toHaveBeenCalledTimes(1) + expect(element.saveErrors).toEqual([]) + }) + + it('revokes a queued no-access grant instead of granting empty modes', async () => { + planRevoke.mockResolvedValue({ target: 'https://example.com/resource.ttl.acl', deletes: [], inserts: [] }) + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.pendingAccessGrants = [{ + subjectType: 'agent', + subjectValue: 'https://alice.example.com/profile/card.ttl#me', + role: 'No Access', + label: 'Alice' + }] + + await element.onSaveClick() + + expect(planGrant).not.toHaveBeenCalled() + expect(planRevoke).toHaveBeenCalledWith( + element.subjectUri, + { type: 'agent', iri: 'https://alice.example.com/profile/card.ttl#me' } + ) + expect(applyPlan).toHaveBeenCalledTimes(1) + expect(element.pendingAccessGrants).toEqual([]) + }) + + it('describes revoke failures as removing access', async () => { + planRevoke.mockRejectedValue(new Error('revoke failed')) + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.pendingAccessGrants = [{ + subjectType: 'agent', + subjectValue: 'https://alice.example.com/profile/card.ttl#me', + role: 'No Access', + label: 'Alice' + }] + document.body.appendChild(element) + + await element.onSaveClick() + await element.updateComplete + + const errors = element.shadowRoot.querySelector('solid-ui-access-control-errors') + await errors.updateComplete + expect(errors.shadowRoot.querySelector('li').textContent).toContain('Could not remove access') + document.body.removeChild(element) + }) + + it('attempts every queued change and reports only the failed changes', async () => { + const failedDuringPlanning = new Set([2, 7]) + const failedDuringApply = new Set([4, 10]) + const grants = Array.from({ length: 12 }, (_, index) => ({ + subjectType: 'agent', + subjectValue: `https://example.com/agent-${index}`, + role: 'Editor', + label: `Agent ${index}` + })) + + planGrant.mockImplementation(async (subjectUri: string, subject: { iri: string }) => { + const index = Number(subject.iri.match(/agent-(\d+)$/)?.[1]) + if (failedDuringPlanning.has(index)) { + throw new Error(`planning failed for ${index}`) + } + + return { target: `${subject.iri}.acl`, deletes: [], inserts: [] } + }) + applyPlan.mockImplementation(async (plan: { target: string }) => { + const index = Number(plan.target.match(/agent-(\d+)\.acl$/)?.[1]) + if (failedDuringApply.has(index)) { + throw new Error(`apply failed for ${index}`) + } + + return new Response('ok', { status: 200 }) + }) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.pendingAccessGrants = grants + document.body.appendChild(element) + await element.updateComplete + + await element.onSaveClick() + await element.updateComplete + + expect(planGrant).toHaveBeenCalledTimes(grants.length) + expect(applyPlan).toHaveBeenCalledTimes(grants.length - failedDuringPlanning.size) + + const errors = element.shadowRoot.querySelector('solid-ui-access-control-errors') + await errors.updateComplete + const saveErrorState = errors.shadowRoot.querySelector('solid-ui-error-state') + expect(saveErrorState).not.toBeNull() + const errorItems = [...saveErrorState.querySelectorAll('li')].map((item: Element) => item.textContent) + const errorLabels = [...saveErrorState.querySelectorAll('li .access-control-save-error-label')].map((label: Element) => label.textContent) + const errorText = errorItems.join(' ') + expect(errorLabels).toEqual(['Agent 2', 'Agent 4', 'Agent 7', 'Agent 10']) + expect(errorText.match(/Could not grant Editor/g)).toHaveLength(4) + + const saveButton = [...element.shadowRoot.querySelectorAll('solid-ui-button')] + .find((button: Element) => button.textContent?.includes('Save Changes')) as HTMLElement + expect(saveButton.hasAttribute('disabled')).toBe(true) + + document.body.removeChild(element) + }) + + it('keeps the dialog open with partial failures and shows the successful ACL change', async () => { + const successfulGrant = { + subjectType: 'agent', + subjectValue: 'https://example.com/successful-agent', + role: 'Editor', + label: 'Successful agent' + } + const failedGrant = { + subjectType: 'agent', + subjectValue: 'https://example.com/failed-agent', + role: 'Editor', + label: 'Failed agent' + } + planGrant.mockImplementation(async (_subjectUri: string, subject: { iri: string }) => { + if (subject.iri === failedGrant.subjectValue) { + throw new Error('grant failed') + } + return { target: `${subject.iri}.acl`, deletes: [], inserts: [] } + }) + findAccessGrants.mockResolvedValue([{ + agent: [successfulGrant.subjectValue], + agentGroup: [], + agentClass: [], + origin: [], + mode: ['Read', 'Write'] + }]) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.pendingAccessGrants = [successfulGrant, failedGrant] + document.body.appendChild(element) + await element.updateComplete + const close = vi.fn() + window.addEventListener('solid-ui:close-dialog', close) + + await element.onSaveClick() + await element.updateComplete + + // The refreshed authorizations replace pending drafts, so successful + // changes are visible while failed changes cannot be retried in-place. + expect(planGrant).toHaveBeenCalledTimes(2) + expect(applyPlan).toHaveBeenCalledTimes(1) + expect(applyPlan).toHaveBeenCalledWith(expect.objectContaining({ + target: `${successfulGrant.subjectValue}.acl` + })) + expect(findAccessGrants).toHaveBeenCalledWith(element.subjectUri) + expect(element.pendingAccessGrants).toEqual([]) + expect(element.accessGrants).toEqual([{ + agent: [successfulGrant.subjectValue], + agentGroup: [], + agentClass: [], + origin: [], + mode: ['Read', 'Write'] + }]) + expect(close).not.toHaveBeenCalled() + const errors = element.shadowRoot.querySelector('solid-ui-access-control-errors') + await errors.updateComplete + expect(errors.shadowRoot.querySelector('solid-ui-error-state')).not.toBeNull() + expect([...errors.shadowRoot.querySelectorAll('solid-ui-error-state li .access-control-save-error-label')].map((label: Element) => label.textContent)) + .toEqual(['Failed agent']) + expect(errors.shadowRoot.querySelector('solid-ui-error-state').getAttribute('message')) + .toContain('Close this dialog and reopen it if you want to try again.') + const saveButton = [...element.shadowRoot.querySelectorAll('solid-ui-button')] + .find((button: Element) => button.textContent?.includes('Save Changes')) as HTMLElement + expect(saveButton.hasAttribute('disabled')).toBe(true) + + document.body.removeChild(element) + }) + + it('keeps the dialog open and reports every failure when all changes fail', async () => { + const grants = [0, 1, 2].map(index => ({ + subjectType: 'agent', + subjectValue: `https://example.com/failed-agent-${index}`, + role: 'Editor', + label: `Failed agent ${index}` + })) + planGrant.mockRejectedValue(new Error('all changes failed')) + findAccessGrants.mockResolvedValue([{ + agent: ['https://example.com/current-agent'], + agentGroup: [], + agentClass: [], + origin: [], + mode: ['Read'] as AccessMode[] + }]) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.pendingAccessGrants = grants + document.body.appendChild(element) + await element.updateComplete + const close = vi.fn() + window.addEventListener('solid-ui:close-dialog', close) + + await element.onSaveClick() + await element.updateComplete + + expect(planGrant).toHaveBeenCalledTimes(grants.length) + expect(applyPlan).not.toHaveBeenCalled() + expect(findAccessGrants).toHaveBeenCalledWith(element.subjectUri) + expect(element.accessGrants).toEqual([{ + agent: ['https://example.com/current-agent'], + agentGroup: [], + agentClass: [], + origin: [], + mode: ['Read'] as AccessMode[] + }]) + expect(close).not.toHaveBeenCalled() + const errors = element.shadowRoot.querySelector('solid-ui-access-control-errors') + await errors.updateComplete + expect([...errors.shadowRoot.querySelectorAll('solid-ui-error-state li .access-control-save-error-label')].map((label: Element) => label.textContent)) + .toEqual(['Failed agent 0', 'Failed agent 1', 'Failed agent 2']) + + document.body.removeChild(element) + }) + + it('clears staged changes and explains when partial-save reconciliation fails', async () => { + const successfulGrant = { + subjectType: 'agent', + subjectValue: 'https://example.com/successful-agent', + role: 'Editor', + label: 'Successful agent' + } + const failedGrant = { + subjectType: 'agent', + subjectValue: 'https://example.com/failed-agent', + role: 'Editor', + label: 'Failed agent' + } + planGrant.mockImplementation(async (_subjectUri: string, subject: { iri: string }) => { + if (subject.iri === failedGrant.subjectValue) { + throw new Error('grant failed') + } + return { target: `${subject.iri}.acl`, deletes: [], inserts: [] } + }) + findAccessGrants.mockRejectedValue(new Error('ACL refresh failed')) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.pendingAccessGrants = [successfulGrant, failedGrant] + document.body.appendChild(element) + await element.updateComplete + + await element.onSaveClick() + await element.updateComplete + + expect(findAccessGrants).toHaveBeenCalledWith(element.subjectUri) + expect(element.pendingAccessGrants).toEqual([]) + expect(element.successfulChangesRefreshed).toBe(false) + const errors = element.shadowRoot.querySelector('solid-ui-access-control-errors') + await errors.updateComplete + expect(errors.shadowRoot.querySelector('solid-ui-error-state').getAttribute('message')) + .toContain('Some changes may have been saved, but we could not refresh the Shared with list.') + const saveButton = [...element.shadowRoot.querySelectorAll('solid-ui-button')] + .find((button: Element) => button.textContent?.includes('Save Changes')) as HTMLElement + expect(saveButton.hasAttribute('disabled')).toBe(true) + + document.body.removeChild(element) + }) + + it('closes the dialog when all changes succeed', async () => { + const grants = [0, 1].map(index => ({ + subjectType: 'agent', + subjectValue: `https://example.com/successful-agent-${index}`, + role: 'Editor', + label: `Successful agent ${index}` + })) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.pendingAccessGrants = grants + document.body.appendChild(element) + await element.updateComplete + const close = vi.fn() + window.addEventListener('solid-ui:close-dialog', close) + + await element.onSaveClick() + + expect(applyPlan).toHaveBeenCalledTimes(grants.length) + expect(close).toHaveBeenCalledTimes(1) + expect(element.pendingAccessGrants).toEqual([]) + + document.body.removeChild(element) + }) + + it('reports an unexpected outer save failure as a general error', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.pendingAccessGrants = [{ + get subjectType () { + throw new Error('unexpected save failure') + }, + subjectValue: 'https://example.com/agent', + role: 'Editor', + label: 'Agent' + }] + document.body.appendChild(element) + await element.updateComplete + const close = vi.fn() + window.addEventListener('solid-ui:close-dialog', close) + await element.onSaveClick() + await element.updateComplete + + expect(close).not.toHaveBeenCalled() + const errors = element.shadowRoot.querySelector('solid-ui-access-control-errors') + await errors.updateComplete + const errorState = errors.shadowRoot.querySelector('solid-ui-error-state') + expect(errorState).not.toBeNull() + expect(errorState.textContent).toContain('Could not save access changes') + expect([...errorState.querySelectorAll('li .access-control-save-error-label')].map((label: Element) => label.textContent)) + .toEqual(['Access changes']) + + document.body.removeChild(element) + }) + + it('persists authenticated and public access changes on save', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.accessGrants = [] + element.authenticatedAccessRoleValue = 'Editor' + element.publicAccessRoleValue = 'Viewer' + + await element.onSaveClick() + + expect(planGrant).toHaveBeenNthCalledWith( + 1, + 'https://example.com/resource.ttl', + { type: 'agentClass', iri: 'http://www.w3.org/ns/auth/acl#AuthenticatedAgent' }, + ['Read', 'Write'] + ) + expect(planGrant).toHaveBeenNthCalledWith( + 2, + 'https://example.com/resource.ttl', + { type: 'agentClass', iri: 'http://xmlns.com/foaf/0.1/Agent' }, + ['Read'] + ) + expect(applyPlan).toHaveBeenCalledTimes(2) + }) + + it('reads the selected role from the rendered combobox change event', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + document.body.appendChild(element) + await element.updateComplete + + const addAccess = element.shadowRoot.querySelector('solid-ui-access-control-add-access') + await addAccess.updateComplete + addAccess.dispatchEvent(new CustomEvent('role-changed', { + bubbles: true, + composed: true, + detail: { + role: 'Editor' + } + })) + + expect(element.addAccessRoleValue).toBe('Editor') + expect(element.authenticatedAccessRoleValue).toBe('No Access') + expect(element.publicAccessRoleValue).toBe('No Access') + + document.body.removeChild(element) + }) + + it('keeps the general access selector independent from the top selector', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.principalInputValue = 'https://alice.example.com/profile/card.ttl#me' + element.addAccessRoleValue = 'Viewer' + element.authenticatedAccessRoleValue = 'No Access' + element.publicAccessRoleValue = 'No Access' + + await element.onSubmit(new Event('submit')) + + document.body.appendChild(element) + await element.updateComplete + + const generalAccess = element.shadowRoot.querySelector('solid-ui-access-control-general-access') + await generalAccess.updateComplete + const sharedRoleSelect = generalAccess.shadowRoot.querySelector('solid-ui-combobox.access-role-select--authenticated') + sharedRoleSelect.dispatchEvent(new CustomEvent('change', { + bubbles: true, + composed: true, + detail: { + option: { + value: 'Editor', + label: 'Editor' + } + } + })) + + expect(element.addAccessRoleValue).toBe('Viewer') + expect(element.authenticatedAccessRoleValue).toBe('Editor') + expect(element.publicAccessRoleValue).toBe('No Access') + expect(element.pendingAccessGrants).toEqual([ + expect.objectContaining({ + role: 'Viewer' + }) + ]) + + document.body.removeChild(element) + }) + + it('enables save when a shared access grant is changed', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.accessGrants = [{ + agent: ['https://alice.example.com/profile/card.ttl#me'], + agentGroup: [], + agentClass: [], + origin: [], + mode: ['Read'] + }] + + document.body.appendChild(element) + await element.updateComplete + + const saveButton = [...element.shadowRoot.querySelectorAll('solid-ui-button')] + .find((button: Element) => button.textContent?.includes('Save Changes')) as HTMLElement + expect(saveButton.hasAttribute('disabled')).toBe(true) + + const sharedAccess = element.shadowRoot.querySelector('solid-ui-access-control-shared-access') + await sharedAccess.updateComplete + const grantRoleSelect = sharedAccess.shadowRoot.querySelector('solid-ui-combobox.access-grants-role--editable') + grantRoleSelect.dispatchEvent(new CustomEvent('change', { + bubbles: true, + composed: true, + detail: { option: { value: 'Editor', label: 'Editor' } } + })) + + await element.updateComplete + expect(saveButton.hasAttribute('disabled')).toBe(false) + + document.body.removeChild(element) + }) + + it('uses the default share title when no subject uri is present', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + + expect(element.getDialogTitle()).toBe('Manage Access of "this resource"') + }) + + it('hides no access in the add selector, keeps it in the authenticated selector, and exposes dokieli public roles', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + document.body.appendChild(element) + await element.updateComplete + + const addAccess = element.shadowRoot.querySelector('solid-ui-access-control-add-access') + const generalAccess = element.shadowRoot.querySelector('solid-ui-access-control-general-access') + await generalAccess.updateComplete + const authenticatedRoleSelect = generalAccess.shadowRoot.querySelector('solid-ui-combobox.access-role-select--authenticated') + const publicRoleSelect = generalAccess.shadowRoot.querySelector('solid-ui-combobox.access-role-select--public') + await addAccess.updateComplete + const addRoleSelect = addAccess.shadowRoot.querySelector('solid-ui-combobox.access-role-select--top') + + const addOptions = [...addRoleSelect.querySelectorAll('solid-ui-combobox-option')].map((option: Element) => option.textContent?.trim()) + const authenticatedOptions = [...authenticatedRoleSelect.querySelectorAll('solid-ui-combobox-option')].map((option: Element) => option.textContent?.trim()) + const publicOptions = [...publicRoleSelect.querySelectorAll('solid-ui-combobox-option')].map((option: Element) => option.textContent?.trim()) + + expect(addOptions).not.toContain('No Access') + expect(addOptions).toContain('Viewer') + expect(authenticatedOptions).toContain('No Access') + expect(authenticatedOptions).toContain('Editor') + expect(publicOptions).toEqual(['No Access', 'Viewer']) + + document.body.removeChild(element) + }) + + it('shows remove for no access in grant rows', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.accessGrants = [{ + agent: ['https://alice.example.com/profile/card.ttl#me'], + agentGroup: [], + agentClass: [], + origin: [], + mode: ['Read'] + }] + + document.body.appendChild(element) + await element.updateComplete + + const sharedAccess = element.shadowRoot.querySelector('solid-ui-access-control-shared-access') + await sharedAccess.updateComplete + const grantRoleSelect = sharedAccess.shadowRoot.querySelector('solid-ui-combobox.access-grants-role--editable') + const grantOptions = [...grantRoleSelect.querySelectorAll('solid-ui-combobox-option')].map((option: Element) => option.textContent?.trim()) + + expect(grantOptions).toContain('Remove') + expect(grantOptions).not.toContain('No Access') + + document.body.removeChild(element) + }) + + it('keeps authenticated and public grants out of the shared-access list', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.accessGrants = [ + { + agent: ['https://alice.example.com/profile/card.ttl#me'], + agentGroup: [], + agentClass: [], + origin: [], + mode: ['Read'] + }, + { + agent: [], + agentGroup: [], + agentClass: ['http://www.w3.org/ns/auth/acl#AuthenticatedAgent'], + origin: [], + mode: ['Read', 'Write'] + }, + { + agent: [], + agentGroup: [], + agentClass: ['http://xmlns.com/foaf/0.1/Agent'], + origin: [], + mode: ['Read'] + } + ] + + document.body.appendChild(element) + await element.updateComplete + + const sharedAccess = element.shadowRoot.querySelector('solid-ui-access-control-shared-access') + await sharedAccess.updateComplete + const sharedAccessLabels = [...sharedAccess.shadowRoot.querySelectorAll('.access-grants-list h3')].map((label: Element) => label.textContent?.trim()) + expect(sharedAccessLabels).toEqual(['card.ttl']) + + const searchOptions = [...sharedAccess.shadowRoot.querySelectorAll('solid-ui-combobox.access-grants-search-input solid-ui-combobox-option')] + .map((option: Element) => option.textContent?.trim()) + expect(searchOptions).toEqual(['card.ttl']) + + document.body.removeChild(element) + }) + + it.each([ + 'http://www.w3.org/ns/auth/acl#AuthenticatedAgent', + 'http://xmlns.com/foaf/0.1/Agent' + ])('retains non-general subjects from an authorization containing %s', async (generalClass) => { + const { getAccessGrantViewModels, getAuthorizationSubjectLabel } = await import('../../src/components/access-control-modal/accessGrantDrafts') + const element = document.createElement('solid-ui-access-control-modal') as any + const agent = 'https://alice.example.com/profile/card.ttl#me' + const group = 'https://example.com/group' + const agentClass = 'https://example.com/team' + element.subjectUri = 'https://example.com/resource.ttl' + element.accessGrants = [{ + agent: [agent], + agentGroup: [group], + agentClass: [generalClass, agentClass], + origin: [], + mode: ['Read'] + }] + + document.body.appendChild(element) + await element.updateComplete + await element.refreshAccessGrantLabels() + await element.updateComplete + + const expectedLabel = getAuthorizationSubjectLabel({ + agent: [agent], + agentGroup: [group], + agentClass: [agentClass], + origin: [], + mode: ['Read'] + }) + const entries = getAccessGrantViewModels(element.accessGrantDrafts).filter(({ subjects }) => subjects.length > 0) + expect(entries).toHaveLength(1) + expect(entries[0].authorization.agentClass).toEqual([agentClass]) + expect(entries[0].subjectLabel).toBe(expectedLabel) + expect(element.accessGrants[0].agentClass).toEqual([generalClass, agentClass]) + const sharedAccess = element.shadowRoot.querySelector('solid-ui-access-control-shared-access') + await sharedAccess.updateComplete + expect(sharedAccess.shadowRoot.querySelector('.access-grants-list h3').textContent.trim()).toBe(expectedLabel) + const searchOptions = [...sharedAccess.shadowRoot.querySelectorAll('solid-ui-combobox.access-grants-search-input solid-ui-combobox-option')] + .map((option: Element) => option.textContent?.trim()) + expect(searchOptions).toEqual([expectedLabel]) + + sharedAccess.searchValue = 'card.ttl' + await sharedAccess.updateComplete + expect(sharedAccess.shadowRoot.querySelector('.access-grants-list h3').textContent.trim()).toBe(expectedLabel) + + const roleSelect = sharedAccess.shadowRoot.querySelector('.access-grants-list solid-ui-combobox') + roleSelect.dispatchEvent(new CustomEvent('change', { + bubbles: true, + composed: true, + detail: { option: { value: 'Editor', label: 'Editor' } } + })) + await element.updateComplete + await element.onSaveClick() + + expect(planGrant.mock.calls.map(([, subject]) => subject)).toEqual([ + { type: 'agent', iri: agent }, + { type: 'agentGroup', iri: group }, + { type: 'agentClass', iri: agentClass } + ]) + document.body.removeChild(element) + }) + + it('loads every group document before refreshing grant labels', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.accessGrants = [{ + agent: [], + agentGroup: [ + 'https://example.com/groups/first', + 'https://example.com/groups/second#members' + ], + agentClass: [], + origin: [], + mode: ['Read'] + }] + + document.body.appendChild(element) + await element.updateComplete + load.mockClear() + + await element.refreshAccessGrantLabels() + + expect(load).toHaveBeenCalledWith(sym('https://example.com/groups/first').doc()) + expect(load).toHaveBeenCalledWith(sym('https://example.com/groups/second#members').doc()) + + document.body.removeChild(element) + }) + + it('refreshes labels for a filtered container grant', async () => { + const folder = 'https://example.com/folder/' + const authorization = { + id: `${folder}.acl#group`, + agent: [], + agentGroup: ['https://example.com/groups/team'], + agentClass: [], + origin: [], + mode: ['Read'], + accessTo: [folder], + default: [folder] + } + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = folder + element.accessContext = { resource: folder, inherited: false, authorizations: [authorization] } + element.accessGrants = [authorization] + document.body.appendChild(element) + await element.updateComplete + element.accessGrantDrafts = [{ + id: authorization.id, + authorization, + role: 'Viewer', + initialRole: 'Viewer', + subjectLabel: 'stale label', + subjects: [{ + type: 'agentGroup', + iri: authorization.agentGroup[0] + }] + }] + + await element.refreshAccessGrantLabels() + + expect(element.accessGrantDrafts[0].subjectLabel).not.toBe('stale label') + document.body.removeChild(element) + }) + + it('ignores an access context that resolves after the resource changes', async () => { + let resolveFirst!: (context: any) => void + let resolveSecond!: (context: any) => void + findEffectiveAcl + .mockImplementationOnce(() => new Promise(resolve => { resolveFirst = resolve })) + .mockImplementationOnce(() => new Promise(resolve => { resolveSecond = resolve })) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/first.ttl' + const firstLoad = element.loadAccessContext() + element.subjectUri = 'https://example.com/second.ttl' + const secondLoad = element.loadAccessContext() + + resolveSecond({ resource: 'https://example.com/second.ttl', inherited: true, authorizations: [] }) + await secondLoad + resolveFirst({ resource: 'https://example.com/first.ttl', inherited: false, authorizations: [] }) + await firstLoad + + expect(element.accessContext.resource).toBe('https://example.com/second.ttl') + }) + + it('replaces a duplicate pending principal with its latest role', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + + await element.queuePendingPrincipal('https://alice.example.com/profile/card.ttl#me', 'Viewer') + await element.queuePendingPrincipal('https://alice.example.com/profile/card.ttl#me', 'Editor') + + expect(element.pendingAccessGrants).toEqual([ + expect.objectContaining({ + subjectValue: 'https://alice.example.com/profile/card.ttl#me', + role: 'Editor' + }) + ]) + }) + + it('persists a changed shared access grant on save', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.accessGrants = [{ + agent: ['https://alice.example.com/profile/card.ttl#me'], + agentGroup: [], + agentClass: [], + origin: [], + mode: ['Read'] + }] + + document.body.appendChild(element) + await element.updateComplete + + const sharedAccess = element.shadowRoot.querySelector('solid-ui-access-control-shared-access') + await sharedAccess.updateComplete + const grantRoleSelect = sharedAccess.shadowRoot.querySelector('solid-ui-combobox.access-grants-role--editable') + grantRoleSelect.dispatchEvent(new CustomEvent('change', { + bubbles: true, + composed: true, + detail: { option: { value: 'Editor', label: 'Editor' } } + })) + + await element.onSaveClick() + + expect(planGrant).toHaveBeenCalledWith( + element.subjectUri, + { type: 'agent', iri: 'https://alice.example.com/profile/card.ttl#me' }, + ['Read', 'Write'] + ) + + document.body.removeChild(element) + }) + + it('aborts saving when principal lookup fails', async () => { + classifyAccessControlSubject.mockResolvedValueOnce(undefined) + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.principalInputValue = 'not-a-principal' + element.publicAccessRoleValue = 'Viewer' + + document.body.appendChild(element) + await element.updateComplete + + await element.onSaveClick() + + expect(planGrant).not.toHaveBeenCalled() + expect(planRevoke).not.toHaveBeenCalled() + expect(applyPlan).not.toHaveBeenCalled() + expect(element.principalInputValue).toBe('not-a-principal') + expect(element.inputError).toBe('Could not verify this access target') + + document.body.removeChild(element) + }) + + it('applies a shared row change to every subject represented by the authorization', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/resource.ttl' + element.accessGrants = [{ + agent: ['https://alice.example.com/profile/card.ttl#me'], + agentGroup: ['https://example.com/group'], + agentClass: [], + origin: [], + mode: ['Read'] + }] + + document.body.appendChild(element) + await element.updateComplete + + const sharedAccess = element.shadowRoot.querySelector('solid-ui-access-control-shared-access') + await sharedAccess.updateComplete + const grantRoleSelect = sharedAccess.shadowRoot.querySelector('solid-ui-combobox.access-grants-role--editable') + grantRoleSelect.dispatchEvent(new CustomEvent('change', { + bubbles: true, + composed: true, + detail: { option: { value: 'Editor', label: 'Editor' } } + })) + + await element.onSaveClick() + + expect(planGrant).toHaveBeenNthCalledWith( + 1, + element.subjectUri, + { type: 'agent', iri: 'https://alice.example.com/profile/card.ttl#me' }, + ['Read', 'Write'] + ) + expect(planGrant).toHaveBeenNthCalledWith( + 2, + element.subjectUri, + { type: 'agentGroup', iri: 'https://example.com/group' }, + ['Read', 'Write'] + ) + expect(planRevoke).not.toHaveBeenCalled() + expect(applyPlan).toHaveBeenCalledTimes(2) + + document.body.removeChild(element) + }) + + it('saves a file directly without an apply-to-contents prompt', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/file.ttl' + element.pendingAccessGrants = [{ + subjectType: 'agent', + subjectValue: 'https://example.com/alice#me', + role: 'Viewer', + label: 'Alice' + }] + + await element.onSaveClick() + + expect(planGrant).toHaveBeenCalledWith( + element.subjectUri, + { type: 'agent', iri: 'https://example.com/alice#me' }, + ['Read'] + ) + }) + + it('uses the common container-and-contents scope by default', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/folder/' + element.pendingAccessGrants = [{ + subjectType: 'agent', + subjectValue: 'https://example.com/alice#me', + role: 'Viewer', + label: 'Alice' + }] + + await element.onSaveClick() + + expect(planGrant).not.toHaveBeenCalled() + expect(planGrantContainer).not.toHaveBeenCalled() + expect(planGrantContainerContents).not.toHaveBeenCalled() + expect(planGrantContainerAndContents).toHaveBeenCalledWith( + element.subjectUri, + { type: 'agent', iri: 'https://example.com/alice#me' }, + ['Read'] + ) + }) + + it('uses the advanced folder-access scope when selected', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/folder/' + element.pendingAccessGrants = [{ + subjectType: 'agent', + subjectValue: 'https://example.com/alice#me', + role: 'Viewer', + label: 'Alice' + }] + + element.advancedSharingOpen = true + element.containerScope = 'folder' + await element.onSaveClick() + + expect(planGrantContainer).toHaveBeenCalledWith( + element.subjectUri, + { type: 'agent', iri: 'https://example.com/alice#me' }, + ['Read'] + ) + expect(planGrantContainerContents).not.toHaveBeenCalled() + }) + + it('uses the advanced contents-defaults scope when selected', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/folder/' + element.pendingAccessGrants = [{ + subjectType: 'agent', + subjectValue: 'https://example.com/alice#me', + role: 'Viewer', + label: 'Alice' + }] + + element.advancedSharingOpen = true + element.containerScope = 'contents' + await element.onSaveClick() + + expect(planGrantContainerContents).toHaveBeenCalledWith( + element.subjectUri, + { type: 'agent', iri: 'https://example.com/alice#me' }, + ['Read'] + ) + expect(planGrantContainer).not.toHaveBeenCalled() + }) + + it('offers advanced folder-versus-contents sharing for containers', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/folder/' + element.accessContext = { inherited: true, effectiveContainer: 'https://example.com/' } + document.body.appendChild(element) + await element.updateComplete + + element.advancedSharingOpen = true + await element.updateComplete + expect(element.shadowRoot.textContent).toContain('different access requirements') + expect(element.shadowRoot.textContent).toContain('Folder Only') + expect(element.shadowRoot.textContent).toContain('Contents Only') + + document.body.removeChild(element) + }) + + it('defaults containers to folder-and-contents without a scope summary', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/folder/' + document.body.appendChild(element) + await element.updateComplete + + expect(element.containerScope).toBe('folder+contents') + expect(element.shadowRoot.querySelector('.access-control-scope-summary')).toBeNull() + + document.body.removeChild(element) + }) + + it.each([ + ['folder+contents', ['https://example.com/both#me']], + ['folder', ['https://example.com/folder#me']], + ['contents', ['https://example.com/contents#me']] + ] as const)('shows only %s authorizations in Shared with', async (scope, expectedSubjects) => { + const folder = 'https://example.com/folder/' + const grants = [ + { agent: ['https://example.com/folder#me'], agentGroup: [], agentClass: [], origin: [], mode: ['Read'] as AccessMode[], accessTo: [folder], default: [] }, + { agent: ['https://example.com/contents#me'], agentGroup: [], agentClass: [], origin: [], mode: ['Read'] as AccessMode[], accessTo: [], default: [folder] }, + { agent: ['https://example.com/both#me'], agentGroup: [], agentClass: [], origin: [], mode: ['Read'] as AccessMode[], accessTo: [folder], default: [folder] } + ] + findEffectiveAcl.mockResolvedValue({ + resource: folder, + inherited: false, + effectiveContainer: folder, + authorizations: grants + }) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = folder + element.accessGrants = grants + document.body.appendChild(element) + await element.updateComplete + await Promise.resolve() + await element.updateComplete + element.selectContainerScope(scope) + await element.updateComplete + const sharedAccess = element.shadowRoot.querySelector('solid-ui-access-control-shared-access') as any + await sharedAccess.updateComplete + + expect(sharedAccess.accessGrantViewModels.map((entry: any) => entry.authorization.agent[0])) + .toEqual(expectedSubjects) + + document.body.removeChild(element) + }) + + it('recomputes Shared with entries immediately when Advanced changes scope', async () => { + const folder = 'https://example.com/folder/' + const grants = [ + { agent: ['https://example.com/folder#me'], agentGroup: [], agentClass: [], origin: [], mode: ['Read'] as AccessMode[], accessTo: [folder], default: [] }, + { agent: ['https://example.com/contents#me'], agentGroup: [], agentClass: [], origin: [], mode: ['Read'] as AccessMode[], accessTo: [], default: [folder] }, + { agent: ['https://example.com/both#me'], agentGroup: [], agentClass: [], origin: [], mode: ['Read'] as AccessMode[], accessTo: [folder], default: [folder] } + ] + findEffectiveAcl.mockResolvedValue({ resource: folder, inherited: false, effectiveContainer: folder, authorizations: grants }) + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = folder + element.accessGrants = grants + element.advancedSharingOpen = true + document.body.appendChild(element) + await element.updateComplete + const sharedAccess = () => element.shadowRoot.querySelector('solid-ui-access-control-shared-access') as any + await sharedAccess().updateComplete + expect(sharedAccess().accessGrantViewModels).toHaveLength(1) + + const contentsOnly = [...element.shadowRoot.querySelectorAll('solid-ui-button')] + .find((button: HTMLElement) => button.textContent?.includes('Contents Only')) as HTMLElement + contentsOnly.click() + await element.updateComplete + await sharedAccess().updateComplete + expect(sharedAccess().accessGrantViewModels).toHaveLength(1) + expect(sharedAccess().accessGrantViewModels[0].authorization.agent).toEqual(['https://example.com/contents#me']) + + document.body.removeChild(element) + }) + + it('updates the summary and save planner when a scope card is selected', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/folder/' + element.pendingAccessGrants = [{ + subjectType: 'agent', + subjectValue: 'https://example.com/alice#me', + role: 'Viewer', + label: 'Alice' + }] + document.body.appendChild(element) + await element.updateComplete + + element.advancedSharingOpen = true + await element.updateComplete + const folderOnly = [...element.shadowRoot.querySelectorAll('solid-ui-button')] + .find((button: HTMLElement) => button.textContent?.includes('Folder Only')) as HTMLElement + folderOnly.click() + await element.updateComplete + + await element.onSaveClick() + expect(planGrantContainer).toHaveBeenCalledWith( + element.subjectUri, + { type: 'agent', iri: 'https://example.com/alice#me' }, + ['Read'] + ) + + document.body.removeChild(element) + }) + + it('supports the same-access toggle in advanced mode', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/folder/' + document.body.appendChild(element) + await element.updateComplete + + element.advancedSharingOpen = true + await element.updateComplete + const toggle = element.shadowRoot.querySelector('.access-control-same-access-toggle') as HTMLButtonElement + expect(toggle.getAttribute('aria-checked')).toBe('true') + toggle.click() + await element.updateComplete + + expect(element.containerScope).toBe('folder') + document.body.removeChild(element) + }) + + it('closes advanced sharing with an accessible ghost X button', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/folder/' + document.body.appendChild(element) + await element.updateComplete + + element.advancedSharingOpen = true + await element.updateComplete + const closeButton = element.shadowRoot.querySelector('.access-control-advanced-sharing-actions solid-ui-button') as HTMLElement + expect(closeButton?.getAttribute('variant')).toBe('ghost') + expect(closeButton?.getAttribute('aria-label')).toBe('Close advanced sharing') + expect(closeButton?.getAttribute('title')).toBe('Close advanced sharing') + expect(closeButton?.querySelector('icon-lucide-x')).not.toBeNull() + expect(closeButton?.querySelector('.sr-only')?.textContent).toBe('Close') + + closeButton.click() + await element.updateComplete + expect(element.shadowRoot.querySelector('.access-control-advanced-sharing')).toBeNull() + + document.body.removeChild(element) + }) + + it('places sharing details inside Advanced', async () => { + const inherited = document.createElement('solid-ui-access-control-modal') as any + inherited.subjectUri = 'https://example.com/inherited-folder/' + inherited.accessContext = { + inherited: true, + effectiveContainer: 'https://example.com/' + } + document.body.appendChild(inherited) + await inherited.updateComplete + + inherited.advancedSharingOpen = true + await inherited.updateComplete + expect(inherited.shadowRoot.querySelector('.access-control-advanced-sharing')).not.toBeNull() + document.body.removeChild(inherited) + + const custom = document.createElement('solid-ui-access-control-modal') as any + custom.subjectUri = 'https://example.com/custom-folder/' + custom.accessContext = { + inherited: false, + effectiveContainer: 'https://example.com/', + authorizations: [] + } + document.body.appendChild(custom) + await custom.updateComplete + + custom.advancedSharingOpen = true + await custom.updateComplete + const advancedActions = custom.shadowRoot.querySelector('.access-control-advanced-sharing-actions') + expect(advancedActions).not.toBeNull() + document.body.removeChild(custom) + }) + + it.each([ + { subjectUri: 'https://example.com/file.ttl', inherited: false, protectedTarget: false }, + { subjectUri: 'https://example.com/file.ttl', inherited: true, protectedTarget: false }, + { subjectUri: 'https://example.com/folder/', inherited: false, protectedTarget: false }, + { subjectUri: 'https://example.com/folder/', inherited: true, protectedTarget: false }, + { subjectUri: 'https://example.com/protected/', inherited: false, protectedTarget: true }, + { subjectUri: 'https://example.com/protected/', inherited: true, protectedTarget: true } + ])('keeps Advanced and reset unreleased for $subjectUri (inherited: $inherited)', async ({ + subjectUri, inherited, protectedTarget + }) => { + findEffectiveAcl.mockResolvedValueOnce({ + resource: subjectUri, + inherited, + effectiveContainer: 'https://example.com/', + authorizations: [] + }) + isProtectedResource.mockReturnValue(protectedTarget) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = subjectUri + document.body.appendChild(element) + + try { + await vi.waitFor(() => { + expect(element.accessContext?.resource).toBe(subjectUri) + expect(element.accessContextLoading).toBe(false) + }) + await element.updateComplete + + expect(element.shadowRoot.querySelector('.access-control-sharing-state')).toBeNull() + expect(element.shadowRoot.querySelector('.access-control-advanced-sharing')).toBeNull() + expect(element.shadowRoot.querySelector('.access-control-same-access-toggle')).toBeNull() + expect(element.shadowRoot.querySelector('[aria-label="Reset custom sharing to default"]')).toBeNull() + expect(element.shadowRoot.textContent).not.toMatch(/Advanced sharing|Folder Only|Contents Only|Reset to default/) + expect(resetToDefault).not.toHaveBeenCalled() + } finally { + element.remove() + } + }) + + it('hides Advanced for files and protected containers without offering reset controls', async () => { + const file = document.createElement('solid-ui-access-control-modal') as any + file.subjectUri = 'https://example.com/file.ttl' + file.accessContext = { inherited: true, effectiveContainer: 'https://example.com/' } + document.body.appendChild(file) + await file.updateComplete + expect(file.shadowRoot.querySelector('.access-control-sharing-state')).toBeNull() + expect(file.shadowRoot.querySelector('solid-ui-button')?.textContent ?? '').not.toContain('Advanced') + document.body.removeChild(file) + + const protectedContainer = document.createElement('solid-ui-access-control-modal') as any + protectedContainer.subjectUri = 'https://example.com/protected/' + protectedContainer.accessContext = { inherited: true, effectiveContainer: 'https://example.com/' } + document.body.appendChild(protectedContainer) + await protectedContainer.updateComplete + protectedContainer.protectedTarget = true + await protectedContainer.updateComplete + expect(protectedContainer.shadowRoot.querySelector('.access-control-sharing-state')).toBeNull() + expect(protectedContainer.shadowRoot.querySelector('solid-ui-button')?.textContent ?? '').not.toContain('Advanced') + expect(protectedContainer.shadowRoot.querySelector('[aria-label="Reset custom sharing to default"]')).toBeNull() + document.body.removeChild(protectedContainer) + }) + + it('keeps resource sharing details inside Advanced', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/folder/file.ttl' + element.accessContext = { + inherited: false, + effectiveContainer: 'https://example.com/folder/', + authorizations: [] + } + document.body.appendChild(element) + await element.updateComplete + element.advancedSharingOpen = true + await element.updateComplete + + const generalAccess = element.shadowRoot.querySelector('solid-ui-access-control-general-access') + const customSharing = element.shadowRoot.querySelector('.access-control-sharing-state') + expect(generalAccess).not.toBeNull() + expect(customSharing).not.toBeNull() + expect(generalAccess.compareDocumentPosition(customSharing) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() + expect(element.shadowRoot.querySelector('.access-control-advanced-sharing')).not.toBeNull() + const panel = element.shadowRoot.querySelector('.access-control-sharing-state-panel') + expect(panel).not.toBeNull() + expect(panel?.querySelector('p')).not.toBeNull() + document.body.removeChild(element) + }) + + it('shows inherited sharing details inside Advanced', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/file.ttl' + element.accessContext = { + inherited: true, + effectiveContainer: 'https://example.com/folder/', + authorizations: [] + } + document.body.appendChild(element) + await element.updateComplete + element.advancedSharingOpen = true + await element.updateComplete + + const sharingState = element.shadowRoot.querySelector('.access-control-sharing-state') + expect(sharingState).not.toBeNull() + expect(sharingState?.querySelector('.access-control-advanced-sharing')).not.toBeNull() + expect(sharingState?.textContent).not.toContain('Reset to default') + document.body.removeChild(element) + }) + + it('keeps the sharing state inside Advanced when no inherited container is known', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/file.ttl' + element.accessContext = { + inherited: false, + authorizations: [] + } + document.body.appendChild(element) + await element.updateComplete + element.advancedSharingOpen = true + await element.updateComplete + + const sharingState = element.shadowRoot.querySelector('.access-control-sharing-state') + expect(sharingState?.querySelector('.access-control-advanced-sharing')).not.toBeNull() + document.body.removeChild(element) + }) + + it('explains when an inherited resource has no default container', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/file.ttl' + element.accessContext = { + inherited: true, + authorizations: [] + } + document.body.appendChild(element) + await element.updateComplete + element.advancedSharingOpen = true + await element.updateComplete + + const sharingState = element.shadowRoot.querySelector('.access-control-sharing-state') + expect(sharingState?.textContent).toContain('no inherited default is available') + expect(sharingState?.textContent).not.toContain('sharing from') + expect(sharingState?.textContent).not.toContain('Reset to default') + document.body.removeChild(element) + }) + + it('blocks edits and explains mixed container policies until Advanced is released', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/folder/' + findEffectiveAcl.mockResolvedValueOnce({ + resource: element.subjectUri, + inherited: false, + authorizations: [ + { + agent: ['https://example.com/folder-agent#me'], + agentGroup: [], + agentClass: [], + origin: [], + accessTo: [element.subjectUri], + default: [], + mode: ['Read'] + }, + { + agent: ['https://example.com/contents-agent#me'], + agentGroup: [], + agentClass: [], + origin: [], + accessTo: [], + default: [element.subjectUri], + mode: ['Read'] + } + ] + }) + document.body.appendChild(element) + await element.updateComplete + await Promise.resolve() + await element.updateComplete + + const warning = element.shadowRoot.querySelector('.access-control-mixed-policy-warning') + expect(warning?.getAttribute('role')).toBe('alert') + expect(warning?.textContent).toContain('different access settings for the folder and its contents') + expect(warning?.textContent).toContain('You can still change access for the folder or its contents separately below.') + expect(element.shadowRoot.querySelector('solid-ui-access-control-add-access')?.disabled).toBe(true) + expect(element.shadowRoot.querySelector('solid-ui-access-control-shared-access')?.disabled).toBe(true) + expect(element.shadowRoot.querySelector('solid-ui-access-control-general-access')?.disabled).toBe(true) + const saveButton = [...element.shadowRoot.querySelectorAll('solid-ui-button')] + .find((button: Element) => button.textContent?.includes('Save Changes')) as HTMLElement + expect(saveButton.hasAttribute('disabled')).toBe(true) + + await element.onSaveClick() + expect(planGrant).not.toHaveBeenCalled() + document.body.removeChild(element) + }) + + it('resets custom sharing and refreshes context and grants', async () => { + findEffectiveAcl + .mockResolvedValueOnce({ inherited: true, authorizations: [{ agent: ['https://example.com/owner#me'], agentGroup: [], agentClass: [], origin: [], mode: ['Read'] }] }) + findAccessGrants.mockResolvedValueOnce([]) + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/file.ttl' + element.accessContext = { inherited: false, effectiveContainer: 'https://example.com/folder/', authorizations: [] } + + await element.onResetToDefault() + + expect(resetToDefault).toHaveBeenCalledWith(element.subjectUri) + expect(findEffectiveAcl).toHaveBeenCalled() + expect(findAccessGrants).toHaveBeenCalledWith(element.subjectUri) + expect(element.accessContext.inherited).toBe(true) + expect(element.accessGrants[0].agent).toEqual(['https://example.com/owner#me']) + }) + + it('shows inherited effective authorizations when the resource has no direct ACL', async () => { + const inheritedAuthorization = { + agent: ['https://example.com/owner#me'], + agentGroup: [], + agentClass: [], + origin: [], + mode: ['Read'] + } + findEffectiveAcl.mockResolvedValueOnce({ + inherited: true, + effectiveContainer: 'https://example.com/folder/', + authorizations: [inheritedAuthorization] + }) + + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/file.ttl' + document.body.appendChild(element) + await element.updateComplete + await Promise.resolve() + await element.updateComplete + + expect(element.accessGrants).toEqual([inheritedAuthorization]) + const sharedAccess = element.shadowRoot.querySelector('solid-ui-access-control-shared-access') + await sharedAccess.updateComplete + expect(sharedAccess.shadowRoot.querySelectorAll('.access-grants-list h3')).toHaveLength(1) + document.body.removeChild(element) + }) + + it('reports reset errors without changing the custom state', async () => { + resetToDefault.mockRejectedValueOnce(new Error('reset failed')) + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/file.ttl' + element.accessContext = { inherited: false, authorizations: [] } + + await element.onResetToDefault() + + expect(element.accessContext.inherited).toBe(false) + expect(element.saveErrors[0]).toEqual(expect.objectContaining({ action: 'reset', message: 'reset failed' })) + }) + + it('does not offer reset for an inherited resource', async () => { + const element = document.createElement('solid-ui-access-control-modal') as any + element.subjectUri = 'https://example.com/file.ttl' + element.accessContext = { inherited: true, effectiveContainer: 'https://example.com/folder/', authorizations: [] } + document.body.appendChild(element) + await element.updateComplete + + expect(element.shadowRoot.textContent).not.toContain('Reset to default') + document.body.removeChild(element) + }) +}) diff --git a/test/components/access-control-modal/accessGrantDrafts.test.ts b/test/components/access-control-modal/accessGrantDrafts.test.ts new file mode 100644 index 000000000..a1d5a3d8f --- /dev/null +++ b/test/components/access-control-modal/accessGrantDrafts.test.ts @@ -0,0 +1,133 @@ +import { describe, expect, it } from 'vitest' +import { Authenticated, Public, type AccessMode } from 'solid-logic' +import type { AccessAuthorization } from '../../../src/components/access-control-modal/types' +import { + getAccessGrantId, + getAccessGrantIds, + getChangedAccessGrants, + getSharedAuthorization, + getAuthenticatedAccessRole, + getPublicAccessRole +} from '../../../src/components/access-control-modal/accessGrantDrafts' + +describe('access-control grant drafts', () => { + it('removes general access classes without mutating the authorization', () => { + const authorization: AccessAuthorization = { + agent: ['https://alice.example/profile#me'], + agentGroup: [], + agentClass: [Authenticated.iri, 'https://example/team', Public.iri], + origin: [], + mode: ['Read'] as const + } + + expect(getSharedAuthorization(authorization)).toEqual({ + ...authorization, + agentClass: ['https://example/team'] + }) + expect(authorization.agentClass).toEqual([Authenticated.iri, 'https://example/team', Public.iri]) + }) + + it('creates the same stable grant id regardless of subject ordering', () => { + const first: AccessAuthorization = { + agent: ['https://alice.example/profile#me', 'https://bob.example/profile#me'], + agentGroup: ['https://example/group', 'https://example/other-group'], + agentClass: [], + origin: ['https://example.test', 'https://other.example.test'], + mode: ['Read'] as AccessMode[] + } + const second = { + ...first, + agent: [...first.agent].reverse(), + agentGroup: [...first.agentGroup].reverse(), + origin: [...first.origin].reverse() + } + + expect(getAccessGrantId(first)).toBe(getAccessGrantId(second)) + }) + + it('gives duplicate shared authorizations distinct ids', () => { + const authorization: AccessAuthorization = { + agent: ['https://alice.example/profile#me'], + agentGroup: [], + agentClass: [Authenticated.iri, Public.iri], + origin: [], + mode: ['Read'] as AccessMode[] + } + + expect(getAccessGrantIds([authorization, { ...authorization, mode: ['Write'] }])).toEqual([ + 'agent:https://alice.example/profile#me', + 'agent:https://alice.example/profile#me#1' + ]) + }) + + it('uses authorization ids instead of subject order for draft identity', () => { + const first: AccessAuthorization = { + id: 'https://example.com/.acl#authorization-1', + agent: ['https://alice.example/profile#me'], + agentGroup: [], + agentClass: [], + origin: [], + mode: ['Read'] as AccessMode[] + } + const second = { ...first, id: 'https://example.com/.acl#authorization-2', mode: ['Write'] as AccessMode[] } + + expect(getAccessGrantIds([second, first])).toEqual([second.id, first.id]) + }) + + it('aggregates modes from all matching general-access authorizations', () => { + const authorizations: AccessAuthorization[] = [ + { + id: 'auth-read', + agent: [], + agentGroup: [], + agentClass: [Authenticated.iri], + origin: [], + mode: ['Read'] as AccessMode[] + }, + { + id: 'auth-write', + agent: [], + agentGroup: [], + agentClass: [Authenticated.iri], + origin: [], + mode: ['Write'] as AccessMode[] + }, + { + id: 'public-read', + agent: [], + agentGroup: [], + agentClass: [Public.iri], + origin: [], + mode: ['Read'] as AccessMode[] + } + ] + + expect(getAuthenticatedAccessRole(authorizations)).toBe('Editor') + expect(getPublicAccessRole(authorizations)).toBe('Viewer') + }) + + it('returns only changed drafts with real shared subjects', () => { + const changed = { + id: 'agent:https://alice.example/profile#me', + authorization: { + agent: ['https://alice.example/profile#me'], + agentGroup: [], + agentClass: [], + origin: [], + mode: ['Read'] as AccessMode[] + }, + role: 'Editor' as const, + initialRole: 'Viewer' as const, + subjectLabel: 'Alice', + subjects: [{ type: 'agent' as const, iri: 'https://alice.example/profile#me' }] + } + const unchanged = { ...changed, id: 'unchanged', role: 'Viewer' as const } + const generalOnly = { ...changed, id: 'general', subjects: [], role: 'Owner' as const } + + expect(getChangedAccessGrants([unchanged, generalOnly, changed])).toEqual([{ + authorization: changed.authorization, + subjects: changed.subjects, + role: 'Editor' + }]) + }) +}) diff --git a/test/components/access-control-modal/accessTargetResolver.test.ts b/test/components/access-control-modal/accessTargetResolver.test.ts new file mode 100644 index 000000000..e5aec9027 --- /dev/null +++ b/test/components/access-control-modal/accessTargetResolver.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it, vi } from 'vitest' +import { + accessPrincipalOptionsProvider, + resolveAccessTarget +} from '../../../src/components/access-control-modal/accessTargetResolver' +import { solidLogicSingleton } from 'solid-logic' +import { sym } from 'rdflib' +import ns from '../../../src/lib/ns' + +describe('access-control target resolver', () => { + it('normalizes a bare origin without requiring directory lookup', async () => { + await expect(resolveAccessTarget('example.test')).resolves.toEqual({ + subjectType: 'origin', + subjectValue: 'https://example.test', + label: 'https://example.test', + targetKind: 'origin' + }) + }) + + it('resolves a selected absolute origin without classifying it as an agent', async () => { + await expect(resolveAccessTarget('https://example.test/')).resolves.toEqual({ + subjectType: 'origin', + subjectValue: 'https://example.test', + label: 'https://example.test', + targetKind: 'origin' + }) + }) + + it('returns a non-selectable prompt for a short query', async () => { + await expect(accessPrincipalOptionsProvider('x')).resolves.toEqual([{ + label: 'Type at least 2 characters to search', + value: '', + selectable: false + }]) + }) + + it('suggests only the current user’s trusted-app origins from the profile RDF', async () => { + const store = solidLogicSingleton.store + const me = sym('https://trusted-app-options.example/profile/card#me') + const profile = me.doc() + const trustedApp = sym('https://trusted-app-options.example/apps/trusted') + const invalidApp = sym('https://trusted-app-options.example/apps/invalid') + const otherUser = sym('https://other-user.example/profile/card#me') + const otherProfile = otherUser.doc() + const unrelatedApp = sym('https://other-user.example/apps/unrelated') + + store.add(me, ns.acl('trustedApp'), trustedApp, profile) + store.add(trustedApp, ns.acl('origin'), sym('https://trusted.example.test/'), profile) + store.add(me, ns.acl('trustedApp'), invalidApp, profile) + store.add(invalidApp, ns.acl('origin'), sym('https://invalid.example.test/app'), profile) + store.add(otherUser, ns.acl('trustedApp'), unrelatedApp, otherProfile) + store.add(unrelatedApp, ns.acl('origin'), sym('https://unrelated.example.test/'), otherProfile) + + const currentUserSpy = vi.spyOn(solidLogicSingleton.authn, 'currentUser').mockReturnValue(me) + const loadProfileSpy = vi.spyOn(solidLogicSingleton.profile, 'loadProfile').mockResolvedValue(profile) + const directorySearchSpy = vi.spyOn(solidLogicSingleton.directory, 'search') + .mockRejectedValue(new Error('directory unavailable')) + + try { + const options = await accessPrincipalOptionsProvider('trusted') + + expect(loadProfileSpy).toHaveBeenCalledWith(me) + expect(directorySearchSpy).toHaveBeenCalled() + expect(options).toHaveLength(1) + expect(options[0]).toEqual(expect.objectContaining({ + label: 'Trusted app: https://trusted.example.test', + value: 'https://trusted.example.test', + targetKind: 'trustedApp' + })) + } finally { + currentUserSpy.mockRestore() + loadProfileSpy.mockRestore() + directorySearchSpy.mockRestore() + store.removeMatches(null, null, null, profile) + store.removeMatches(null, null, null, otherProfile) + } + }) + + it('preserves related document lookup failures for direct resolution', async () => { + const classifySpy = vi.spyOn(solidLogicSingleton.acl, 'classifyAccessControlSubject') + .mockResolvedValueOnce({ + kind: 'agent', + subjectValue: 'https://example.test/profile#me' + }) + const loadSpy = vi.spyOn(solidLogicSingleton.store.fetcher, 'load') + .mockRejectedValueOnce(new Error('related document failed')) + + await expect(resolveAccessTarget('https://example.test/profile#me')) + .rejects.toThrow('related document failed') + + classifySpy.mockRestore() + loadSpy.mockRestore() + }) +}) diff --git a/test/components/access-control-modal/buildAccessChanges.test.ts b/test/components/access-control-modal/buildAccessChanges.test.ts new file mode 100644 index 000000000..d9739bc71 --- /dev/null +++ b/test/components/access-control-modal/buildAccessChanges.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest' +import type { AccessMode } from 'solid-logic' +import { buildAccessChanges } from '../../../src/components/access-control-modal/buildAccessChanges' + +describe('buildAccessChanges', () => { + it('orders pending, general, and changed shared access changes', () => { + const pendingAccessGrant = { + subjectType: 'agent' as const, + subjectValue: 'https://alice.example/profile#me', + role: 'Editor' as const, + label: 'Alice' + } + const generalAccessChange = { + subject: { type: 'agentClass' as const, iri: 'http://www.w3.org/ns/auth/acl#AuthenticatedAgent' }, + role: 'Viewer' as const, + label: 'Authenticated users' + } + const changedAccessGrant = { + authorization: { + agent: [], + agentGroup: [], + agentClass: [], + origin: ['https://example.test'], + mode: ['Read'] as AccessMode[] + }, + subjects: [{ type: 'origin' as const, iri: 'https://example.test' }], + role: 'No Access' as const + } + + expect(buildAccessChanges({ + pendingAccessGrants: [pendingAccessGrant], + generalAccessChanges: [generalAccessChange], + changedAccessGrants: [changedAccessGrant] + })).toEqual([ + { + subject: { type: 'agent', iri: 'https://alice.example/profile#me' }, + role: 'Editor', + label: 'Alice' + }, + generalAccessChange, + { + subject: { type: 'origin', iri: 'https://example.test' }, + role: 'No Access', + label: 'example.test' + } + ]) + }) +}) diff --git a/test/components/compose-explorer.test.ts b/test/components/compose-explorer.test.ts new file mode 100644 index 000000000..0e1e6327c --- /dev/null +++ b/test/components/compose-explorer.test.ts @@ -0,0 +1,116 @@ +// @vitest-environment jsdom + +import { describe, expect, it } from 'vitest' +import * as paneRegistry from 'pane-registry' +import type { DataBrowserContext, PaneDefinition } from 'pane-registry' +import { solidLogicSingleton } from 'solid-logic' +import '../../src/components/file-explorer-provider' +import '../../src/components/file-explorer-header' +import { isExplorerEvent } from '../../src/lib/file-explorer/events' +import { composeExplorer } from '../../src/lib/file-explorer/compose-explorer' + +const context: DataBrowserContext = { + dom: document, + getOutliner: () => undefined, + session: { paneRegistry, store: solidLogicSingleton.store, logic: solidLogicSingleton } +} +const pane: PaneDefinition = { + name: 'test', + icon: '', + label: () => 'Test', + render: () => document.createElement('div') +} +const composition = { + context, + subjectUri: 'https://example.test/resource', + pane, + relevantPanes: [pane], + header: { paneIcon: '', menuItems: [] } +} + +describe('explorer composition', () => { + it('places the header first and pane content after it, with provider context', () => { + const provider = document.createElement('solid-ui-file-explorer-provider') + const paneElement = document.createElement('div') + composeExplorer(provider, { ...composition, paneElement }) + + expect(provider.context).toBe(context) + expect(provider.subjectUri).toBe(composition.subjectUri) + expect(provider.pane).toBe(pane) + expect(provider.relevantPanes).toBe(composition.relevantPanes) + expect(provider.firstElementChild?.localName).toBe('solid-ui-file-explorer-header') + expect(provider.lastElementChild).toBe(paneElement) + expect(paneElement.classList.contains('paneDiv')).toBe(true) + }) + + it('reuses the header and preserves content during a menu-only update', () => { + const provider = document.createElement('solid-ui-file-explorer-provider') + const paneElement = document.createElement('div') + composeExplorer(provider, { ...composition, paneElement }) + const header = provider.firstElementChild + const menuItems = [{ label: 'Other pane', action: () => {} }] + composeExplorer(provider, { ...composition, header: { paneIcon: '', menuItems } }) + + expect(provider.firstElementChild).toBe(header) + expect(provider.querySelector('solid-ui-file-explorer-header')?.menuItems).toBe(menuItems) + expect(provider.lastElementChild).toBe(paneElement) + expect(provider.children).toHaveLength(2) + }) + + it('hides the back control when the composition is a direct resource view', () => { + const provider = document.createElement('solid-ui-file-explorer-provider') + composeExplorer(provider, { + ...composition, + header: { paneIcon: '', menuItems: [], showBack: false } + }) + + expect(provider.querySelector('solid-ui-file-explorer-header')?.showBack).toBe(false) + }) + + it('hides and restores only the outer header without touching nested explorers', () => { + const outer = document.createElement('solid-ui-file-explorer-provider') + const inner = document.createElement('solid-ui-file-explorer-provider') + composeExplorer(inner, { ...composition, paneElement: document.createElement('div') }) + const innerHeader = inner.firstElementChild + const content = document.createElement('div') + content.appendChild(inner) + composeExplorer(outer, { ...composition, paneElement: content }) + + composeExplorer(outer, { ...composition, header: undefined }) + expect(outer.firstElementChild).toBe(content) + expect(inner.firstElementChild).toBe(innerHeader) + + composeExplorer(outer, composition) + expect(outer.firstElementChild?.localName).toBe('solid-ui-file-explorer-header') + expect(outer.lastElementChild).toBe(content) + expect(inner.firstElementChild).toBe(innerHeader) + }) + + it('replaces only direct pane content even when the current pane is a provider', () => { + const outer = document.createElement('solid-ui-file-explorer-provider') + const inner = document.createElement('solid-ui-file-explorer-provider') + const innerContent = document.createElement('div') + composeExplorer(inner, { ...composition, paneElement: innerContent }) + composeExplorer(outer, { ...composition, paneElement: inner }) + + const replacement = document.createElement('div') + composeExplorer(outer, { ...composition, paneElement: replacement }) + expect(outer.lastElementChild).toBe(replacement) + expect(inner.lastElementChild).toBe(innerContent) + expect(outer.contains(inner)).toBe(false) + }) + + it('identifies the nearest provider for bubbling events', () => { + const outer = document.createElement('solid-ui-file-explorer-provider') + const inner = document.createElement('solid-ui-file-explorer-provider') + const header = document.createElement('div') + inner.appendChild(header) + outer.appendChild(inner) + const owners: boolean[] = [] + inner.addEventListener('test-explorer', event => owners.push(isExplorerEvent(event, inner))) + outer.addEventListener('test-explorer', event => owners.push(isExplorerEvent(event, outer))) + + header.dispatchEvent(new CustomEvent('test-explorer', { bubbles: true, composed: true })) + expect(owners).toEqual([true, false]) + }) +}) diff --git a/test/components/error-state.test.ts b/test/components/error-state.test.ts new file mode 100644 index 000000000..e9119cd46 --- /dev/null +++ b/test/components/error-state.test.ts @@ -0,0 +1,40 @@ +// @vitest-environment jsdom + +import { describe, expect, it, vi } from 'vitest' +import '../../src/components/error-state' + +describe('solid-ui-error-state', () => { + it('renders its title, message, close button, and slotted errors', async () => { + const element = document.createElement('solid-ui-error-state') + element.setAttribute('title', 'Changes could not be synced') + element.setAttribute('message', 'Try again when your connection is restored.') + element.innerHTML = '
    • Network request failed
    ' + document.body.append(element) + + await (element as unknown as { updateComplete: Promise }).updateComplete + + expect(element.shadowRoot?.textContent).toContain('Changes could not be synced') + expect(element.shadowRoot?.textContent).toContain('Try again when your connection is restored.') + const closeButton = element.shadowRoot?.querySelector('solid-ui-button.close-button') + expect(closeButton?.getAttribute('variant')).toBe('ghost') + expect(closeButton?.textContent).toContain('Close') + expect(element.querySelector('li')?.textContent).toBe('Network request failed') + }) + + it('dispatches a close event when the close button is activated', async () => { + const element = document.createElement('solid-ui-error-state') + document.body.append(element) + + await (element as unknown as { updateComplete: Promise }).updateComplete + + const closeButton = element.shadowRoot?.querySelector('solid-ui-button.close-button') + const innerButton = closeButton?.shadowRoot?.querySelector('button') + const closeHandler = vi.fn() + element.addEventListener('close', closeHandler) + + innerButton?.click() + + expect(closeHandler).toHaveBeenCalledTimes(1) + expect(closeHandler.mock.calls[0][0]).toBeInstanceOf(CustomEvent) + }) +}) diff --git a/test/unit/acl/add-agent-buttons.test.ts b/test/unit/acl/add-agent-buttons.test.ts index 712998e14..086f88869 100644 --- a/test/unit/acl/add-agent-buttons.test.ts +++ b/test/unit/acl/add-agent-buttons.test.ts @@ -1,9 +1,10 @@ -import { beforeEach, describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { silenceDebugMessages } from '../helpers/debugger' import { AddAgentButtons } from '../../../src/acl/add-agent-buttons' import { instantiateAccessGroups } from '../helpers/instantiateAccessGroups' import { JSDOM } from 'jsdom' import { solidLogicSingleton } from 'solid-logic' +import ns from '../../../src/lib/ns' const store = solidLogicSingleton.store @@ -129,6 +130,64 @@ describe('When "Add Group" button is clicked', () => { }) }) +describe('Adding Everyone', () => { + let groupList: ReturnType + let groups: HTMLElement + let button: HTMLButtonElement + let image: HTMLImageElement + + beforeEach(() => { + groupList = instantiateAccessGroups(dom, store) + vi.spyOn(groupList.controller, 'isEditable', 'get').mockReturnValue(true) + vi.spyOn(groupList.controller, 'save').mockResolvedValue(undefined) + vi.spyOn(groupList.controller, 'render').mockReturnValue(dom.createElement('div')) + vi.spyOn(store.fetcher, 'load').mockRejectedValue(new Error('Unexpected RDF lookup')) + groups = groupList.render() + groups.querySelector('img[title="Add ..."]')!.click() + image = groups.querySelector('img[title="Add Everyone"]')! + button = image.parentElement as HTMLButtonElement + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it.each(['text/uri-list', 'text/plain'])('drags Everyone directly to Editors using %s without fetching the icon', async format => { + const payload = new Map([[format, image.src]]) + const dataTransfer = { + types: [format], + setData: (type: string, value: string) => payload.set(type, value), + getData: (type: string) => payload.get(type) || '' + } + const dragStart = new dom.defaultView!.Event('dragstart', { bubbles: true }) + Object.defineProperty(dragStart, 'dataTransfer', { value: dataTransfer }) + button.dispatchEvent(dragStart) + + expect(dataTransfer.getData(format)).toBe(ns.foaf('Agent').uri) + expect(button.draggable).toBe(true) + expect(image.draggable).toBe(false) + + const editors = Array.from(groups.children).find(group => + (group.firstElementChild as HTMLElement)?.innerText === 'Editors')! + const drop = new dom.defaultView!.Event('drop', { bubbles: true, cancelable: true }) + Object.defineProperty(drop, 'dataTransfer', { value: dataTransfer }) + editors.dispatchEvent(drop) + + await vi.waitFor(() => expect(groupList.controller.save).toHaveBeenCalledOnce()) + const editorModes = [ns.acl('Read').uri, ns.acl('Write').uri].join('\n') + expect(groupList.byCombo[editorModes]).toEqual([['agentClass', ns.foaf('Agent').uri]]) + expect(store.fetcher.load).not.toHaveBeenCalled() + }) + + it('still adds Everyone as a Viewer when clicked', async () => { + button.click() + + await vi.waitFor(() => expect(groupList.controller.save).toHaveBeenCalledOnce()) + expect(groupList.byCombo[ns.acl('Read').uri]).toEqual([['agentClass', ns.foaf('Agent').uri]]) + expect(store.fetcher.load).not.toHaveBeenCalled() + }) +}) + describe('When "Add Bot" button is clicked', () => { let bar const buttonIndex = 4 diff --git a/test/unit/codeEditor.test.js b/test/unit/codeEditor.test.js index 872978396..935f9def1 100644 --- a/test/unit/codeEditor.test.js +++ b/test/unit/codeEditor.test.js @@ -12,7 +12,10 @@ vi.mock('@codemirror/state', () => { } const EditorState = { - create: vi.fn((config) => ({ ...config })) + create: vi.fn((config) => ({ ...config })), + readOnly: { + of: vi.fn((value) => ({ type: 'readOnly', value })) + } } return { Compartment, EditorState } @@ -47,6 +50,9 @@ vi.mock('@codemirror/view', () => { EditorView.editable = { of: vi.fn((value) => ({ type: 'editable', value })) } + EditorView.contentAttributes = { + of: vi.fn((value) => ({ type: 'contentAttributes', value })) + } EditorView.theme = vi.fn((spec, options) => ({ type: 'theme', spec, options })) EditorView.lineWrapping = { type: 'lineWrapping' } EditorView.updateListener = { @@ -79,7 +85,8 @@ vi.mock('@codemirror/language', () => ({ vi.mock('@codemirror/commands', () => ({ defaultKeymap: [{ key: 'default' }], history: vi.fn(() => ({ type: 'history' })), - historyKeymap: [{ key: 'history' }] + historyKeymap: [{ key: 'history' }], + indentWithTab: { key: 'Tab', run: vi.fn() } })) vi.mock('@codemirror/lang-css', () => ({ @@ -161,7 +168,10 @@ describe('CodeEditor', () => { editor.setReadOnly(true) expect(view.dispatch).toHaveBeenLastCalledWith({ - effects: { type: 'reconfigure', value: { type: 'editable', value: false } } + effects: [ + { type: 'reconfigure', value: { type: 'editable', value: false } }, + { type: 'reconfigure', value: { type: 'readOnly', value: true } } + ] }) editor.focusEditor() @@ -187,4 +197,72 @@ describe('CodeEditor', () => { expect(onDirtyChange).toHaveBeenCalledTimes(1) expect(onDirtyChange).toHaveBeenCalledWith(true) }) + + it('keeps Tab in the editor for indentation', async () => { + const editor = new CodeEditor() + const container = document.createElement('div') + document.body.appendChild(container) + + await editor.initialize(container, 'first', 'text/plain') + + const keymapExtension = createdViews[0].state.extensions.find( + extension => extension.type === 'keymap' + ) + expect(keymapExtension.value[0]).toMatchObject({ key: 'Tab' }) + }) + + it('exposes an accessible name and description on the editing surface', async () => { + const editor = new CodeEditor() + const container = document.createElement('div') + document.body.appendChild(container) + + await editor.initialize( + container, + 'generated content', + 'text/n3', + 'dark', + undefined, + { + label: 'Generated N3 for https://pod.example/card', + description: 'Generated N3 serialization. This content is read-only.' + } + ) + + const attributes = createdViews[0].state.extensions.find( + extension => extension.type === 'compartment-of' && + extension.value.type === 'contentAttributes' + ).value + expect(attributes.value).toEqual({ + 'aria-label': 'Generated N3 for https://pod.example/card', + 'aria-description': 'Generated N3 serialization. This content is read-only.' + }) + }) + + it('updates the accessible description when editor mode changes', async () => { + const editor = new CodeEditor() + const container = document.createElement('div') + document.body.appendChild(container) + + await editor.initialize(container, 'content', 'text/plain', 'dark', undefined, { + label: 'Source editor for https://pod.example/file.txt', + description: 'Read-only source content.' + }) + editor.setAccessibility({ + label: 'Source editor for https://pod.example/file.txt', + description: 'Editable source content.' + }) + + expect(createdViews[0].dispatch).toHaveBeenLastCalledWith({ + effects: { + type: 'reconfigure', + value: { + type: 'contentAttributes', + value: { + 'aria-label': 'Source editor for https://pod.example/file.txt', + 'aria-description': 'Editable source content.' + } + } + } + }) + }) }) diff --git a/test/unit/file-explorer/pane-menu-items.test.ts b/test/unit/file-explorer/pane-menu-items.test.ts new file mode 100644 index 000000000..6bab6b6d6 --- /dev/null +++ b/test/unit/file-explorer/pane-menu-items.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it, vi } from 'vitest' +import type { DataBrowserContext } from 'pane-registry' +import { sym } from 'rdflib' +import { + preparePaneMenuItems, + type PaneIconResolver, + type PaneMenuPane +} from '../../../src/lib/file-explorer/pane-menu-items' + +const subject = sym('https://example.test/resource') +const context = {} as DataBrowserContext + +function createPane (name: string, icon: string | PaneIconResolver): PaneMenuPane { + return { + name, + icon, + label: () => name, + render: () => document.createElement('div') + } +} + +describe('preparePaneMenuItems', () => { + it('prepares labeled pane actions in caller-provided order', async () => { + const openPane = vi.fn() + const panes = [createPane('first', 'first.svg'), createPane('second', 'second.svg')] + + const items = await preparePaneMenuItems({ subject, context, panes, openPane }) + + expect(items.map(item => item.label)).toEqual(['first', 'second']) + expect(items.map(item => item.icon)).toEqual(['first.svg', 'second.svg']) + + const event = new Event('select') + items[1].action(event) + expect(openPane).toHaveBeenCalledWith(subject, 'second', event) + }) + + it('skips panes without labels and resolves function-valued icons', async () => { + const iconResolver = vi.fn(() => 'resolved.svg') + const panes = [ + { ...createPane('hidden', 'hidden.svg'), label: () => null }, + { ...createPane('visible', iconResolver), label: () => 'Visible' } + ] + + const items = await preparePaneMenuItems({ + subject, + context, + panes, + openPane: vi.fn() + }) + + expect(items).toHaveLength(1) + expect(items[0].label).toBe('Visible') + expect(items[0].icon).toBe('resolved.svg') + expect(iconResolver).toHaveBeenCalledWith(subject, context) + }) + + it('uses a host icon override without changing pane ordering', async () => { + const override = vi.fn(() => 'profile.svg') + const pane = createPane('profile', 'default.svg') + + const items = await preparePaneMenuItems({ + subject, + context, + panes: [pane], + openPane: vi.fn(), + iconOverrides: { profile: override } + }) + + expect(items[0].icon).toBe('profile.svg') + expect(override).toHaveBeenCalledWith(subject, context) + }) +}) diff --git a/test/unit/file-explorer/relevant-panes.test.ts b/test/unit/file-explorer/relevant-panes.test.ts new file mode 100644 index 000000000..89ff4e301 --- /dev/null +++ b/test/unit/file-explorer/relevant-panes.test.ts @@ -0,0 +1,180 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { DataBrowserContext, PaneDefinition } from 'pane-registry' +import { sym } from 'rdflib' +import { solidLogicSingleton } from 'solid-logic' +import { filterAvailablePanes } from '../../../src/login/login' +import ns from '../../../src/lib/ns' +import { getPreferredPane, getRelevantPanes } from '../../../src/lib/file-explorer/relevant-panes' + +vi.mock('../../../src/login/login', () => ({ + filterAvailablePanes: vi.fn() +})) + +const subject = sym('https://example.test/resource') + +function createPane (name: string, properties: Partial = {}): PaneDefinition { + return { + name, + icon: '', + label: () => name, + render: () => document.createElement('div'), + ...properties + } +} + +function createContext (panes: PaneDefinition[]): DataBrowserContext { + return { + dom: document, + getOutliner: () => undefined, + session: { + logic: solidLogicSingleton, + store: solidLogicSingleton.store, + paneRegistry: { + list: panes, + paneForIcon: {}, + paneForPredicate: {}, + register: vi.fn(), + byName: name => panes.find(pane => pane.name === name) || null + } + } + } +} + +describe('getRelevantPanes', () => { + beforeEach(() => { + vi.mocked(filterAvailablePanes).mockReset() + vi.mocked(filterAvailablePanes).mockImplementation(async panes => panes) + }) + + it('preserves registry order when all relevant panes are available', async () => { + const panes = [createPane('first'), createPane('second')] + + expect(await getRelevantPanes(subject, createContext(panes))).toEqual(panes) + expect(filterAvailablePanes).toHaveBeenCalledWith(panes) + }) + + it('preserves the first relevant pane when audience filtering removes every pane', async () => { + const first = createPane('first') + const second = createPane('second') + const fallback = createPane('internal', { label: () => null }) + vi.mocked(filterAvailablePanes).mockResolvedValue([]) + + expect(await getRelevantPanes(subject, createContext([first, second, fallback]), { + fallbackPaneName: 'internal' + })).toEqual([first]) + }) + + it('prepends the first relevant pane when audience filtering removes it', async () => { + const first = createPane('first') + const second = createPane('second') + const third = createPane('third') + vi.mocked(filterAvailablePanes).mockResolvedValue([second, third]) + + expect(await getRelevantPanes(subject, createContext([first, second, third]))).toEqual([ + first, second, third + ]) + }) + + it('does not restore other audience-filtered panes or duplicate the first pane', async () => { + const first = createPane('first') + const second = createPane('second') + const third = createPane('third') + vi.mocked(filterAvailablePanes).mockResolvedValue([first, third]) + + expect(await getRelevantPanes(subject, createContext([first, second, third]))).toEqual([ + first, third + ]) + }) + + it('does not restore global, excluded, or nonmatching panes as the first relevant pane', async () => { + const global = createPane('global', { global: true }) + const excluded = createPane('sharing') + const nonmatching = createPane('nonmatching', { label: () => null }) + const first = createPane('first') + const second = createPane('second') + const context = createContext([global, excluded, nonmatching, first, second]) + vi.mocked(filterAvailablePanes).mockResolvedValue([]) + + expect(await getRelevantPanes(subject, context, { + excludedPaneNames: ['sharing'] + })).toEqual([first]) + expect(filterAvailablePanes).toHaveBeenCalledWith([first, second]) + }) + + it('uses the configured fallback without audience filtering when no pane is relevant', async () => { + const fallback = createPane('internal', { label: () => null }) + + expect(await getRelevantPanes(subject, createContext([fallback]), { + fallbackPaneName: 'internal' + })).toEqual([fallback]) + expect(filterAvailablePanes).not.toHaveBeenCalled() + }) + + it('returns an empty list when no pane is relevant and no fallback is configured', async () => { + expect(await getRelevantPanes(subject, createContext([]))).toEqual([]) + expect(filterAvailablePanes).not.toHaveBeenCalled() + }) + + it('returns an empty list when the configured fallback is not registered', async () => { + expect(await getRelevantPanes(subject, createContext([]), { + fallbackPaneName: 'missing' + })).toEqual([]) + expect(filterAvailablePanes).not.toHaveBeenCalled() + }) + + it('propagates audience-filtering errors instead of hiding them with a fallback', async () => { + const error = new Error('Unable to load audience roles') + vi.mocked(filterAvailablePanes).mockRejectedValue(error) + + await expect(getRelevantPanes(subject, createContext([createPane('first')]))).rejects.toBe(error) + }) +}) + +describe('getPreferredPane', () => { + function contextWithMintClass (mintClass?: string): DataBrowserContext { + const store = Object.create(solidLogicSingleton.store) + store.any = vi.fn((candidate, predicate) => { + if (mintClass && candidate.sameTerm(subject) && predicate.sameTerm(ns.ui('mintClass'))) { + return sym(mintClass) + } + return undefined + }) + return { + dom: document, + getOutliner: () => undefined, + session: { + logic: solidLogicSingleton, + store, + paneRegistry: { + list: [], + paneForIcon: {}, + paneForPredicate: {}, + register: vi.fn(), + byName: () => null + } + } + } + } + + it('prefers the pane matching a resource mint class over a focus request', () => { + const mintClass = sym('https://schema.example/PhotoAlbum') + const focus = createPane('focus', { shouldGetFocus: () => true }) + const minted = createPane('album', { mintClass }) + + expect(getPreferredPane(subject, contextWithMintClass(mintClass.uri), [focus, minted])).toBe(minted) + }) + + it('uses a focus request when no relevant pane matches the mint class', () => { + const focus = createPane('focus', { shouldGetFocus: () => true }) + const other = createPane('other', { mintClass: sym('https://schema.example/Other') }) + + expect(getPreferredPane(subject, contextWithMintClass('https://schema.example/PhotoAlbum'), [other, focus])).toBe(focus) + }) + + it('falls back to registry order when no pane requests focus', () => { + const first = createPane('first') + const second = createPane('second') + + expect(getPreferredPane(subject, contextWithMintClass(), [first, second])).toBe(first) + }) +}) diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 89a6d9818..eb4fc5657 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -12,10 +12,14 @@ describe('Index', () => { 'CodeEditor', 'DEFAULT_AUTH_CONTEXT', 'DEFAULT_DIALOG_CONTEXT', + 'DEFAULT_FILE_EXPLORER_CONTEXT', 'DEFAULT_SIGNUP_URL', 'Dialog', 'DialogComponent', 'DialogTrait', + 'FILE_EXPLORER_BACK_EVENT', + 'FILE_EXPLORER_MANAGE_ACCESS_EVENT', + 'FILE_EXPLORER_OPEN_PANE_EVENT', 'FormControlComponent', 'FormControlTrait', 'NoopAuth', @@ -25,6 +29,7 @@ describe('Index', () => { 'acl', 'aclControl', 'authContext', + 'composeExplorer', 'create', 'createTypes', 'customElement', @@ -32,10 +37,13 @@ describe('Index', () => { 'dom', 'fileExplorerContext', 'generateId', + 'getPreferredPane', + 'getRelevantPanes', 'icons', 'infiniteMessageArea', 'initFooter', 'initHeader', + 'isExplorerEvent', 'language', 'log', 'login', @@ -46,6 +54,7 @@ describe('Index', () => { 'pad', 'participation', 'preferences', + 'preparePaneMenuItems', 'showDialog', 'style', 'table', diff --git a/test/unit/widgets/buttons.test.ts b/test/unit/widgets/buttons.test.ts index 71bb1a640..41b9dff2d 100644 --- a/test/unit/widgets/buttons.test.ts +++ b/test/unit/widgets/buttons.test.ts @@ -123,6 +123,94 @@ describe('attachmentList', () => { expect(attachmentList(dom, subject, div, options)).toBeTruthy() }) + it('keeps delete confirmations inside the row by default', () => { + const subject = sym('https://subject.example/profile/card#me') + const target = sym('https://friend.example/profile/card#me') + store.add(subject, ns.foaf('knows'), target, subject.doc()) + const list = attachmentList(dom, subject, element, { predicate: ns.foaf('knows') }) + const remove = list.querySelector('[title="Remove this attachment"]')! + remove.click() + + const reference = remove.previousElementSibling as HTMLElement + expect(reference.parentElement).toBe(remove.parentElement) + expect(reference.style.position).toBe('relative') + expect((reference.firstElementChild as HTMLElement).style.position).toBe('absolute') + expect(getByRole(reference, 'button', { name: 'Remove this attachment' })).toBeTruthy() + }) + + it('places confirmations in the optional container, including after a refresh', () => { + dom.body.appendChild(element) + const subject = sym('https://subject.example/profile/card#me') + const target = sym('https://friend.example/profile/card#me') + const host = dom.body.appendChild(dom.createElement('div')) + const sentinel = host.appendChild(dom.createElement('p')) + store.add(subject, ns.foaf('knows'), target, subject.doc()) + const list = attachmentList(dom, subject, element, { + predicate: ns.foaf('knows'), + noun: 'friend', + confirmationContainer: host, + renderSupportingInfo: () => null, + refreshOnDocumentLoad: false + }) + const open = () => list.querySelector('[title="Remove this friend"]')!.click() + open() + open() + + expect(host.querySelectorAll('[role="dialog"]')).toHaveLength(1) + const dialog = host.querySelector('[role="dialog"]')! + expect(dialog.parentElement).toBe(host) + expect(host.contains(sentinel)).toBe(true) + expect(list.contains(dialog)).toBe(false) + expect((dialog.firstElementChild as HTMLElement).style.position).toBe('static') + const cancel = Array.from(dialog.querySelectorAll('button')) + .find(button => button.textContent === 'Cancel')! + expect(dom.activeElement).toBe(cancel) + cancel.click() + expect(host.querySelector('[role="dialog"]')).toBeNull() + expect(store.holds(subject, ns.foaf('knows'), target)).toBe(true) + expect(dom.activeElement).toBe(list.querySelector('[title="Remove this friend"]')) + + refreshTree(list) + open() + expect(host.querySelectorAll('[role="dialog"]')).toHaveLength(1) + }) + + it('restores focus after asynchronous deletion and when the list becomes empty', async () => { + dom.body.appendChild(element) + const host = dom.body.appendChild(dom.createElement('div')) + const subject = sym('https://subject.example/profile/card#me') + const targets = [ + sym('https://first.example/profile/card#me'), + sym('https://second.example/profile/card#me') + ] + const predicate = ns.foaf('knows') + targets.forEach(target => store.add(subject, predicate, target, subject.doc())) + const update = vi.spyOn(store.updater!, 'update').mockImplementation(() => Promise.resolve()) + const list = attachmentList(dom, subject, element, { + predicate, + confirmationContainer: host, + renderSupportingInfo: () => null, + refreshOnDocumentLoad: false + }) + + for (const target of targets) { + const remove = list.querySelector('[data-testid="deleteButtonWithCheck"]')! + remove.click() + getByRole(host, 'button', { name: 'Remove this attachment' }).click() + expect(host.querySelector('[role="dialog"]')).toBeNull() + expect(dom.activeElement).toBe(remove) + + const callback = update.mock.calls[update.mock.calls.length - 1][2]! + store.removeMatches(subject, predicate, target) + callback(subject.doc().uri, true, '') + await Promise.resolve() + + const remainingRemove = list.querySelector('[data-testid="deleteButtonWithCheck"]') + expect(dom.activeElement).toBe(remainingRemove || list.querySelector('button')) + expect(dom.activeElement?.isConnected).toBe(true) + } + }) + it('refreshes rows after pending profile fetches by default', () => { const subject = sym('https://subject.example/profile/card#me') const predicate = ns.foaf('knows') @@ -324,6 +412,83 @@ describe('deleteButtonWithCheck', () => { const deleteFunction = () => {} expect(deleteButtonWithCheck(dom, container, noun, deleteFunction)).toBeTruthy() }) + + it.each([false, true])('preserves cancel and confirm handlers with an external container: %s', external => { + const host = external ? dom.createElement('div') : undefined + const deleteFunction = vi.fn() + const remove = deleteButtonWithCheck(dom, element, 'friend', deleteFunction, host) + const popupContainer = host || element + remove.click() + const cancel = Array.from(popupContainer.querySelectorAll('button')) + .find(button => button.textContent === 'Cancel')! + cancel.click() + expect(popupContainer.querySelector('button')).toBeNull() + expect(deleteFunction).not.toHaveBeenCalled() + + remove.click() + const popup = external ? host! : remove.previousElementSibling as HTMLElement + getByRole(popup, 'button', { name: 'Remove this friend' }).click() + expect(deleteFunction).toHaveBeenCalledTimes(1) + expect(popupContainer.querySelector('button')).toBeNull() + }) + + it.each(['text', 'icon'])('restores focus to the originating control after %s cancellation', control => { + dom.body.appendChild(element) + const host = dom.body.appendChild(dom.createElement('div')) + const deleteFunction = vi.fn() + const remove = deleteButtonWithCheck(dom, element, 'friend', deleteFunction, host) + remove.focus() + expect(dom.activeElement).toBe(remove) + expect(getByRole(element, 'button', { name: 'Remove this friend' })).toBe(remove) + + remove.click() + const cancel = Array.from(host.querySelectorAll('button')) + .find(button => button.textContent === 'Cancel')! + expect(dom.activeElement).toBe(cancel) + if (control === 'text') { + cancel.click() + } else { + host.querySelector('button')!.click() + } + + expect(host.querySelector('[role="dialog"]')).toBeNull() + expect(dom.activeElement).toBe(remove) + expect(deleteFunction).not.toHaveBeenCalled() + }) + + it.each(['Enter', ' '])('opens confirmation with the %s key', key => { + dom.body.appendChild(element) + const host = dom.body.appendChild(dom.createElement('div')) + const remove = deleteButtonWithCheck(dom, element, 'friend', vi.fn(), host) + remove.focus() + remove.dispatchEvent(new window.KeyboardEvent('keydown', { key, bubbles: true })) + + const cancel = Array.from(host.querySelectorAll('button')) + .find(button => button.textContent === 'Cancel')! + expect(dom.activeElement).toBe(cancel) + }) + + it.each(['first', 'last'])('focuses a surviving control after deleting the %s row', position => { + dom.body.appendChild(element) + const host = dom.body.appendChild(dom.createElement('div')) + const firstRow = element.appendChild(dom.createElement('div')) + const lastRow = element.appendChild(dom.createElement('div')) + const firstRemove = deleteButtonWithCheck(dom, firstRow, 'friend', () => firstRow.remove(), host) + const lastRemove = deleteButtonWithCheck(dom, lastRow, 'friend', () => lastRow.remove(), host) + const remove = position === 'first' ? firstRemove : lastRemove + const survivor = position === 'first' ? lastRemove : firstRemove + + remove.click() + if (position === 'first') { + getByRole(host, 'button', { name: 'Remove this friend' }).click() + } else { + host.querySelector('img[title="Delete it"]')!.parentElement!.click() + } + + expect(host.querySelector('[role="dialog"]')).toBeNull() + expect(remove.isConnected).toBe(false) + expect(dom.activeElement).toBe(survivor) + }) }) describe('extractLogURI', () => { diff --git a/test/unit/widgets/buttons/iconLink.test.ts b/test/unit/widgets/buttons/iconLink.test.ts index 13c5d2fdd..a3eba1ec7 100644 --- a/test/unit/widgets/buttons/iconLink.test.ts +++ b/test/unit/widgets/buttons/iconLink.test.ts @@ -28,6 +28,29 @@ describe('linkIcon', () => { const iconURI = '' expect(linkIcon(dom, subject, iconURI)).toBeTruthy() }) + + it('renders the shared Lucide component with a neutral color and accessible link name', () => { + const anchor = linkIcon(dom, obj) + const icon = anchor.querySelector('icon-lucide-square-arrow-out-up-right')! + expect(icon).not.toBeNull() + expect(icon.getAttribute('aria-hidden')).toBe('true') + expect(icon.style.width).toBe('1em') + expect(icon.style.height).toBe('1em') + expect(icon.style.margin).toBe('0.2em') + expect(icon.style.color).toBe('var(--color-text-secondary, #4A5565)') + expect(anchor.getAttribute('href')).toBe(obj.value) + expect(anchor.getAttribute('target')).toBe('_blank') + expect(anchor.getAttribute('aria-label')).toBe('Open resource') + expect(anchor.querySelector('img')).toBeNull() + }) + + it('preserves explicitly supplied image icons and non-HTTP link behavior', () => { + const anchor = linkIcon(dom, new NamedNode('mailto:friend@example.com'), 'https://example.com/custom.svg') + expect(anchor.querySelector('img')?.getAttribute('src')).toBe('https://example.com/custom.svg') + expect(anchor.querySelector('img')?.style.margin).toBe('0.3em') + expect(anchor.querySelector('icon-lucide-square-arrow-out-up-right')).toBeNull() + expect(anchor.hasAttribute('target')).toBe(false) + }) }) describe('createLinkForURI', () => { diff --git a/test/unit/widgets/forms/autocomplete/__snapshots__/autocomplete.test.ts.snap b/test/unit/widgets/forms/autocomplete/__snapshots__/autocomplete.test.ts.snap index 903baa0e4..2adf7068a 100644 --- a/test/unit/widgets/forms/autocomplete/__snapshots__/autocomplete.test.ts.snap +++ b/test/unit/widgets/forms/autocomplete/__snapshots__/autocomplete.test.ts.snap @@ -99,10 +99,13 @@ exports[`autocompleteField > clicking on row of greenn table then accecpt button class="hoverControl" > @@ -190,10 +193,13 @@ exports[`autocompleteField > creates a autocomplete field 1`] = ` class="hoverControl" > @@ -316,10 +322,13 @@ exports[`autocompleteField > typing more search term till unique selects the who class="hoverControl" >