diff --git a/.changeset/frontend-product-sdk.md b/.changeset/frontend-product-sdk.md new file mode 100644 index 0000000..880ae46 --- /dev/null +++ b/.changeset/frontend-product-sdk.md @@ -0,0 +1,5 @@ +--- +"@dotdm/env": patch +--- + +Add a lightweight registry export for frontend consumers. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 231f7be..37250df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,20 +16,23 @@ catalogs: specifier: ^2.0.1 version: 2.0.1 '@parity/product-sdk-bulletin': - specifier: ^0.4.0 - version: 0.4.0 + specifier: ^0.4.1 + version: 0.4.1 + '@parity/product-sdk-chain-client': + specifier: ^0.4.1 + version: 0.4.1 '@parity/product-sdk-contracts': - specifier: ^0.4.0 - version: 0.4.0 + specifier: ^0.5.0 + version: 0.5.0 '@parity/product-sdk-descriptors': specifier: ^0.4.0 version: 0.4.0 '@parity/product-sdk-host': - specifier: ^0.2.2 - version: 0.2.2 + specifier: ^0.3.0 + version: 0.3.0 '@parity/product-sdk-tx': - specifier: ^0.2.2 - version: 0.2.2 + specifier: ^0.2.3 + version: 0.2.3 '@polkadot-api/sdk-ink': specifier: ^0.7.0 version: 0.7.0 @@ -157,7 +160,7 @@ importers: version: link:../../lib/utils '@parity/product-sdk-contracts': specifier: 'catalog:' - version: 0.4.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2)(typescript@5.9.3)(zod@4.3.6) + version: 0.5.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2)(typescript@5.9.3)(zod@4.3.6) commander: specifier: 'catalog:' version: 12.1.0 @@ -198,21 +201,27 @@ importers: '@dotdm/utils': specifier: workspace:* version: link:../../lib/utils + '@parity/product-sdk-bulletin': + specifier: 'catalog:' + version: 0.4.1(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) + '@parity/product-sdk-chain-client': + specifier: 'catalog:' + version: 0.4.1(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) '@parity/product-sdk-contracts': specifier: 'catalog:' - version: 0.4.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2)(typescript@5.9.3)(zod@4.3.6) + version: 0.5.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2)(typescript@5.9.3)(zod@4.3.6) '@parity/product-sdk-descriptors': specifier: 'catalog:' version: 0.4.0(esbuild@0.27.3)(rxjs@7.8.2) + '@parity/product-sdk-host': + specifier: 'catalog:' + version: 0.3.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) dompurify: specifier: 'catalog:' version: 3.3.1 marked: specifier: 'catalog:' version: 17.0.3 - polkadot-api: - specifier: 'catalog:' - version: 2.1.2(esbuild@0.27.3)(rxjs@7.8.2) react: specifier: 'catalog:' version: 19.2.4 @@ -304,16 +313,16 @@ importers: version: 2.0.1 '@parity/product-sdk-bulletin': specifier: 'catalog:' - version: 0.4.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) + version: 0.4.1(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) '@parity/product-sdk-contracts': specifier: 'catalog:' - version: 0.4.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2)(typescript@5.9.3)(zod@4.3.6) + version: 0.5.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2)(typescript@5.9.3)(zod@4.3.6) '@parity/product-sdk-descriptors': specifier: 'catalog:' version: 0.4.0(esbuild@0.27.3)(rxjs@7.8.2) '@parity/product-sdk-tx': specifier: 'catalog:' - version: 0.2.2(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) + version: 0.2.3(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) multiformats: specifier: 'catalog:' version: 13.4.2 @@ -341,7 +350,7 @@ importers: version: 0.4.0(esbuild@0.27.3)(rxjs@7.8.2) '@parity/product-sdk-host': specifier: 'catalog:' - version: 0.2.2(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) + version: 0.3.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) '@polkadot-labs/hdkd': specifier: 'catalog:' version: 0.0.26 @@ -1092,14 +1101,14 @@ packages: '@parity/product-sdk-address@0.1.1': resolution: {integrity: sha512-sSymun3alNGdvawhdc0Ha0KEkuqMwBZui1bsUVeZIZRJAfWvQzrV1AVaf8aah5JFlcaRdg8FYyp7xL2eP+ZplA==} - '@parity/product-sdk-bulletin@0.4.0': - resolution: {integrity: sha512-R/Y2S2Wh5VXSezpU/u1DIr7c4vH6PKg+c3sdBWGHN2dztQA0LdtbFDIcD63+PqaT21E0F+a4SDWIo8/7zaXeNw==} + '@parity/product-sdk-bulletin@0.4.1': + resolution: {integrity: sha512-PzX6B1XAoMHL/hBaiL1snWWv5sZ8RGBxgulbjVNKBjC8Yn9WMWVCgjGXqnLh73iRSDffqjEJ/jtfmKICnwRiDg==} - '@parity/product-sdk-chain-client@0.4.0': - resolution: {integrity: sha512-KpRYXN/ZPFFI/HK9QMrJUn19NfTMQso8K/InnBdsmZR43epRrF3G90z+SMOtTk+OC3QmpBLb6GG9GsqAItz4Og==} + '@parity/product-sdk-chain-client@0.4.1': + resolution: {integrity: sha512-Scqb1YKKfCmluI+dyGnOjKJYGI39wdm+evr+5v1+knsqOYGc8UujIY8Ju1otfm43lmRnnroi751RYtHY7kCLhQ==} - '@parity/product-sdk-contracts@0.4.0': - resolution: {integrity: sha512-D12u3c/tg7r1J2J2w6tSKtiiC7PKxuiFQ3UZucLL6Pehmpfk/czSnPrlvwvbJdpTo1V9ckG83NqR1B1Boqiipw==} + '@parity/product-sdk-contracts@0.5.0': + resolution: {integrity: sha512-6iIOEvFPgIsTq4W6N9tlxvGEax1d8+2RJ0FHM/Hy0zadhYMs8jECTdu58tul5wiWGTydqSksG13jS1kztv/Ygg==} '@parity/product-sdk-crypto@0.1.1': resolution: {integrity: sha512-No6AyTLw1Nv3ym8SDdXh/tnezdClNOL9pJgaciVr9Ny6hIL5rs6MQiXsP0+1bc1Nwymz5Q4FqsYg/htE4lejNg==} @@ -1107,8 +1116,8 @@ packages: '@parity/product-sdk-descriptors@0.4.0': resolution: {integrity: sha512-ckFpRjUVXAlA34ei9gn8XmoguGNwMBsvBuq2A4l+Mlk3fC4tzZxrFrLuEbWfLIyrW20BVGRZ32J8ml5zwu53Eg==} - '@parity/product-sdk-host@0.2.2': - resolution: {integrity: sha512-MUHu9FB/7i/pRakhYULzAzcYISjn0nIlVa7YI9ioqMWlaFBwsmWyaC6RaDNOtDFJyW58z6MwTJyx8FdVUMAW0w==} + '@parity/product-sdk-host@0.3.0': + resolution: {integrity: sha512-c4nmD1VQqMlbzqF4vHBRB0YQWLWvmdIS2QgchcDCvlGQbxiL/iz9iLMW9Mz34K8BzHea0bc0mtpYH9HDeYYUZA==} peerDependencies: '@novasamatech/host-api': '>=0.6.0' '@novasamatech/product-sdk': '>=0.1.0' @@ -1118,20 +1127,20 @@ packages: '@novasamatech/product-sdk': optional: true - '@parity/product-sdk-keys@0.2.2': - resolution: {integrity: sha512-1+dvQlBrjCC5nTntwtkKX/hTvL4oBCchTpAnddeSdQZVcsrgn6jHIePfhKpMOA33BLXPEzS0XYC7LggHIAJ7Mw==} + '@parity/product-sdk-keys@0.2.3': + resolution: {integrity: sha512-TdfUEIb+kWtnVOJQZe4AYYAtudfHTpPNP/3rWhHW5SUNvpH7ZEh5WD4MtB8PfyPqlt6DikLkHtiaZFJKTQ8kVw==} '@parity/product-sdk-logger@0.1.1': resolution: {integrity: sha512-AiSV3TTNlMZJftLQsO78BZsEymGFuJtGMSpGrJ+vUtqaZavWaW/Hc6MICBLnEYgeCrdNpv7QBso3dRsTfnAZXQ==} - '@parity/product-sdk-signer@0.2.3': - resolution: {integrity: sha512-t2FGGuhDSFpTgr8j6S7sKcoKVwlF5chUO0PfruloUwTQXdMR9JlA2e9fqBhIvN5JzO7HYa2ZIFL0uFy4tXYZ9Q==} + '@parity/product-sdk-signer@0.2.4': + resolution: {integrity: sha512-3e3R3P/toG97UbCB+I0L8mQ1CT7QeYteNRO3bvEod+S7ST5CG7meGwllyGiaBR97eMHIYN9pQIkzVmQBFokPpg==} - '@parity/product-sdk-storage@0.1.3': - resolution: {integrity: sha512-kIkQw2MVhev0ZZYtc0dOd5wBnW8P0Av6MFpHboGYmQwzzTS35m/hykffaxadZQtg0BPwaxAtxgyttqcksI9R/A==} + '@parity/product-sdk-storage@0.1.4': + resolution: {integrity: sha512-tNUwidl265/z3rkX2GEPuZTV6q8Uk3Cm3uo1K0cxIN37NHjFaQwSwyF/SAXRhvubYSNzO3gfGVlE4Ar9j/V8WQ==} - '@parity/product-sdk-tx@0.2.2': - resolution: {integrity: sha512-MHkSsB1FovYElYPGdo4szXt5SVNzGK8HE+2OjWJPapsKDl2UqAEVvG+QnC6nhkny1qL+sVnSncJh6cVEbT8sJA==} + '@parity/product-sdk-tx@0.2.3': + resolution: {integrity: sha512-WjzN8pVlGekBP7lN1nypAfU3q8EPtg6L5x/Y0UvFVa/F0R5j4rDFUAs2GeEjrIumSvZhpe0OxYhgTke+/4qz2w==} '@polkadot-api/cli@0.21.1': resolution: {integrity: sha512-mPOiQxsGW499PgKls/o34vuKQkyPmnUI1wGxy0q/hUl4Dx9AUsTcVUHcm9LvZkHmFq7GfSNFih7Qeh1LXz6L1A==} @@ -4180,14 +4189,14 @@ snapshots: '@noble/hashes': 1.8.0 '@polkadot-api/substrate-bindings': 0.12.0 - '@parity/product-sdk-bulletin@0.4.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2)': + '@parity/product-sdk-bulletin@0.4.1(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2)': dependencies: '@parity/bulletin-sdk': 0.3.0(multiformats@13.4.2)(polkadot-api@2.1.2(esbuild@0.27.3)(rxjs@7.8.2)) - '@parity/product-sdk-chain-client': 0.4.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) + '@parity/product-sdk-chain-client': 0.4.1(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) '@parity/product-sdk-descriptors': 0.4.0(esbuild@0.27.3)(rxjs@7.8.2) - '@parity/product-sdk-host': 0.2.2(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) + '@parity/product-sdk-host': 0.3.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) '@parity/product-sdk-logger': 0.1.1 - '@parity/product-sdk-tx': 0.2.2(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) + '@parity/product-sdk-tx': 0.2.3(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) multiformats: 13.4.2 polkadot-api: 2.1.2(esbuild@0.27.3)(rxjs@7.8.2) transitivePeerDependencies: @@ -4199,10 +4208,10 @@ snapshots: - supports-color - utf-8-validate - '@parity/product-sdk-chain-client@0.4.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2)': + '@parity/product-sdk-chain-client@0.4.1(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2)': dependencies: '@parity/product-sdk-descriptors': 0.4.0(esbuild@0.27.3)(rxjs@7.8.2) - '@parity/product-sdk-host': 0.2.2(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) + '@parity/product-sdk-host': 0.3.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) '@parity/product-sdk-logger': 0.1.1 polkadot-api: 2.1.2(esbuild@0.27.3)(rxjs@7.8.2) transitivePeerDependencies: @@ -4214,13 +4223,13 @@ snapshots: - supports-color - utf-8-validate - '@parity/product-sdk-contracts@0.4.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2)(typescript@5.9.3)(zod@4.3.6)': + '@parity/product-sdk-contracts@0.5.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2)(typescript@5.9.3)(zod@4.3.6)': dependencies: '@parity/product-sdk-address': 0.1.1 - '@parity/product-sdk-keys': 0.2.2(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) + '@parity/product-sdk-keys': 0.2.3(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) '@parity/product-sdk-logger': 0.1.1 - '@parity/product-sdk-signer': 0.2.3(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2) - '@parity/product-sdk-tx': 0.2.2(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) + '@parity/product-sdk-signer': 0.2.4(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2) + '@parity/product-sdk-tx': 0.2.3(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) '@polkadot-labs/hdkd-helpers': 0.0.27 polkadot-api: 2.1.2(esbuild@0.27.3)(rxjs@7.8.2) viem: 2.46.3(typescript@5.9.3)(zod@4.3.6) @@ -4254,7 +4263,7 @@ snapshots: - supports-color - utf-8-validate - '@parity/product-sdk-host@0.2.2(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2)': + '@parity/product-sdk-host@0.3.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2)': dependencies: '@parity/product-sdk-logger': 0.1.1 polkadot-api: 2.1.2(esbuild@0.27.3)(rxjs@7.8.2) @@ -4268,11 +4277,11 @@ snapshots: - supports-color - utf-8-validate - '@parity/product-sdk-keys@0.2.2(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2)': + '@parity/product-sdk-keys@0.2.3(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2)': dependencies: '@parity/product-sdk-address': 0.1.1 '@parity/product-sdk-crypto': 0.1.1 - '@parity/product-sdk-storage': 0.1.3(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) + '@parity/product-sdk-storage': 0.1.4(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) '@polkadot-labs/hdkd': 0.0.28 '@polkadot-labs/hdkd-helpers': 0.0.10 polkadot-api: 2.1.2(esbuild@0.27.3)(rxjs@7.8.2) @@ -4287,11 +4296,11 @@ snapshots: '@parity/product-sdk-logger@0.1.1': {} - '@parity/product-sdk-signer@0.2.3(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2)': + '@parity/product-sdk-signer@0.2.4(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2)': dependencies: '@parity/product-sdk-address': 0.1.1 - '@parity/product-sdk-host': 0.2.2(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) - '@parity/product-sdk-keys': 0.2.2(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) + '@parity/product-sdk-host': 0.3.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) + '@parity/product-sdk-keys': 0.2.3(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) '@parity/product-sdk-logger': 0.1.1 polkadot-api: 2.1.2(esbuild@0.27.3)(rxjs@7.8.2) optionalDependencies: @@ -4306,9 +4315,9 @@ snapshots: - supports-color - utf-8-validate - '@parity/product-sdk-storage@0.1.3(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2)': + '@parity/product-sdk-storage@0.1.4(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2)': dependencies: - '@parity/product-sdk-host': 0.2.2(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) + '@parity/product-sdk-host': 0.3.0(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) '@parity/product-sdk-logger': 0.1.1 transitivePeerDependencies: - '@novasamatech/host-api' @@ -4319,9 +4328,9 @@ snapshots: - supports-color - utf-8-validate - '@parity/product-sdk-tx@0.2.2(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2)': + '@parity/product-sdk-tx@0.2.3(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2)': dependencies: - '@parity/product-sdk-keys': 0.2.2(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) + '@parity/product-sdk-keys': 0.2.3(@novasamatech/host-api@0.7.8)(@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.3)(esbuild@0.27.3)(rxjs@7.8.2))(esbuild@0.27.3)(rxjs@7.8.2) '@parity/product-sdk-logger': 0.1.1 '@polkadot-labs/hdkd-helpers': 0.0.10 polkadot-api: 2.1.2(esbuild@0.27.3)(rxjs@7.8.2) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f9f24fb..42c4410 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,11 +9,13 @@ packages: catalog: polkadot-api: ^2.1.2 "@polkadot-api/sdk-ink": ^0.7.0 - "@parity/product-sdk-bulletin": ^0.4.0 - "@parity/product-sdk-contracts": ^0.4.0 + "@parity/bulletin-sdk": ^0.3.0 + "@parity/product-sdk-bulletin": ^0.4.1 + "@parity/product-sdk-chain-client": ^0.4.1 + "@parity/product-sdk-contracts": ^0.5.0 "@parity/product-sdk-descriptors": ^0.4.0 - "@parity/product-sdk-host": ^0.2.2 - "@parity/product-sdk-tx": ^0.2.2 + "@parity/product-sdk-host": ^0.3.0 + "@parity/product-sdk-tx": ^0.2.3 "@polkadot-api/smoldot": ^0.4.2 "@polkadot-api/cli": ^0.21.1 "@polkadot-labs/hdkd": ^0.0.26 diff --git a/src/apps/frontend/package.json b/src/apps/frontend/package.json index 34ece4f..780db55 100644 --- a/src/apps/frontend/package.json +++ b/src/apps/frontend/package.json @@ -14,11 +14,13 @@ "@dotdm/contracts": "workspace:*", "@dotdm/env": "workspace:*", "@dotdm/utils": "workspace:*", + "@parity/product-sdk-bulletin": "catalog:", + "@parity/product-sdk-chain-client": "catalog:", "@parity/product-sdk-contracts": "catalog:", "@parity/product-sdk-descriptors": "catalog:", + "@parity/product-sdk-host": "catalog:", "dompurify": "catalog:", "marked": "catalog:", - "polkadot-api": "catalog:", "react": "catalog:", "react-dom": "catalog:", "react-router-dom": "catalog:" diff --git a/src/apps/frontend/public/vite.svg b/src/apps/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/src/apps/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/apps/frontend/src/assets/react.svg b/src/apps/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/apps/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/apps/frontend/src/components/CommandBox.css b/src/apps/frontend/src/components/CommandBox.css new file mode 100644 index 0000000..f759f4b --- /dev/null +++ b/src/apps/frontend/src/components/CommandBox.css @@ -0,0 +1,66 @@ +.command-box { + position: relative; + display: flex; + align-items: center; + gap: 12px; + width: 100%; + min-height: 38px; + padding: 0 22px; + color: var(--color-text-secondary); + background: transparent; + border: 0; + border-radius: 24px; + font-family: var(--font-code); + font-size: 13px; + text-align: left; + cursor: pointer; + transition: background-color 0.12s ease; +} + +.command-box:hover { + background: rgba(255, 255, 255, 0.06); +} + +.command-box-label { + color: var(--color-text-tertiary); + font-family: var(--font-sans); + font-weight: 600; + flex-shrink: 0; +} + +.command-box-prompt { + color: var(--accent); + font-weight: 800; + user-select: none; + flex-shrink: 0; +} + +.command-box-value { + flex: 1; + min-width: 0; + overflow: hidden; + color: #ffffff; + text-overflow: ellipsis; + white-space: nowrap; + user-select: none; +} + +.command-box-icon-slot { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + color: var(--color-text-tertiary); + flex-shrink: 0; +} + +.command-box-icon-slot--copied { + color: #22c55e; +} + +.command-box-icon { + display: block; + width: 14px; + height: 14px; +} diff --git a/src/apps/frontend/src/components/CommandBox.tsx b/src/apps/frontend/src/components/CommandBox.tsx new file mode 100644 index 0000000..ffdd4cc --- /dev/null +++ b/src/apps/frontend/src/components/CommandBox.tsx @@ -0,0 +1,44 @@ +import { useState } from "react"; +import { CheckIcon, CopyIcon } from "./Icons"; +import "./CommandBox.css"; + +interface CommandBoxProps { + command: string; + /** Optional label rendered before the prompt. Omit for a label-less variant. */ + label?: string; + className?: string; +} + +export default function CommandBox({ command, label, className }: CommandBoxProps) { + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(command); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const ariaLabel = `Copy ${label ? label.toLowerCase() : "install"} command`; + + return ( + + ); +} diff --git a/src/apps/frontend/src/components/ContractGrid.css b/src/apps/frontend/src/components/ContractGrid.css index 13f474f..18cfaf5 100644 --- a/src/apps/frontend/src/components/ContractGrid.css +++ b/src/apps/frontend/src/components/ContractGrid.css @@ -1,12 +1,13 @@ .contract-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); - gap: 16px; + grid-template-columns: repeat(3, minmax(0, 1fr)); + column-gap: 36px; + row-gap: 40px; } .contract-grid-message { - text-align: center; - padding: 40px 20px; + text-align: left; + padding: 0; color: var(--color-text-secondary); } @@ -25,5 +26,13 @@ @media (max-width: 768px) { .contract-grid { grid-template-columns: 1fr; + row-gap: 34px; + } +} + +@media (min-width: 769px) and (max-width: 1120px) { + .contract-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + column-gap: 28px; } } diff --git a/src/apps/frontend/src/components/ContractGrid.tsx b/src/apps/frontend/src/components/ContractGrid.tsx index bdd36c2..7827123 100644 --- a/src/apps/frontend/src/components/ContractGrid.tsx +++ b/src/apps/frontend/src/components/ContractGrid.tsx @@ -1,6 +1,7 @@ import type { Package } from "../data/types"; import PackageCard from "./PackageCard"; import InfiniteScroll from "./InfiniteScroll"; +import { SkeletonGrid } from "./SkeletonCard"; import "./ContractGrid.css"; interface ContractGridProps { @@ -36,11 +37,7 @@ export default function ContractGrid({ } if (connecting || (loading && packages.length === 0)) { - return ( -
-

Connecting to {network}...

-
- ); + return ; } if (packages.length === 0) { diff --git a/src/apps/frontend/src/components/Footer.css b/src/apps/frontend/src/components/Footer.css deleted file mode 100644 index 70cc8e1..0000000 --- a/src/apps/frontend/src/components/Footer.css +++ /dev/null @@ -1,68 +0,0 @@ -.footer { - background-color: var(--color-surface); - padding: 40px 20px 20px; - border-top: 1px solid var(--color-border); -} - -.footer-inner { - max-width: 1200px; - margin: 0 auto; -} - -.footer-columns { - display: flex; - gap: 60px; - flex-wrap: wrap; - margin-bottom: 30px; -} - -.footer-column h4 { - color: var(--color-text-primary); - font-size: 16px; - margin-bottom: 12px; - font-weight: 600; -} - -.footer-column ul { - list-style: none; -} - -.footer-column li { - margin-bottom: 8px; -} - -.footer-column a { - color: var(--color-text-secondary); - text-decoration: none; - font-size: 14px; -} - -.footer-column a:hover { - color: var(--color-text-primary); - text-decoration: underline; -} - -.footer-bottom { - border-top: 1px solid var(--color-border-strong); - padding-top: 16px; - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 10px; -} - -.footer-bottom a { - color: var(--color-text-secondary); - text-decoration: none; - font-size: 13px; -} - -.footer-bottom a:hover { - color: var(--color-text-primary); -} - -.footer-bottom span { - color: var(--color-text-tertiary); - font-size: 13px; -} diff --git a/src/apps/frontend/src/components/Footer.tsx b/src/apps/frontend/src/components/Footer.tsx deleted file mode 100644 index 87877fa..0000000 --- a/src/apps/frontend/src/components/Footer.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import "./Footer.css"; - -export default function Footer() { - return ( - - ); -} diff --git a/src/apps/frontend/src/components/GrainCanvas.tsx b/src/apps/frontend/src/components/GrainCanvas.tsx deleted file mode 100644 index 6aa99dc..0000000 --- a/src/apps/frontend/src/components/GrainCanvas.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useEffect, useRef } from "react"; - -export default function GrainCanvas() { - const canvasRef = useRef(null); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - const parent = canvas.parentElement; - if (!parent) return; - const ctx = canvas.getContext("2d"); - if (!ctx) return; - - let width = 0; - let height = 0; - let rafId: number | null = null; - let startTs: number | null = null; - let seeded = false; - let smoothX = 0; - let smoothY = 0; - - const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); - const baseEase = 0.03; - const minEase = 0.007; - const rampDurationMs = 2200; - - function resizeCanvas() { - const rect = parent!.getBoundingClientRect(); - width = canvas!.width = Math.max(1, Math.floor(rect.width)); - height = canvas!.height = Math.max(1, Math.floor(rect.height)); - } - - function generateGrain() { - const imgData = ctx!.createImageData(width, height); - const alpha = 28; - for (let i = 0; i < imgData.data.length; i += 4) { - const v = Math.random() * 255; - imgData.data[i] = v; - imgData.data[i + 1] = v; - imgData.data[i + 2] = v; - imgData.data[i + 3] = alpha; - } - return imgData; - } - - resizeCanvas(); - let grain = generateGrain(); - - function draw(t: number) { - if (prefersReducedMotion.matches) { - ctx!.clearRect(0, 0, width, height); - ctx!.putImageData(grain, 0, 0); - const x = width * 0.5; - const y = height * 0.35; - const gradient = ctx!.createRadialGradient(x, y, 0, x, y, 600); - gradient.addColorStop(0, "rgba(255,255,255,0.12)"); - gradient.addColorStop(1, "rgba(255,255,255,0)"); - ctx!.fillStyle = gradient; - ctx!.fillRect(0, 0, width, height); - return; - } - - if (startTs === null) startTs = t; - - if (!seeded) { - const time0 = t * 0.00025; - smoothX = width * (0.5 + 0.28 * Math.cos(time0)); - smoothY = height * (0.42 + 0.2 * Math.sin(time0 * 1.15)); - seeded = true; - } - - if (!(draw as any).lastGrainTs || t - (draw as any).lastGrainTs > 120) { - grain = generateGrain(); - (draw as any).lastGrainTs = t; - } - - ctx!.clearRect(0, 0, width, height); - ctx!.putImageData(grain, 0, 0); - - const time = t * 0.00025; - const targetX = width * (0.5 + 0.28 * Math.cos(time)); - const targetY = height * (0.42 + 0.2 * Math.sin(time * 1.15)); - - const elapsed = t - startTs!; - const ramp = Math.min(1, elapsed / rampDurationMs); - const ease = minEase + (baseEase - minEase) * ramp; - smoothX += (targetX - smoothX) * ease; - smoothY += (targetY - smoothY) * ease; - - const gradient = ctx!.createRadialGradient(smoothX, smoothY, 0, smoothX, smoothY, 900); - gradient.addColorStop(0, "rgba(255,255,255,0.18)"); - gradient.addColorStop(0.35, "rgba(255,255,255,0.08)"); - gradient.addColorStop(1, "rgba(255,255,255,0)"); - - ctx!.fillStyle = gradient; - ctx!.fillRect(0, 0, width, height); - - rafId = requestAnimationFrame(draw); - } - - function start() { - if (rafId) cancelAnimationFrame(rafId); - rafId = requestAnimationFrame(draw); - } - - const ro = new ResizeObserver(() => { - resizeCanvas(); - grain = generateGrain(); - smoothX = width * 0.5; - smoothY = height * 0.38; - startTs = null; - seeded = false; - start(); - }); - ro.observe(parent); - - start(); - - return () => { - if (rafId) cancelAnimationFrame(rafId); - ro.disconnect(); - }; - }, []); - - return ( - - ); -} diff --git a/src/apps/frontend/src/components/Header.css b/src/apps/frontend/src/components/Header.css index c7ea2ba..2e0f231 100644 --- a/src/apps/frontend/src/components/Header.css +++ b/src/apps/frontend/src/components/Header.css @@ -1,53 +1,46 @@ .header { - background-color: var(--color-bg); - padding: 18px 18px; - border-bottom: 1px solid var(--color-border-strong); - min-height: 64px; display: flex; align-items: center; - position: sticky; + justify-content: center; + position: relative; top: 0; z-index: 100; + min-height: 72px; + padding: 28px 0 0; + background-color: #000000; } -.header-inner { - width: 100%; +.header-nav { display: flex; align-items: center; - gap: 8px; + gap: 22px; + width: 100%; + max-width: 1140px; + padding: 0 calc(32px + 22px); } -.header-logo { - display: inline-flex; - align-items: center; +.header-nav-link { + color: var(--color-text-tertiary); + font-family: var(--font-sans); + font-size: 14px; + font-weight: 500; + letter-spacing: -0.01em; text-decoration: none; - flex-shrink: 0; - gap: 8px; - margin: 0; - padding: 0; + transition: color 0.1s ease; } -.header-logo:hover { +.header-nav-link:hover { + color: var(--color-text-primary); text-decoration: none; } -.header-logo-img { - height: 42px; - width: 42px; - object-fit: contain; -} +@media (max-width: 900px) { + .header { + padding: 24px 0 0; + } -.header-logo-text { - color: var(--color-text-primary); - font-weight: 400; - font-size: 1.25rem; - letter-spacing: -0.02em; - font-family: var(--font-serif); -} - -.header-separator { - color: var(--color-text-tertiary); - font-size: 1.25rem; - font-family: var(--font-serif); - user-select: none; + .header-nav { + gap: 16px; + padding: 0 calc(24px + 22px); + } } diff --git a/src/apps/frontend/src/components/Header.tsx b/src/apps/frontend/src/components/Header.tsx index 2646ed2..7f9ecad 100644 --- a/src/apps/frontend/src/components/Header.tsx +++ b/src/apps/frontend/src/components/Header.tsx @@ -1,19 +1,37 @@ import { Link } from "react-router-dom"; -import logo from "../assets/logo.png"; import NetworkConfig from "./NetworkConfig"; +import { handleExternalClick } from "../lib/external-link"; import "./Header.css"; +const REPO_URL = "https://github.com/paritytech/contract-dependency-manager"; + +const EXTERNAL_LINKS: { label: string; href: string }[] = [ + { label: "Docs", href: REPO_URL }, + { label: "Github", href: REPO_URL }, + { label: "Playground", href: "https://playground.dot" }, +]; + export default function Header() { return (
-
- - cdm logo - Contract Hub +
+
); } diff --git a/src/apps/frontend/src/components/Layout.tsx b/src/apps/frontend/src/components/Layout.tsx index 59b3c98..ddb0d4c 100644 --- a/src/apps/frontend/src/components/Layout.tsx +++ b/src/apps/frontend/src/components/Layout.tsx @@ -1,6 +1,5 @@ import type { ReactNode } from "react"; import Header from "./Header"; -import Footer from "./Footer"; interface LayoutProps { children: ReactNode; @@ -11,7 +10,6 @@ export default function Layout({ children }: LayoutProps) {
{children}
-
); } diff --git a/src/apps/frontend/src/components/NetworkConfig.css b/src/apps/frontend/src/components/NetworkConfig.css index 3f71ac1..9968729 100644 --- a/src/apps/frontend/src/components/NetworkConfig.css +++ b/src/apps/frontend/src/components/NetworkConfig.css @@ -1,170 +1,102 @@ -.net-selector { +.net-picker { + display: inline-flex; + align-items: center; position: relative; } -/* Trigger button */ -.net-selector-trigger { +.net-picker-item { display: inline-flex; align-items: center; - gap: 4px; - background: none; - border: none; - padding: 2px 4px; margin: 0; + padding: 0; + background: transparent; + border: 0; + color: var(--color-text-tertiary); + font-family: var(--font-sans); + font-size: 14px; + font-weight: 500; + letter-spacing: -0.01em; cursor: pointer; - font-family: var(--font-serif); - font-size: 1.25rem; - font-weight: 400; - letter-spacing: -0.02em; - color: var(--color-text-primary); - border-radius: 6px; - transition: color 0.15s; -} - -.net-selector-trigger:hover { - color: var(--accent); + transition: color 0.1s ease; } -.net-selector-name { - display: inline-flex; - align-items: center; - gap: 6px; -} - -/* Connection status dot */ -.net-dot { - display: inline-block; - width: 7px; - height: 7px; - border-radius: 50%; - flex-shrink: 0; -} - -.net-dot--connected { - background-color: #22c55e; -} - -.net-dot--connecting { - background-color: #eab308; - animation: net-dot-pulse 1.2s ease-in-out infinite; -} - -.net-dot--disconnected { - background-color: var(--color-text-tertiary); -} - -@keyframes net-dot-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.3; } -} - -/* Chevron */ -.net-selector-chevron { +.net-picker-current { color: var(--color-text-secondary); - transition: transform 0.15s; - flex-shrink: 0; } -.net-selector-chevron--open { - transform: rotate(180deg); -} - -/* Dropdown panel */ -.net-selector-dropdown { - position: absolute; - top: calc(100% + 6px); - left: 0; - min-width: 220px; - background: var(--color-surface); - border: 1px solid var(--color-border-strong); - border-radius: 8px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); - z-index: 200; - overflow: hidden; -} - -/* Option list */ -.net-selector-list { - list-style: none; - margin: 0; - padding: 4px; -} - -.net-selector-option { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - background: none; - border: none; - padding: 8px 12px; - margin: 0; - cursor: pointer; - font-family: var(--font-sans); - font-size: 0.875rem; +.net-picker-current:hover { color: var(--color-text-primary); - border-radius: 6px; - transition: background-color 0.1s; } -.net-selector-option:hover { - background-color: var(--color-surface-secondary); +.net-picker-label { + line-height: 1.1; } -.net-selector-option--active { - background-color: var(--color-surface-secondary); - color: var(--accent); +/* Status pill outline around the active network name */ +.net-picker-pill { + display: inline-block; + padding: 3px 10px; + border: 1px solid var(--color-border-strong); + border-radius: 999px; + line-height: 1.1; + color: var(--color-text-secondary); + transition: + border-color 0.2s ease, + color 0.2s ease; } -.net-selector-option--active svg { - color: var(--accent); +.net-picker-pill--connected { + border-color: rgba(34, 197, 94, 0.45); } -/* Input fields section */ -.net-selector-fields { - display: flex; - flex-direction: column; - gap: 8px; - padding: 10px 12px; - border-top: 1px solid var(--color-border-strong); +.net-picker-pill--connecting { + border-color: rgba(234, 179, 8, 0.45); + animation: net-pill-pulse 1.6s ease-in-out infinite; } -.net-selector-field { - display: flex; - flex-direction: column; - gap: 2px; +.net-picker-pill--disconnected { + border-color: var(--color-border-strong); } -.net-selector-field-label { - font-size: 11px; - color: var(--color-text-secondary); - padding-left: 2px; - font-family: var(--font-sans); +@keyframes net-pill-pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } } -.net-selector-field-input { - background: var(--color-surface-secondary); - color: var(--color-text-primary); - border: 1px solid var(--color-border-strong); - border-radius: 6px; - padding: 5px 8px; - font-size: 12px; - font-family: var(--font-sans); - min-width: 200px; - outline: none; +/* "Other options" float to the right of the trigger without taking flow space, + so the surrounding (centered) nav doesn't shift when the picker opens. */ +.net-picker-others { + display: inline-flex; + align-items: center; + gap: 18px; + position: absolute; + left: 100%; + top: 50%; + margin-left: 18px; + transform: translate(-8px, -50%); + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: + opacity 0.2s ease, + transform 0.2s ease; } -.net-selector-field-input::placeholder { - color: var(--color-text-tertiary); +.net-picker--open .net-picker-others { + opacity: 1; + transform: translate(0, -50%); + pointer-events: auto; } -.net-selector-field-input:focus { - border-color: var(--color-text-tertiary); +.net-picker-other { + white-space: nowrap; } -.net-selector-field-input:disabled { - background: var(--color-surface-secondary); - color: var(--color-text-tertiary); - border-color: var(--color-border); - cursor: not-allowed; +.net-picker-other:hover { + color: var(--color-text-primary); } diff --git a/src/apps/frontend/src/components/NetworkConfig.tsx b/src/apps/frontend/src/components/NetworkConfig.tsx index 4a61f84..df5a69f 100644 --- a/src/apps/frontend/src/components/NetworkConfig.tsx +++ b/src/apps/frontend/src/components/NetworkConfig.tsx @@ -1,157 +1,58 @@ -import { useState, useRef, useEffect } from "react"; +import { useEffect, useRef, useState } from "react"; import "./NetworkConfig.css"; -import { useNetwork } from "../context/NetworkContext"; - -const DISPLAY_NAMES: Record = { - "preview-net": "Preview Net", - paseo: "Paseo", - polkadot: "Polkadot", - local: "Local", - custom: "Custom", -}; - -const NETWORK_OPTIONS = ["paseo", "preview-net", "polkadot", "local", "custom"]; +import { useNetwork } from "../context/useNetwork"; +import type { NetworkKey } from "../config/networks"; export default function NetworkConfig() { - const { - network, - setNetwork, - assethubUrl, - bulletinUrl, - registryAddress, - setAssethubUrl, - setBulletinUrl, - setRegistryAddress, - connected, - connecting, - } = useNetwork(); - + const { network, networks, setNetwork, connected, connecting } = useNetwork(); const [open, setOpen] = useState(false); - const dropdownRef = useRef(null); + const containerRef = useRef(null); useEffect(() => { function handleClickOutside(e: MouseEvent) { - if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setOpen(false); } } - if (open) { - document.addEventListener("mousedown", handleClickOutside); - } + if (open) document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [open]); - const handleSelect = (key: string) => { - setNetwork(key); - }; - - const showInputs = open && (network === "custom" || network === "local"); + const active = networks.find((item) => item.key === network); + const others = networks.filter((item) => item.key !== network); + const statusModifier = connecting ? "connecting" : connected ? "connected" : "disconnected"; return ( -
+
- - {open && ( -
-
    - {NETWORK_OPTIONS.map((key) => ( -
  • - -
  • - ))} -
- - {showInputs && ( -
- {network === "custom" && ( - <> -
- - setAssethubUrl(e.target.value)} - placeholder="ws://..." - /> -
-
- - setBulletinUrl(e.target.value)} - placeholder="ws://..." - /> -
-
- - setRegistryAddress(e.target.value)} - placeholder="0x..." - /> -
- - )} -
- )} -
- )} +
+ {others.map((item, index) => ( + + ))} +
); } diff --git a/src/apps/frontend/src/components/PackageCard.css b/src/apps/frontend/src/components/PackageCard.css index 2d24831..7724ec2 100644 --- a/src/apps/frontend/src/components/PackageCard.css +++ b/src/apps/frontend/src/components/PackageCard.css @@ -1,109 +1,121 @@ .package-card { - padding: 20px 0; - border-bottom: 1px solid var(--color-border); + position: relative; display: flex; flex-direction: column; + min-width: 0; height: 100%; + color: inherit; + text-decoration: none; + cursor: pointer; +} + +.package-card::before { + content: ""; + position: absolute; + inset: -14px -16px; + border-radius: 12px; + background-color: transparent; + transition: background-color 0.1s ease; + pointer-events: none; + z-index: 0; +} + +.package-card:hover::before { + background-color: rgba(255, 255, 255, 0.06); +} + +.package-card:hover { + text-decoration: none; +} + +.package-card > * { + position: relative; + z-index: 1; } .package-card-header { display: flex; align-items: baseline; gap: 10px; - margin-bottom: 6px; + min-width: 0; + margin-bottom: 12px; + min-height: 22px; } .package-card-name { - color: var(--color-text-primary); - font-size: 20px; + display: inline-flex; + min-width: 0; + max-width: 100%; + overflow: hidden; + color: var(--color-text-secondary); + font-size: 16px; font-weight: 600; + letter-spacing: -0.02em; + line-height: 1.25; text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; } -.package-card-name:hover { - color: var(--accent); +.package-card-name-prefix { + min-width: 0; + overflow: hidden; + color: var(--color-text-tertiary); + text-overflow: ellipsis; +} + +.package-card-name-leaf { + color: #ffffff; } .package-card-version { background-color: var(--color-surface-secondary); color: var(--color-text-secondary); font-size: 12px; - padding: 2px 8px; - border-radius: 3px; + line-height: 1; + padding: 5px 7px; + border-radius: 5px; font-weight: 600; + flex-shrink: 0; } .package-card-description { + display: flex; + flex-direction: column; + flex: 1; + gap: 8px; + margin-bottom: 22px; + min-height: 44px; +} + +.package-card-description-text { color: var(--color-text-secondary); font-size: 15px; - margin-bottom: 10px; - line-height: 1.4; - flex: 1; + line-height: 1.42; + margin: 0; } .package-card-meta { display: flex; align-items: center; - gap: 16px; + justify-content: space-between; + gap: 18px; flex-wrap: wrap; font-size: 13px; - color: var(--color-text-tertiary); + line-height: 1.4; + color: var(--color-text-quaternary); + min-height: 16px; } .package-card-author { - color: var(--color-text-secondary); + color: var(--color-text-quaternary); } .package-card-downloads { - color: var(--color-text-secondary); + display: none; } .package-card-date { - color: var(--color-text-tertiary); - flex-basis: 100%; -} - -.package-card-keywords { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-top: 10px; -} - -.package-card-keyword { - background-color: var(--color-surface-secondary); - color: var(--color-text-secondary); - font-size: 12px; - padding: 3px 10px; - border-radius: 12px; - text-decoration: none; -} - -.package-card-keyword:hover { - background-color: var(--color-border-strong); - text-decoration: none; -} - -.package-card-shimmer { - background: linear-gradient( - 90deg, - var(--color-surface-secondary) 25%, - var(--color-border) 50%, - var(--color-surface-secondary) 75% - ); - background-size: 200% 100%; - animation: shimmer 1.5s ease-in-out infinite; - border-radius: 4px; - color: transparent !important; - min-width: 120px; - display: inline-block; -} - -@keyframes shimmer { - 0% { - background-position: 200% 0; - } - 100% { - background-position: -200% 0; - } + color: var(--color-text-quaternary); + flex-shrink: 0; } diff --git a/src/apps/frontend/src/components/PackageCard.tsx b/src/apps/frontend/src/components/PackageCard.tsx index ba98b58..46b5c09 100644 --- a/src/apps/frontend/src/components/PackageCard.tsx +++ b/src/apps/frontend/src/components/PackageCard.tsx @@ -11,79 +11,79 @@ function formatCalls(n: number): string { return n.toLocaleString(); } +function splitPackageName(name: string): { prefix: string; leaf: string } { + const idx = name.lastIndexOf("/"); + if (idx < 0) return { prefix: "", leaf: name }; + return { + prefix: name.slice(0, idx + 1), + leaf: name.slice(idx + 1), + }; +} + export default function PackageCard({ pkg, linkTarget }: PackageCardProps) { const metadataLoading = pkg.metadataUri && !pkg.metadataUri.includes(":") && !pkg.metadataLoaded; + const name = splitPackageName(pkg.name); - return ( -
+ const cardBody = ( + <>
- {linkTarget ? ( - - {pkg.name} - - ) : ( - - {pkg.name} - - )} + + {name.prefix} + {name.leaf} + v{pkg.version}
- {pkg.description ? ( -

{pkg.description}

- ) : metadataLoading ? ( -

 

- ) : null} +
+ {pkg.description ? ( +

{pkg.description}

+ ) : metadataLoading ? ( + <> + + + + ) : null} +
{pkg.author ? ( - by {pkg.author} + {pkg.author} ) : metadataLoading ? ( - -          - - ) : null} + + ) : ( + + )} {pkg.weeklyCalls != null && ( {formatCalls(pkg.weeklyCalls)} weekly calls )} - {pkg.publishedDate && ( - published {pkg.publishedDate} - )} + {pkg.publishedDate ? ( + {pkg.publishedDate} + ) : metadataLoading ? ( + + ) : null}
+ + ); - {(pkg.keywords ?? []).length > 0 && ( -
- {(pkg.keywords ?? []).map((kw) => - linkTarget ? ( - - {kw} - - ) : ( - - {kw} - - ), - )} -
- )} -
+ if (linkTarget) { + return ( + + {cardBody} + + ); + } + + return ( + + {cardBody} + ); } diff --git a/src/apps/frontend/src/components/SearchBox.css b/src/apps/frontend/src/components/SearchBox.css new file mode 100644 index 0000000..80de60f --- /dev/null +++ b/src/apps/frontend/src/components/SearchBox.css @@ -0,0 +1,33 @@ +.search-box { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: 14px; + width: 100%; + min-height: 56px; + padding: 0 22px; + background: #2a2a2a; + border: 0; + border-radius: 28px; +} + +.search-box-icon { + color: var(--color-text-secondary); + flex-shrink: 0; +} + +.search-box-input { + width: 100%; + min-width: 0; + border: 0; + outline: 0; + background: transparent; + color: #ffffff; + font-family: var(--font-sans); + font-size: 15px; + line-height: 1; +} + +.search-box-input::placeholder { + color: var(--color-text-secondary); +} diff --git a/src/apps/frontend/src/components/SearchBox.tsx b/src/apps/frontend/src/components/SearchBox.tsx new file mode 100644 index 0000000..c1f744d --- /dev/null +++ b/src/apps/frontend/src/components/SearchBox.tsx @@ -0,0 +1,51 @@ +import type { FormEvent } from "react"; +import "./SearchBox.css"; + +interface SearchBoxProps { + value: string; + onChange: (value: string) => void; + onSubmit: (value: string) => void; + placeholder?: string; + ariaLabel?: string; + className?: string; +} + +export default function SearchBox({ + value, + onChange, + onSubmit, + placeholder = "Search...", + ariaLabel = "Search", + className, +}: SearchBoxProps) { + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + onSubmit(value); + }; + + return ( +
+ + onChange(event.target.value)} + placeholder={placeholder} + aria-label={ariaLabel} + /> +
+ ); +} diff --git a/src/apps/frontend/src/components/SkeletonCard.css b/src/apps/frontend/src/components/SkeletonCard.css new file mode 100644 index 0000000..9594b1b --- /dev/null +++ b/src/apps/frontend/src/components/SkeletonCard.css @@ -0,0 +1,46 @@ +.package-card--skeleton { + cursor: default; + pointer-events: none; +} + +.package-card--skeleton::before { + display: none; +} + +.skeleton-bar { + display: inline-block; + background-color: rgba(255, 255, 255, 0.05); + border-radius: 4px; + flex-shrink: 0; +} + +.skeleton-bar--name { + width: 60%; + max-width: 220px; + height: 16px; +} + +.skeleton-bar--version { + width: 32px; + height: 18px; + border-radius: 5px; +} + +.skeleton-bar--desc { + width: 100%; + height: 13px; +} + +.skeleton-bar--desc-short { + width: 65%; +} + +.skeleton-bar--author { + width: 110px; + height: 12px; +} + +.skeleton-bar--date { + width: 70px; + height: 12px; +} diff --git a/src/apps/frontend/src/components/SkeletonCard.tsx b/src/apps/frontend/src/components/SkeletonCard.tsx new file mode 100644 index 0000000..87f9d31 --- /dev/null +++ b/src/apps/frontend/src/components/SkeletonCard.tsx @@ -0,0 +1,35 @@ +import "./SkeletonCard.css"; + +interface SkeletonGridProps { + count?: number; +} + +export function SkeletonCard() { + return ( + + ); +} + +export function SkeletonGrid({ count = 9 }: SkeletonGridProps) { + return ( + + ); +} diff --git a/src/apps/frontend/src/config/networks.ts b/src/apps/frontend/src/config/networks.ts new file mode 100644 index 0000000..7cdd809 --- /dev/null +++ b/src/apps/frontend/src/config/networks.ts @@ -0,0 +1,49 @@ +import { getRegistryAddress, type ProductSdkEnvironment } from "@dotdm/env/registry"; +import { paseo_asset_hub } from "@parity/product-sdk-descriptors/paseo-asset-hub"; +import { previewnet_asset_hub } from "@parity/product-sdk-descriptors/previewnet-asset-hub"; + +export type NetworkKey = "paseo" | "previewnet"; +type AssetHubDescriptor = typeof paseo_asset_hub | typeof previewnet_asset_hub; + +export interface NetworkConfig { + key: NetworkKey; + label: string; + installName: string; + productSdkEnvironment: ProductSdkEnvironment; + assetHubDescriptor: AssetHubDescriptor; + registryAddress: `0x${string}`; +} + +function registryAddressFor(name: string): `0x${string}` { + const registryAddress = getRegistryAddress(name); + if (!registryAddress) throw new Error(`No CDM registry address configured for ${name}`); + return registryAddress as `0x${string}`; +} + +export const NETWORKS: Record = { + paseo: { + key: "paseo", + label: "Paseo", + installName: "paseo", + productSdkEnvironment: "paseo", + assetHubDescriptor: paseo_asset_hub, + registryAddress: registryAddressFor("paseo"), + }, + previewnet: { + key: "previewnet", + label: "PreviewNet", + installName: "preview-net", + productSdkEnvironment: "previewnet", + assetHubDescriptor: previewnet_asset_hub, + registryAddress: registryAddressFor("preview-net"), + }, +}; + +export const DEFAULT_NETWORK: NetworkKey = "paseo"; +export const NETWORK_OPTIONS = Object.values(NETWORKS); + +export function resolveNetworkKey(value: string | null | undefined): NetworkKey | null { + if (!value) return null; + if (value === "preview-net") return "previewnet"; + return value in NETWORKS ? (value as NetworkKey) : null; +} diff --git a/src/apps/frontend/src/context/NetworkContext.tsx b/src/apps/frontend/src/context/NetworkContext.tsx index 366d2f3..fc3091a 100644 --- a/src/apps/frontend/src/context/NetworkContext.tsx +++ b/src/apps/frontend/src/context/NetworkContext.tsx @@ -1,164 +1,100 @@ -import React, { createContext, useContext, useState, useEffect, useRef, useCallback } from "react"; -import { createClient, type HexString, type PolkadotClient } from "polkadot-api"; -import { getWsProvider } from "polkadot-api/ws"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { - createContractFromClient, - type Contract, - type ContractDef, -} from "@parity/product-sdk-contracts"; -import { polkadot_asset_hub } from "@parity/product-sdk-descriptors/polkadot-asset-hub"; -import { paseo_asset_hub } from "@parity/product-sdk-descriptors/paseo-asset-hub"; -import { previewnet_asset_hub } from "@parity/product-sdk-descriptors/previewnet-asset-hub"; + destroyAll, + getChainAPI, + isInsideContainer, + type ChainClient, + type PresetChains, +} from "@parity/product-sdk-chain-client"; +import { createContract, createContractRuntimeFromClient } from "@parity/product-sdk-contracts"; +import type { ContractRuntime } from "@parity/product-sdk-contracts"; import { CONTRACTS_REGISTRY_ABI } from "@dotdm/contracts/abi"; -import { getChainPreset, REGISTRY_ADDRESS, type ChainPreset } from "@dotdm/env"; import { ALICE_SS58 } from "@dotdm/utils"; - -const NETWORK_PRESETS: Record = { - polkadot: getChainPreset("polkadot"), - paseo: getChainPreset("paseo"), - "preview-net": getChainPreset("preview-net"), - local: getChainPreset("local"), - custom: { - assethubUrl: "", - bulletinUrl: "", - ipfsGatewayUrl: "", - registryAddress: REGISTRY_ADDRESS, - }, -}; - -const ASSET_HUB_DESCRIPTORS = { - polkadot: polkadot_asset_hub, - paseo: paseo_asset_hub, - "preview-net": previewnet_asset_hub, - local: paseo_asset_hub, - custom: paseo_asset_hub, -} as const; - -export type RegistryContract = Contract; - -interface NetworkContextType { - network: string; - setNetwork: (name: string) => void; - assethubUrl: string; - bulletinUrl: string; - ipfsGatewayUrl: string; - registryAddress: string; - setAssethubUrl: (url: string) => void; - setBulletinUrl: (url: string) => void; - setIpfsGatewayUrl: (url: string) => void; - setRegistryAddress: (address: string) => void; - registry: RegistryContract | null; - connected: boolean; - connecting: boolean; - error: string | null; -} - -const NetworkContext = createContext(null); - -export function useNetwork(): NetworkContextType { - const ctx = useContext(NetworkContext); - if (!ctx) throw new Error("useNetwork must be used within a NetworkProvider"); - return ctx; -} +import { + DEFAULT_NETWORK, + NETWORKS, + NETWORK_OPTIONS, + type NetworkConfig, + type NetworkKey, +} from "../config/networks"; +import { withTimeout } from "../data/timeout"; +import { NetworkContext, type RegistryContract } from "./network-context"; + +type ProductChainClient = ChainClient>; +const CONNECTION_TIMEOUT_MS = 20_000; export function NetworkProvider({ children }: { children: React.ReactNode }) { - const [network, setNetworkState] = useState("paseo"); - const [assethubUrl, setAssethubUrl] = useState(NETWORK_PRESETS["paseo"].assethubUrl); - const [bulletinUrl, setBulletinUrl] = useState(NETWORK_PRESETS["paseo"].bulletinUrl); - const [ipfsGatewayUrl, setIpfsGatewayUrl] = useState(NETWORK_PRESETS["paseo"].ipfsGatewayUrl); - const [registryAddress, setRegistryAddress] = useState( - NETWORK_PRESETS["paseo"].registryAddress ?? REGISTRY_ADDRESS, - ); + const [network, setNetworkState] = useState(DEFAULT_NETWORK); const [registry, setRegistry] = useState(null); const [connected, setConnected] = useState(false); const [connecting, setConnecting] = useState(false); const [error, setError] = useState(null); - const clientRef = useRef(null); + const networkConfig = NETWORKS[network]; - const setNetwork = useCallback((name: string) => { + const setNetwork = useCallback((name: NetworkKey) => { setNetworkState(name); - const preset = NETWORK_PRESETS[name]; - if (preset) { - setAssethubUrl(preset.assethubUrl); - setBulletinUrl(preset.bulletinUrl); - setIpfsGatewayUrl(preset.ipfsGatewayUrl); - setRegistryAddress(preset.registryAddress ?? REGISTRY_ADDRESS); - } + setConnecting(true); + setConnected(false); + setRegistry(null); + setError(null); }, []); useEffect(() => { - if (!assethubUrl) { - setRegistry(null); - setConnected(false); - setError(null); - return; - } - const abort = new AbortController(); + let client: ProductChainClient | null = null; const connect = async () => { - // Clean up previous connection - if (clientRef.current) { - clientRef.current.destroy(); - clientRef.current = null; - } - setConnecting(true); setConnected(false); setError(null); setRegistry(null); try { - const client = createClient(getWsProvider(assethubUrl)); - clientRef.current = client; - - // Verify the connection actually works by fetching chain spec with a timeout. - // getWsProvider silently retries forever, so without this the UI would just - // spin indefinitely when the node is unreachable. - const CONNECTION_TIMEOUT_MS = 15_000; - await Promise.race([ - client.getChainSpecData(), - new Promise((_, reject) => { - const tid = setTimeout( - () => - reject( - new Error( - `Connection to ${assethubUrl} timed out after ${CONNECTION_TIMEOUT_MS / 1000}s`, - ), - ), - CONNECTION_TIMEOUT_MS, - ); - abort.signal.addEventListener("abort", () => { - clearTimeout(tid); - reject(new Error("aborted")); - }); - }), - ]); + if (!(await isInsideContainer())) { + throw new Error( + "Host provider unavailable. Open Contract Hub inside Polkadot Desktop.", + ); + } - if (abort.signal.aborted) return; + client = (await withTimeout( + getChainAPI(networkConfig.productSdkEnvironment), + `Connection to ${networkConfig.label} timed out after ${CONNECTION_TIMEOUT_MS / 1000}s.`, + CONNECTION_TIMEOUT_MS, + abort.signal, + )) as ProductChainClient; + if (abort.signal.aborted) { + client.destroy(); + return; + } - const reg = await createContractFromClient( - client, - ASSET_HUB_DESCRIPTORS[network as keyof typeof ASSET_HUB_DESCRIPTORS], - registryAddress as HexString, + const runtime: ContractRuntime = createContractRuntimeFromClient( + client.raw.assetHub, + networkConfig.assetHubDescriptor, + ); + const reg = createContract( + runtime, + networkConfig.registryAddress, CONTRACTS_REGISTRY_ABI, { defaultOrigin: ALICE_SS58 }, ); - if (abort.signal.aborted) return; + if (abort.signal.aborted) { + client.destroy(); + return; + } setRegistry(reg); setConnected(true); - setConnecting(false); } catch (err) { if (!abort.signal.aborted) { - // Clean up the failed client so it stops retrying - if (clientRef.current) { - clientRef.current.destroy(); - clientRef.current = null; - } + client?.destroy(); + client = null; + destroyAll(); setError(err instanceof Error ? err.message : "Connection failed"); + } + } finally { + if (!abort.signal.aborted) { setConnecting(false); } } @@ -168,35 +104,24 @@ export function NetworkProvider({ children }: { children: React.ReactNode }) { return () => { abort.abort(); - if (clientRef.current) { - clientRef.current.destroy(); - clientRef.current = null; - } + client?.destroy(); }; - }, [assethubUrl, registryAddress, network]); - - return ( - - {children} - + }, [networkConfig]); + + const value = useMemo( + () => ({ + network, + networkConfig, + networks: NETWORK_OPTIONS, + setNetwork, + registryAddress: networkConfig.registryAddress, + registry, + connected, + connecting, + error, + }), + [network, networkConfig, setNetwork, registry, connected, connecting, error], ); -} -export { NETWORK_PRESETS }; + return {children}; +} diff --git a/src/apps/frontend/src/context/network-context.ts b/src/apps/frontend/src/context/network-context.ts new file mode 100644 index 0000000..9f2ff3f --- /dev/null +++ b/src/apps/frontend/src/context/network-context.ts @@ -0,0 +1,19 @@ +import { createContext } from "react"; +import type { Contract, ContractDef } from "@parity/product-sdk-contracts"; +import type { NetworkConfig, NetworkKey } from "../config/networks"; + +export type RegistryContract = Contract; + +export interface NetworkContextType { + network: NetworkKey; + networkConfig: NetworkConfig; + networks: NetworkConfig[]; + setNetwork: (name: NetworkKey) => void; + registryAddress: `0x${string}`; + registry: RegistryContract | null; + connected: boolean; + connecting: boolean; + error: string | null; +} + +export const NetworkContext = createContext(null); diff --git a/src/apps/frontend/src/context/useNetwork.ts b/src/apps/frontend/src/context/useNetwork.ts new file mode 100644 index 0000000..6b5f9d5 --- /dev/null +++ b/src/apps/frontend/src/context/useNetwork.ts @@ -0,0 +1,8 @@ +import { useContext } from "react"; +import { NetworkContext, type NetworkContextType } from "./network-context"; + +export function useNetwork(): NetworkContextType { + const ctx = useContext(NetworkContext); + if (!ctx) throw new Error("useNetwork must be used within a NetworkProvider"); + return ctx; +} diff --git a/src/apps/frontend/src/data/bulletin-client.ts b/src/apps/frontend/src/data/bulletin-client.ts new file mode 100644 index 0000000..136a01d --- /dev/null +++ b/src/apps/frontend/src/data/bulletin-client.ts @@ -0,0 +1,29 @@ +import { BulletinClient, createLazySigner } from "@parity/product-sdk-bulletin"; +import type { ProductSdkEnvironment } from "@dotdm/env/registry"; +import { withTimeout } from "./timeout"; + +const clients = new Map>(); + +function getBulletinClient(environment: ProductSdkEnvironment): Promise { + let client = clients.get(environment); + if (!client) { + client = BulletinClient.create({ + environment, + signer: createLazySigner(() => null), + }); + clients.set(environment, client); + } + return client; +} + +export async function queryBulletinJson( + environment: ProductSdkEnvironment, + cid: string, +): Promise { + const client = await getBulletinClient(environment); + return withTimeout( + client.fetchJson(cid), + `Bulletin metadata lookup timed out for CID ${cid}.`, + 30_000, + ); +} diff --git a/src/apps/frontend/src/data/registry-queries.ts b/src/apps/frontend/src/data/registry-queries.ts index cdb6c67..43e2294 100644 --- a/src/apps/frontend/src/data/registry-queries.ts +++ b/src/apps/frontend/src/data/registry-queries.ts @@ -1,5 +1,5 @@ import type { Package, AbiEntry } from "./types"; -import type { RegistryContract } from "../context/NetworkContext"; +import type { RegistryContract } from "../context/network-context"; export function unwrapOption(val: unknown): T | undefined { if (val && typeof val === "object" && "isSome" in val) { @@ -34,6 +34,15 @@ export async function queryContractByName( }; } +export function metadataCidFromUri(uri: string | undefined): string | undefined { + if (!uri) return undefined; + if (uri.startsWith("ipfs://")) return uri.slice("ipfs://".length); + const ipfsPath = "/ipfs/"; + const idx = uri.indexOf(ipfsPath); + if (idx >= 0) return uri.slice(idx + ipfsPath.length); + return uri.includes(":") ? undefined : uri; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any export function parseMetadata(metadata: any): Partial { const author = @@ -56,6 +65,12 @@ export function parseMetadata(metadata: any): Partial { readme: metadata.readme || undefined, homepage: metadata.homepage || undefined, repository: metadata.repository || undefined, + license: metadata.license || undefined, + keywords: Array.isArray(metadata.keywords) ? metadata.keywords : undefined, + dependencies: + metadata.dependencies && typeof metadata.dependencies === "object" + ? metadata.dependencies + : undefined, author, lastPublished, publishedDate: lastPublished, diff --git a/src/apps/frontend/src/data/timeout.ts b/src/apps/frontend/src/data/timeout.ts new file mode 100644 index 0000000..815a3fb --- /dev/null +++ b/src/apps/frontend/src/data/timeout.ts @@ -0,0 +1,31 @@ +export function withTimeout( + promise: PromiseLike, + message: string, + timeoutMs = 20_000, + signal?: AbortSignal, +): Promise { + if (signal?.aborted) return Promise.reject(new Error("aborted")); + + return new Promise((resolve, reject) => { + let settled = false; + const timer = setTimeout(() => { + settle(() => reject(new Error(message))); + }, timeoutMs); + + const onAbort = () => settle(() => reject(new Error("aborted"))); + signal?.addEventListener("abort", onAbort, { once: true }); + + Promise.resolve(promise).then( + (value) => settle(() => resolve(value)), + (err) => settle(() => reject(err)), + ); + + function settle(done: () => void) { + if (settled) return; + settled = true; + clearTimeout(timer); + signal?.removeEventListener("abort", onAbort); + done(); + } + }); +} diff --git a/src/apps/frontend/src/hooks/usePackage.ts b/src/apps/frontend/src/hooks/usePackage.ts index dcb3cd5..9a17bde 100644 --- a/src/apps/frontend/src/hooks/usePackage.ts +++ b/src/apps/frontend/src/hooks/usePackage.ts @@ -1,18 +1,12 @@ import { useState, useEffect, useRef } from "react"; -import { useNetwork } from "../context/NetworkContext"; +import { useNetwork } from "../context/useNetwork"; +import { queryBulletinJson } from "../data/bulletin-client"; import type { Package } from "../data/types"; -import { connectIpfsGateway } from "@dotdm/env"; -import { queryContractByName, parseMetadata } from "../data/registry-queries"; +import { queryContractByName, parseMetadata, metadataCidFromUri } from "../data/registry-queries"; +import { withTimeout } from "../data/timeout"; export function usePackage(name: string | undefined) { - const { - registry, - connected, - connecting, - error: networkError, - network, - ipfsGatewayUrl, - } = useNetwork(); + const { registry, connected, connecting, error: networkError, networkConfig } = useNetwork(); const [pkg, setPkg] = useState(null); const [loading, setLoading] = useState(true); @@ -37,7 +31,10 @@ export function usePackage(name: string | undefined) { let cancelled = false; (async () => { try { - const result = await queryContractByName(registry, name); + const result = await withTimeout( + queryContractByName(registry, name), + `Registry package query timed out for ${name}.`, + ); if (cancelled) return; if (!result) { setNotFound(true); @@ -58,18 +55,21 @@ export function usePackage(name: string | undefined) { }, [registry, connected, name]); // Phase 2: IPFS metadata enrichment + const pkgName = pkg?.name; + const metadataUri = pkg?.metadataUri; + const metadataLoaded = pkg?.metadataLoaded; + useEffect(() => { - if (!pkg || !ipfsGatewayUrl || pkg.metadataLoaded) return; - if (!pkg.metadataUri || pkg.metadataUri.includes(":")) { + if (!pkgName || metadataLoaded) return; + const cid = metadataCidFromUri(metadataUri); + if (!cid) { setPkg((prev) => (prev ? { ...prev, metadataLoaded: true } : null)); return; } let cancelled = false; - connectIpfsGateway(ipfsGatewayUrl) - .fetch(pkg.metadataUri) - .then((r) => r.json()) - .then((metadata: any) => { + queryBulletinJson(networkConfig.productSdkEnvironment, cid) + .then((metadata) => { if (!cancelled) setPkg((prev) => (prev ? { ...prev, ...parseMetadata(metadata) } : null)); }) @@ -80,7 +80,13 @@ export function usePackage(name: string | undefined) { return () => { cancelled = true; }; - }, [pkg?.name, pkg?.metadataUri, pkg?.metadataLoaded, ipfsGatewayUrl]); + }, [pkgName, metadataUri, metadataLoaded, networkConfig.productSdkEnvironment]); - return { pkg, loading: loading || connecting, notFound, error: networkError ?? error, network }; + return { + pkg, + loading: loading || connecting, + notFound, + error: networkError ?? error, + networkConfig, + }; } diff --git a/src/apps/frontend/src/hooks/useRegistry.ts b/src/apps/frontend/src/hooks/useRegistry.ts index f3fc126..1291d9a 100644 --- a/src/apps/frontend/src/hooks/useRegistry.ts +++ b/src/apps/frontend/src/hooks/useRegistry.ts @@ -1,8 +1,9 @@ import { useState, useEffect, useCallback, useRef, useMemo } from "react"; -import { useNetwork } from "../context/NetworkContext"; +import { useNetwork } from "../context/useNetwork"; +import { queryBulletinJson } from "../data/bulletin-client"; import type { Package } from "../data/types"; -import { connectIpfsGateway } from "@dotdm/env"; -import { queryContractByName, parseMetadata } from "../data/registry-queries"; +import { queryContractByName, parseMetadata, metadataCidFromUri } from "../data/registry-queries"; +import { withTimeout } from "../data/timeout"; import { useInfiniteLoad } from "./useInfiniteLoad"; const PAGE_SIZE = 10; @@ -14,16 +15,19 @@ export function useRegistry() { connecting, error: networkError, network, - ipfsGatewayUrl, + networkConfig, } = useNetwork(); // Phase 1: Paginated on-chain data via useInfiniteLoad const fetchCount = useCallback(async () => { if (!registry) throw new Error("Registry not connected"); - const result = await registry.getContractCount.query(); + const result = await withTimeout( + registry.getContractCount.query(), + `Registry count query timed out on ${networkConfig.label}.`, + ); if (!result.success) throw new Error("Failed to query contract count"); return result.value as number; - }, [registry]); + }, [networkConfig.label, registry]); const fetchPage = useCallback( async (start: number, count: number) => { @@ -31,16 +35,22 @@ export function useRegistry() { const packages: Package[] = []; for (let i = start; i < start + count; i++) { - const nameResult = await registry.getContractNameAt.query(i); + const nameResult = await withTimeout( + registry.getContractNameAt.query(i), + `Registry name query timed out on ${networkConfig.label}.`, + ); if (!nameResult.success) continue; const name = nameResult.value as string; - const pkg = await queryContractByName(registry, name); + const pkg = await withTimeout( + queryContractByName(registry, name), + `Registry package query timed out for ${name}.`, + ); if (pkg) packages.push(pkg); } return packages; }, - [registry], + [networkConfig.label, registry], ); const { @@ -63,49 +73,40 @@ export function useRegistry() { const [metadataMap, setMetadataMap] = useState>>({}); const metadataAttempted = useRef>(new Set()); - // Reset metadata when the registry changes - useEffect(() => { - setMetadataMap({}); - metadataAttempted.current = new Set(); - }, [registry]); + const metadataKey = useCallback((pkg: Package) => `${network}:${pkg.name}`, [network]); useEffect(() => { - if (!ipfsGatewayUrl) return; - const toFetch = basePackages.filter( (p) => - p.metadataUri && - !p.metadataUri.includes(":") && - !metadataAttempted.current.has(p.name), + metadataCidFromUri(p.metadataUri) && !metadataAttempted.current.has(metadataKey(p)), ); if (toFetch.length === 0) return; for (const p of toFetch) { - metadataAttempted.current.add(p.name); + metadataAttempted.current.add(metadataKey(p)); } - const ipfs = connectIpfsGateway(ipfsGatewayUrl); - for (const pkg of toFetch) { - ipfs.fetch(pkg.metadataUri!) - .then((r) => r.json()) - .then((metadata: any) => { - setMetadataMap((prev) => ({ ...prev, [pkg.name]: parseMetadata(metadata) })); + const cid = metadataCidFromUri(pkg.metadataUri)!; + const key = metadataKey(pkg); + queryBulletinJson(networkConfig.productSdkEnvironment, cid) + .then((metadata) => { + setMetadataMap((prev) => ({ ...prev, [key]: parseMetadata(metadata) })); }) .catch(() => { setMetadataMap((prev) => ({ ...prev, - [pkg.name]: { metadataLoaded: true }, + [key]: { metadataLoaded: true }, })); }); } - }, [basePackages, ipfsGatewayUrl]); + }, [basePackages, metadataKey, networkConfig.productSdkEnvironment]); // Merge on-chain data with IPFS metadata const packages = useMemo( - () => basePackages.map((pkg) => ({ ...pkg, ...metadataMap[pkg.name] })), - [basePackages, metadataMap], + () => basePackages.map((pkg) => ({ ...pkg, ...metadataMap[metadataKey(pkg)] })), + [basePackages, metadataKey, metadataMap], ); const error = networkError ?? pageError; diff --git a/src/apps/frontend/src/lib/external-link.ts b/src/apps/frontend/src/lib/external-link.ts new file mode 100644 index 0000000..b7bcb5b --- /dev/null +++ b/src/apps/frontend/src/lib/external-link.ts @@ -0,0 +1,22 @@ +import { getTruApi } from "@parity/product-sdk-host"; + +/** + * Click handler for external anchor tags. Inside the Polkadot Desktop sandbox, + * routes navigation through `hostApi.navigateTo` so the shell opens the URL in + * a new tab. Outside Desktop, falls through to the anchor's default behavior. + */ +export function handleExternalClick(e: React.MouseEvent) { + const url = e.currentTarget.href; + e.preventDefault(); + getTruApi() + .then((truApi) => { + if (truApi) { + return truApi.navigateTo({ tag: "v1", value: url }); + } + window.open(url, "_blank", "noopener,noreferrer"); + return null; + }) + .catch(() => { + window.open(url, "_blank", "noopener,noreferrer"); + }); +} diff --git a/src/apps/frontend/src/pages/HomePage.css b/src/apps/frontend/src/pages/HomePage.css index 16b0395..10cfb19 100644 --- a/src/apps/frontend/src/pages/HomePage.css +++ b/src/apps/frontend/src/pages/HomePage.css @@ -1,169 +1,63 @@ -.hero-grain-wrapper { - position: relative; - width: 100%; - overflow: hidden; -} - -.hero { - position: relative; - z-index: 1; - padding: 60px 20px; - text-align: center; -} - -.hero-inner { - max-width: 700px; +.registry-home { + max-width: 1140px; margin: 0 auto; + padding: 0 32px; + background: #000000; } -.hero-tagline { - font-size: 2.5rem; - color: var(--color-text-primary); - margin-bottom: 8px; - line-height: 1.2; -} - -.hero-subtitle { - font-size: 1.1rem; - color: var(--color-text-secondary); - margin-bottom: 24px; -} - -.install-widget { - margin-top: 48px; - margin-bottom: 16px; - display: inline-flex; - flex-direction: column; - align-items: flex-start; - gap: 8px; +.registry-hero { + padding: 20vh 0 78px; } -.install-widget-title { - font-size: 0.8rem; - color: var(--color-text-primary); - font-weight: 700; - letter-spacing: 0.03em; +.hero-inner { + max-width: 640px; } -.install-line { - display: inline-flex; +.hero-title { + display: flex; align-items: center; - gap: 10px; - background: var(--color-surface); - border: 1px solid var(--color-border-strong); - border-radius: 8px; - padding: 12px 16px; - font-family: var(--font-code); - font-size: 0.9rem; - cursor: pointer; - transition: border-color 0.15s; - max-width: 100%; -} - -.install-line:hover { - border-color: var(--color-text-tertiary); -} - -.install-line-prompt { - color: var(--accent); - font-weight: 700; - user-select: none; -} - -.install-line-cmd { - color: var(--color-text-primary); - user-select: none; -} - -.install-line-icon { - color: var(--color-text-tertiary); - flex-shrink: 0; - transition: color 0.15s; -} - -.install-line:hover .install-line-icon { - color: var(--color-text-secondary); -} - -.install-line-copied, -.install-line-copied:hover { - border-color: #4ade80aa; -} - -.install-line-copied .install-line-icon, -.install-line-copied:hover .install-line-icon { - color: #4ade80; -} - -.install-line { - position: relative; -} - -.install-line-tooltip { - position: absolute; - top: -28px; - left: 50%; - transform: translateX(-50%); - color: #4ade80; + gap: 12px; + margin: 0 0 28px; + padding-left: 22px; + color: #ffffff; font-family: var(--font-sans); - font-size: 0.75rem; - white-space: nowrap; - user-select: none; - opacity: 0; - transition: opacity 0.15s; - pointer-events: none; + font-size: clamp(1.85rem, 3vw, 2.35rem); + font-weight: 500; + letter-spacing: -0.02em; + line-height: 1.1; } -.install-line-tooltip-visible { - opacity: 1; +.hero-title-logo { + width: 1.05em; + height: 1.05em; + margin-left: -4px; + object-fit: contain; + flex-shrink: 0; } -.stats-row { - position: relative; - z-index: 1; +.hero-actions { display: flex; - justify-content: center; - gap: 60px; - padding: 32px 20px; - border-bottom: 1px solid var(--color-border); -} - -.stat-item { - text-align: center; -} - -.stat-value { - font-size: 2rem; - font-weight: 700; - color: var(--color-text-primary); -} - -.stat-label { - font-size: 0.9rem; - color: var(--color-text-secondary); - margin-top: 4px; + flex-direction: column; + gap: 10px; } -.featured-section { - max-width: 1200px; - margin: 0 auto; - padding: 40px 20px; - min-height: 600px; +.registry-list { + margin: 0 22px; + padding: 46px 0 96px; } -.featured-title { - font-size: 1.5rem; - color: var(--color-text-primary); - margin-bottom: 24px; -} +@media (max-width: 900px) { + .registry-home { + padding: 0 24px; + } -@media (max-width: 768px) { - .hero-tagline { - font-size: 1.8rem; + .registry-hero { + padding: 16vh 0 64px; } +} - .stats-row { - flex-direction: column; - gap: 20px; +@media (max-width: 640px) { + .hero-title { + margin-bottom: 26px; } } diff --git a/src/apps/frontend/src/pages/HomePage.tsx b/src/apps/frontend/src/pages/HomePage.tsx index 5c2c8c4..7ea77ff 100644 --- a/src/apps/frontend/src/pages/HomePage.tsx +++ b/src/apps/frontend/src/pages/HomePage.tsx @@ -1,89 +1,68 @@ import { useState } from "react"; +import { useNavigate } from "react-router-dom"; import Layout from "../components/Layout"; import ContractGrid from "../components/ContractGrid"; -import GrainCanvas from "../components/GrainCanvas"; -import { CopyIcon, CheckIcon } from "../components/Icons"; -import { useNetwork } from "../context/NetworkContext"; +import CommandBox from "../components/CommandBox"; +import SearchBox from "../components/SearchBox"; +import logo from "../assets/logo.png"; +import { useNetwork } from "../context/useNetwork"; import { useRegistry } from "../hooks/useRegistry"; import "./HomePage.css"; export default function HomePage() { - const { network, connecting, error: networkError } = useNetwork(); - const { - packages, - loading, - error: registryError, - totalCount, - hasMore, - loadMore, - } = useRegistry(); - const [copied, setCopied] = useState(false); + const { networkConfig, connecting, error: networkError } = useNetwork(); + const { packages, loading, error: registryError, hasMore, loadMore } = useRegistry(); + const [query, setQuery] = useState(""); + const navigate = useNavigate(); const installCmd = "curl -fsSL https://raw.githubusercontent.com/paritytech/contract-dependency-manager/main/install.sh | bash"; const error = networkError || registryError; - const handleCopy = () => { - navigator.clipboard.writeText(installCmd); - setCopied(true); - setTimeout(() => setCopied(false), 2000); + const handleSearch = (value: string) => { + const trimmed = value.trim(); + if (trimmed) navigate(`/search?q=${encodeURIComponent(trimmed)}`); }; return ( -
- -
+
+
-

Build the future

-

The world's largest smart contract library

-
- Install cdm v0.0.1 -
- $ - {installCmd} - {copied ? ( - - ) : ( - - )} - - Copied! - -
+

+ + Contract Registry +

+ +
+ +
-
-
-
{totalCount.toLocaleString()}
-
Contracts
-
-
-
0
-
Weekly Calls
-
+
+
- -
-

Featured Contracts

- -
); } diff --git a/src/apps/frontend/src/pages/PackagePage.css b/src/apps/frontend/src/pages/PackagePage.css index 91971f5..4fb9722 100644 --- a/src/apps/frontend/src/pages/PackagePage.css +++ b/src/apps/frontend/src/pages/PackagePage.css @@ -1,485 +1,759 @@ .package-page { - max-width: 1200px; + display: grid; + grid-template-columns: minmax(0, 1fr) 260px; + gap: 64px; + max-width: 1140px; margin: 0 auto; - padding: 32px 20px; - display: flex; - gap: 40px; + padding: 56px calc(32px + 22px) 120px; + color: var(--color-text-secondary); } .package-main { - flex: 1; min-width: 0; + display: flex; + flex-direction: column; + gap: 18px; } .package-sidebar { - width: 300px; - flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 22px; + align-self: start; + position: sticky; + top: 32px; + padding-top: 6px; } +/* ============ Title ============ */ + .package-title-row { display: flex; align-items: baseline; - gap: 12px; - margin-bottom: 16px; + gap: 14px; flex-wrap: wrap; } -.package-name { - font-size: 2rem; - font-weight: 400; - color: var(--color-text-primary); +.package-title { + display: inline-flex; + align-items: baseline; + flex-wrap: wrap; + margin: 0; + color: #ffffff; font-family: var(--font-serif); - letter-spacing: -0.02em; + font-size: clamp(2.1rem, 3.5vw, 2.6rem); + font-weight: 400; + letter-spacing: 0; + line-height: 1.1; + min-width: 0; + max-width: 100%; } -.package-version-badge { +.package-name-prefix { + color: var(--color-text-tertiary); + font-weight: 400; + overflow-wrap: anywhere; +} + +.package-name-leaf { + color: #ffffff; + font-weight: 400; + overflow-wrap: anywhere; +} + +.package-version-pill { display: inline-block; - padding: 2px 10px; - background: rgba(5, 150, 105, 0.15); - color: var(--success); - border-radius: 4px; - font-size: 0.85rem; - font-weight: 600; + padding: 3px 9px; + border: 1px solid var(--color-border-strong); + border-radius: 999px; + color: var(--color-text-secondary); + font-family: var(--font-sans); + font-size: 12px; + font-weight: 500; + line-height: 1.1; +} + +.package-description { + margin: 0; + max-width: 720px; + color: var(--color-text-secondary); + font-size: 16px; + line-height: 1.55; } -.install-box { +.package-description-skeleton { display: flex; + flex-direction: column; + gap: 8px; + max-width: 720px; +} + +/* ============ Install block ============ */ + +.install-block { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; - background: var(--color-surface); - color: var(--color-text-primary); - border-radius: 8px; + gap: 12px; + width: 100%; + max-width: 640px; + margin: 8px 0 8px; padding: 12px 16px; - margin-bottom: 24px; - font-family: var(--font-code); - font-size: 0.9rem; + background: rgba(255, 255, 255, 0.04); border: 1px solid var(--color-border); + border-radius: 10px; + color: var(--color-text-secondary); + font-family: var(--font-code); + font-size: 13px; + text-align: left; + cursor: pointer; + transition: + background-color 0.1s ease, + border-color 0.1s ease; } -.install-command { - flex: 1; - user-select: all; +.install-block:hover { + background: rgba(255, 255, 255, 0.06); + border-color: var(--color-border-strong); } -.install-copy-btn { - background: none; - border: 1px solid var(--color-border-strong); - color: var(--color-text-secondary); - padding: 4px 12px; - border-radius: 4px; - cursor: pointer; - font-size: 0.8rem; - font-family: inherit; - margin-left: 12px; +.install-block-prompt { + color: var(--accent); + font-weight: 700; + user-select: none; +} + +.install-block-cmd { + min-width: 0; + overflow: hidden; + color: #ffffff; + font-family: var(--font-code); + font-size: 13px; + text-overflow: ellipsis; white-space: nowrap; + user-select: none; } -.install-copy-btn:hover { - border-color: var(--color-text-primary); - color: var(--color-text-primary); +.install-block-icon { + display: inline-flex; + width: 14px; + height: 14px; + color: var(--color-text-tertiary); + transition: color 0.1s ease; +} + +.install-block:hover .install-block-icon { + color: var(--color-text-secondary); +} + +.install-block-icon--copied, +.install-block:hover .install-block-icon--copied { + color: #22c55e; +} + +.install-block-icon svg { + width: 14px; + height: 14px; +} + +/* ============ Body / tabs ============ */ + +.package-body { + display: flex; + flex-direction: column; + gap: 28px; + margin-top: 18px; } .package-tabs { display: flex; + align-items: center; + gap: 24px; border-bottom: 1px solid var(--color-border); - margin-bottom: 24px; - gap: 0; } .package-tab { - padding: 10px 20px; - font-size: 0.95rem; - cursor: pointer; - border: none; - background: none; + display: inline-flex; + align-items: center; + gap: 8px; + margin: 0 0 -1px; + padding: 10px 0; + background: transparent; + border: 0; + border-bottom: 2px solid transparent; color: var(--color-text-secondary); + font-family: var(--font-sans); + font-size: 15px; font-weight: 600; - border-bottom: 2px solid transparent; - margin-bottom: -1px; - font-family: inherit; + letter-spacing: -0.01em; + cursor: pointer; + transition: + color 0.1s ease, + border-color 0.1s ease; } .package-tab:hover { color: var(--color-text-primary); } -.package-tab.active { +.package-tab--active { color: var(--color-text-primary); border-bottom-color: var(--accent); } -.package-readme { - line-height: 1.7; - word-wrap: break-word; +.package-tab-count { + display: inline-block; + padding: 1px 7px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.06); + color: var(--color-text-tertiary); + font-size: 11px; + font-weight: 600; + line-height: 1.4; +} + +.package-tab--active .package-tab-count { + background: rgba(255, 255, 255, 0.12); color: var(--color-text-secondary); } -.package-readme pre { - background: var(--color-surface); - padding: 16px; - border-radius: 8px; - overflow-x: auto; - margin: 16px 0; - font-size: 0.85rem; - line-height: 1.5; - border: 1px solid var(--color-border); +.package-content { + min-height: 220px; } -.package-readme code { - font-family: var(--font-code); - font-size: 0.85rem; +.package-empty { + margin: 0; + padding: 12px 0; + color: var(--color-text-tertiary); + font-size: 14px; } -.package-readme h1 { - font-size: 1.8rem; - margin: 24px 0 12px; - padding-bottom: 8px; +/* ============ Sidebar ============ */ + +.sidebar-section { + display: flex; + flex-direction: column; + gap: 6px; + padding-bottom: 22px; border-bottom: 1px solid var(--color-border); - line-height: 1.2; } -.package-readme h2 { - font-size: 1.4rem; - margin: 20px 0 10px; - line-height: 1.25; +.sidebar-section:last-child { + border-bottom: 0; + padding-bottom: 0; } -.package-readme h3 { - font-size: 1.1rem; - margin: 16px 0 8px; - line-height: 1.3; - letter-spacing: -0.01em; +.sidebar-label { + color: var(--color-text-quaternary); + font-family: var(--font-sans); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; } -.package-readme p { - margin: 12px 0; +.sidebar-value { + color: var(--color-text-secondary); + font-family: var(--font-sans); + font-size: 14px; + line-height: 1.4; + word-break: break-word; } -.package-readme ul, -.package-readme ol { - margin: 12px 0; - padding-left: 24px; +.sidebar-value--mono { + font-family: var(--font-code); + font-size: 13px; } -.package-readme li { - margin: 4px 0; +.sidebar-link { + color: var(--color-text-secondary); + font-family: var(--font-sans); + font-size: 14px; + line-height: 1.4; + text-decoration: none; + word-break: break-all; + transition: color 0.1s ease; } -.package-readme img { - max-width: 100%; +.sidebar-link:hover { + color: var(--color-text-primary); + text-decoration: none; } -.package-readme table { - width: 100%; - border-collapse: collapse; - margin: 16px 0; - font-size: 0.9rem; +.sidebar-address { + display: inline-flex; + align-items: center; + gap: 8px; + margin: 0; + padding: 4px 8px 4px 0; + background: transparent; + border: 0; + color: var(--color-text-secondary); + font-family: var(--font-sans); + font-size: 13px; + cursor: pointer; + transition: color 0.1s ease; + align-self: flex-start; } -.package-readme thead th { - text-align: left; - padding: 10px 12px; - background: var(--color-surface-secondary); +.sidebar-address:hover { color: var(--color-text-primary); - font-weight: 600; - border-bottom: 2px solid var(--color-border-strong); } -.package-readme tbody td { - padding: 8px 12px; - border-bottom: 1px solid var(--color-border); - color: var(--color-text-secondary); +.sidebar-address-text { + font-family: var(--font-code); + font-size: 13px; } -.package-readme tbody tr:hover { - background: var(--color-surface); +.sidebar-address-icon { + display: inline-flex; + width: 12px; + height: 12px; + color: var(--color-text-tertiary); } -.deps-list { - list-style: none; +.sidebar-address-icon--copied { + color: #22c55e; } -.deps-list li { - padding: 8px 0; - border-bottom: 1px solid var(--color-border); - font-size: 0.95rem; +.sidebar-address-icon svg { + width: 12px; + height: 12px; } -.deps-list li a { - font-weight: 600; +.sidebar-keywords { + display: flex; + flex-wrap: wrap; + gap: 6px; } -.deps-list li span { - color: var(--color-text-tertiary); - margin-left: 8px; +.sidebar-keyword { + display: inline-block; + padding: 3px 9px; + background: rgba(255, 255, 255, 0.04); + border-radius: 999px; + color: var(--color-text-secondary); + font-size: 12px; + text-decoration: none; + transition: + background-color 0.1s ease, + color 0.1s ease; +} + +.sidebar-keyword:hover { + background: rgba(255, 255, 255, 0.08); + color: var(--color-text-primary); + text-decoration: none; } -.deps-empty { +/* ============ Status pages ============ */ + +.package-status { + max-width: 600px; + margin: 0 auto; + padding: 80px calc(32px + 22px); +} + +.package-status-title { + margin: 0 0 12px; + color: #ffffff; + font-family: var(--font-sans); + font-size: 1.6rem; + font-weight: 500; + letter-spacing: -0.02em; +} + +.package-status-text { + margin: 0 0 8px; color: var(--color-text-secondary); - font-style: italic; + font-size: 15px; + line-height: 1.5; } -.versions-table { - width: 100%; - border-collapse: collapse; +.package-status-text code { + padding: 1px 6px; + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; + font-family: var(--font-code); + font-size: 13px; + color: var(--color-text-primary); } -.versions-table th { - text-align: left; - padding: 10px 12px; - border-bottom: 2px solid var(--color-border); - font-size: 0.85rem; +.package-status-detail { + margin: 12px 0 0; + font-family: var(--font-code); + font-size: 12px; color: var(--color-text-tertiary); - text-transform: uppercase; - letter-spacing: 0.05em; } -.versions-table td { - padding: 10px 12px; - border-bottom: 1px solid var(--color-border); - font-size: 0.95rem; +.package-status-link { + display: inline-block; + margin-top: 20px; color: var(--color-text-secondary); + font-size: 14px; + text-decoration: none; + transition: color 0.1s ease; } -.sidebar-section { - margin-bottom: 24px; - padding-bottom: 24px; - border-bottom: 1px solid var(--color-border); +.package-status-link:hover { + color: var(--color-text-primary); } -.sidebar-section:last-child { - border-bottom: none; +/* ============ Dependencies & versions ============ */ + +.package-deps, +.package-versions { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; } -.sidebar-section-title { - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-text-tertiary); - margin-bottom: 8px; - font-weight: 600; +.package-dep { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 12px 14px; + margin: 0 -14px; + border-radius: 8px; + color: inherit; + text-decoration: none; + transition: background-color 0.1s ease; } -.sidebar-value { - font-size: 1rem; - color: var(--color-text-secondary); +.package-dep:hover { + background: rgba(255, 255, 255, 0.04); + text-decoration: none; } -.sidebar-value.address-value { - font-family: var(--font-mono, monospace); - font-size: 0.85rem; - display: inline-flex; +.package-dep-name { + color: var(--color-text-primary); + font-size: 14px; + font-weight: 500; +} + +.package-dep-version { + color: var(--color-text-tertiary); + font-family: var(--font-code); + font-size: 12px; +} + +.package-version-row { + display: flex; align-items: center; - gap: 20px; - cursor: pointer; - border-radius: 4px; - padding: 4px 6px; - margin: -4px -6px; + gap: 16px; + padding: 10px 0; + border-bottom: 1px solid var(--color-border); + font-size: 14px; } -.sidebar-value.address-value:hover { - background: var(--color-surface-secondary); +.package-version-row:last-child { + border-bottom: 0; } -.address-copy-icon { +.package-version-num { + color: var(--color-text-primary); + font-family: var(--font-code); + font-size: 13px; +} + +.package-version-date { + flex: 1; color: var(--color-text-tertiary); - flex-shrink: 0; + font-size: 13px; +} + +.package-version-current { + padding: 2px 8px; + border: 1px solid rgba(34, 197, 94, 0.45); + border-radius: 999px; + color: rgba(34, 197, 94, 0.85); + font-size: 11px; + letter-spacing: 0.02em; } -.sidebar-value.address-value:hover .address-copy-icon { +/* ============ Readme ============ */ + +.package-readme { + max-width: 720px; color: var(--color-text-secondary); + font-family: var(--font-sans); + font-size: 15px; + line-height: 1.6; + letter-spacing: -0.005em; + word-wrap: break-word; } -.sidebar-value.large { - font-size: 1.5rem; +.package-readme h1, +.package-readme h2, +.package-readme h3, +.package-readme h4 { + color: var(--color-text-primary); + font-family: var(--font-sans); + font-weight: 600; + letter-spacing: -0.015em; + line-height: 1.25; +} + +.package-readme h1 { + font-size: 1.75rem; font-weight: 700; + margin: 32px 0 14px; + padding-bottom: 8px; + border-bottom: 1px solid var(--color-border); } -.downloads-chart { - display: flex; - align-items: flex-end; - gap: 4px; - height: 60px; - margin-top: 12px; +.package-readme h1:first-child { + margin-top: 0; } -.chart-bar { - flex: 1; - background: var(--accent); - border-radius: 2px 2px 0 0; - min-height: 4px; - opacity: 0.7; +.package-readme h2 { + font-size: 1.35rem; + margin: 28px 0 10px; } -.chart-bar:last-child { - opacity: 1; +.package-readme h3 { + font-size: 1.1rem; + margin: 22px 0 8px; } -.sidebar-keywords { - display: flex; - flex-wrap: wrap; - gap: 6px; +.package-readme h4 { + font-size: 1rem; + margin: 18px 0 6px; } -.sidebar-keyword { - display: inline-block; - padding: 3px 10px; - background: var(--color-surface-secondary); - color: var(--color-text-secondary); - border-radius: 9999px; - font-size: 0.8rem; - text-decoration: none; +.package-readme p { + margin: 10px 0; } -.sidebar-keyword:hover { - background: var(--color-border-strong); - text-decoration: none; +.package-readme p:first-child { + margin-top: 0; } -.sidebar-link { - display: block; - font-size: 0.95rem; - margin-bottom: 6px; +.package-readme a { + color: var(--color-text-primary); + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-color: rgba(255, 255, 255, 0.25); } -.package-not-found { - text-align: center; - padding: 80px 20px; +.package-readme a:hover { + text-decoration-color: var(--color-text-primary); +} + +.package-readme code { + padding: 1px 6px; + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; + font-family: var(--font-code); + font-size: 0.85em; + color: var(--color-text-primary); +} + +.package-readme pre { + margin: 18px 0; + padding: 16px 18px; + background: rgba(255, 255, 255, 0.03); + border-radius: 10px; + overflow-x: auto; + font-size: 13px; + line-height: 1.5; +} + +.package-readme pre code { + padding: 0; + background: transparent; +} + +.package-readme ul, +.package-readme ol { + margin: 12px 0; + padding-left: 22px; +} + +.package-readme li { + margin: 4px 0; +} + +.package-readme img { + max-width: 100%; + border-radius: 6px; } -.package-not-found h2 { - font-size: 1.8rem; - margin-bottom: 12px; +.package-readme table { + width: 100%; + border-collapse: collapse; + margin: 18px 0; + font-size: 14px; +} + +.package-readme thead th { + text-align: left; + padding: 8px 12px; + border-bottom: 1px solid var(--color-border-strong); + color: var(--color-text-primary); + font-weight: 500; } -.package-not-found p { +.package-readme tbody td { + padding: 8px 12px; + border-bottom: 1px solid var(--color-border); color: var(--color-text-secondary); - margin-bottom: 20px; } -/* ABI Tab - Swagger-inspired layout */ +.package-readme-skeleton { + display: flex; + flex-direction: column; + gap: 12px; + padding-top: 8px; +} + +/* ============ ABI ============ */ .abi-tab { display: flex; flex-direction: column; - gap: 24px; + gap: 28px; +} + +.abi-section { + display: flex; + flex-direction: column; + gap: 10px; } .abi-section-title { - font-size: 0.8rem; + margin: 0; + color: var(--color-text-quaternary); + font-family: var(--font-sans); + font-size: 11px; + font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; - color: var(--color-text-tertiary); - font-weight: 700; - margin-bottom: 8px; - font-family: var(--font-sans); +} + +.abi-section-entries { + display: flex; + flex-direction: column; + gap: 6px; } .abi-entry { - border: 1px solid var(--color-border); - border-radius: 6px; - margin-bottom: 4px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.03); overflow: hidden; - transition: border-color 0.15s ease; + transition: background-color 0.1s ease; } .abi-entry:hover { - border-color: var(--color-border-strong); + background: rgba(255, 255, 255, 0.05); } -.abi-entry.expanded { - border-color: var(--color-border-strong); +.abi-entry--expanded { + background: rgba(255, 255, 255, 0.05); } .abi-entry-header { display: flex; align-items: center; - gap: 10px; + gap: 12px; width: 100%; padding: 10px 14px; - background: var(--color-surface); - border: none; + margin: 0; + background: transparent; + border: 0; cursor: pointer; text-align: left; font-family: inherit; color: var(--color-text-primary); } -.abi-entry-header:hover { - background: var(--color-surface-secondary); +.abi-fn-signature { + font-family: var(--font-code); + font-size: 13px; + color: var(--color-text-primary); + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .abi-badge { display: inline-block; padding: 2px 8px; - border-radius: 3px; - font-size: 0.65rem; - font-weight: 700; + border-radius: 999px; + font-size: 10px; + font-weight: 600; letter-spacing: 0.04em; + text-transform: uppercase; white-space: nowrap; min-width: 72px; text-align: center; flex-shrink: 0; } -.abi-badge.view { - background: rgba(5, 150, 105, 0.15); +.abi-badge--view { + background: rgba(34, 197, 94, 0.12); color: #34d399; } -.abi-badge.nonpayable { - background: rgba(59, 130, 246, 0.15); - color: #60a5fa; +.abi-badge--nonpayable { + background: rgba(96, 165, 250, 0.12); + color: #93c5fd; } -.abi-badge.payable { - background: rgba(245, 158, 11, 0.15); +.abi-badge--payable { + background: rgba(245, 158, 11, 0.14); color: #fbbf24; } -.abi-fn-signature { - font-family: var(--font-code); - font-size: 0.85rem; - color: var(--color-text-primary); - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +.abi-badge--event { + background: rgba(168, 85, 247, 0.13); + color: #c4b5fd; +} + +.abi-badge--error { + background: rgba(248, 113, 113, 0.13); + color: #fca5a5; } .abi-expand-icon { - font-size: 0.6rem; color: var(--color-text-tertiary); flex-shrink: 0; + transition: transform 0.18s ease; } -.abi-entry-body { - padding: 16px 14px; - border-top: 1px solid var(--color-border); - background: var(--color-bg); +.abi-expand-icon--open { + transform: rotate(180deg); } -.abi-params-section { - margin-bottom: 12px; +.abi-entry-body { + padding: 4px 14px 16px; + display: flex; + flex-direction: column; + gap: 14px; } -.abi-params-section:last-child { - margin-bottom: 0; +.abi-params-section { + display: flex; + flex-direction: column; + gap: 6px; } .abi-params-label { - font-size: 0.75rem; + color: var(--color-text-quaternary); + font-size: 11px; + font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; - color: var(--color-text-tertiary); - font-weight: 600; - margin-bottom: 6px; } .abi-params-none { - font-size: 0.85rem; color: var(--color-text-tertiary); - font-style: italic; + font-size: 13px; } .abi-params-table { @@ -488,33 +762,34 @@ } .abi-params-table th { - text-align: left; - padding: 4px 10px; - font-size: 0.7rem; - color: var(--color-text-tertiary); - text-transform: uppercase; - letter-spacing: 0.05em; + padding: 6px 0; border-bottom: 1px solid var(--color-border); + color: var(--color-text-quaternary); + font-size: 11px; font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + text-align: left; } .abi-params-table td { - padding: 6px 10px; - font-size: 0.85rem; + padding: 6px 0; border-bottom: 1px solid var(--color-border); + font-size: 13px; +} + +.abi-params-table tr:last-child td { + border-bottom: 0; } .abi-params-table td code { + padding: 0; + background: transparent; font-family: var(--font-code); - font-size: 0.82rem; + font-size: 12px; color: var(--color-text-secondary); } -.abi-params-table tr:last-child td { - border-bottom: none; -} - -/* Tuple / nested struct rendering */ .abi-tuple-type { display: inline-flex; flex-direction: column; @@ -528,7 +803,6 @@ .abi-tuple-field { display: flex; align-items: baseline; - gap: 0; line-height: 1.6; } @@ -541,15 +815,93 @@ } .abi-type-name { - color: #34d399; + color: #93c5fd; +} + +.abi-skeleton { + display: flex; + flex-direction: column; + gap: 6px; +} + +/* ============ Skeleton sizes ============ */ + +.skeleton-bar--title { + width: 280px; + height: 32px; + border-radius: 6px; +} + +.skeleton-bar--install { + width: 100%; + max-width: 640px; + height: 48px; + border-radius: 10px; +} + +.skeleton-bar--readme { + width: 100%; + height: 13px; +} + +.skeleton-bar--readme-short { + width: 65%; +} + +.skeleton-bar--abi-row { + width: 100%; + height: 40px; + border-radius: 10px; +} + +.skeleton-bar--side-label { + width: 70px; + height: 10px; +} + +.skeleton-bar--side-value { + width: 140px; + height: 13px; } -@media (max-width: 768px) { +/* ============ Responsive ============ */ + +@media (max-width: 1024px) { .package-page { - flex-direction: column; + grid-template-columns: minmax(0, 1fr); + gap: 32px; } .package-sidebar { - width: 100%; + position: static; + flex-direction: row; + flex-wrap: wrap; + gap: 24px 36px; + padding-top: 12px; + border-top: 1px solid var(--color-border); + order: 2; + } + + .package-sidebar .sidebar-section { + min-width: 160px; + } +} + +@media (max-width: 900px) { + .package-page { + padding: 40px calc(24px + 22px) 96px; + } + + .package-status { + padding: 60px calc(24px + 22px); + } + + .package-tabs { + overflow-x: auto; + scrollbar-width: none; + } + + .package-tabs::-webkit-scrollbar { + display: none; } } diff --git a/src/apps/frontend/src/pages/PackagePage.tsx b/src/apps/frontend/src/pages/PackagePage.tsx index c50e787..cdacc99 100644 --- a/src/apps/frontend/src/pages/PackagePage.tsx +++ b/src/apps/frontend/src/pages/PackagePage.tsx @@ -4,8 +4,10 @@ import { marked } from "marked"; import DOMPurify from "dompurify"; import Layout from "../components/Layout"; import { CopyIcon, CheckIcon } from "../components/Icons"; +import { handleExternalClick } from "../lib/external-link"; import { usePackage } from "../hooks/usePackage"; -import type { AbiEntry, AbiParam } from "../data/types"; +import type { AbiEntry, AbiParam, Package } from "../data/types"; +import "../components/SkeletonCard.css"; import "./PackagePage.css"; marked.setOptions({ gfm: true, breaks: true }); @@ -13,8 +15,9 @@ marked.setOptions({ gfm: true, breaks: true }); type TabName = "readme" | "abi" | "dependencies" | "versions"; function formatParamType(param: AbiParam): string { - if (param.type === "tuple" && param.components) { - return `(${param.components.map((c) => formatParamType(c)).join(", ")})`; + if (param.type.startsWith("tuple") && param.components) { + const suffix = param.type.slice("tuple".length); + return `(${param.components.map((c) => formatParamType(c)).join(", ")})${suffix}`; } return param.type; } @@ -24,30 +27,54 @@ function formatSignature(entry: AbiEntry): string { const params = (entry.inputs ?? []).map((p) => formatParamType(p)).join(", "); const returns = (entry.outputs ?? []).filter((o) => o.type); const returnStr = - returns.length > 0 ? ` \u2192 ${returns.map((r) => formatParamType(r)).join(", ")}` : ""; + returns.length > 0 ? ` → ${returns.map((r) => formatParamType(r)).join(", ")}` : ""; return `${name}(${params})${returnStr}`; } -function getBadgeClass(mutability?: string): string { - switch (mutability) { +function getBadgeLabel(entry: AbiEntry): string { + if (entry.type !== "function" && entry.type !== "constructor") { + return entry.type.toUpperCase(); + } + return (entry.stateMutability ?? "nonpayable").toUpperCase(); +} + +function getBadgeClass(entry: AbiEntry): string { + if (entry.type === "event") return "abi-badge abi-badge--event"; + if (entry.type === "error") return "abi-badge abi-badge--error"; + + switch (entry.stateMutability) { case "view": case "pure": - return "abi-badge view"; + return "abi-badge abi-badge--view"; case "payable": - return "abi-badge payable"; + return "abi-badge abi-badge--payable"; default: - return "abi-badge nonpayable"; + return "abi-badge abi-badge--nonpayable"; } } +function splitName(name: string): { prefix: string; leaf: string } { + const idx = name.lastIndexOf("/"); + if (idx < 0) return { prefix: "", leaf: name }; + return { prefix: name.slice(0, idx + 1), leaf: name.slice(idx + 1) }; +} + +function shortAddress(addr: string): string { + if (addr.length <= 14) return addr; + return `${addr.slice(0, 8)}…${addr.slice(-6)}`; +} + function ParamType({ param, depth = 0 }: { param: AbiParam; depth?: number }) { - if (param.type === "tuple" && param.components && param.components.length > 0) { + if (param.type.startsWith("tuple") && param.components && param.components.length > 0) { + const suffix = param.type.slice("tuple".length); + return ( {"{"} {param.components.map((c, i) => ( {"}"} + {suffix} ); @@ -73,13 +101,25 @@ function AbiEntryCard({ entry }: { entry: AbiEntry }) { const outputs = entry.outputs ?? []; return ( -
- {expanded && (
@@ -97,6 +137,7 @@ function AbiEntryCard({ entry }: { entry: AbiEntry }) { {inputs.map((p, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: param index is stable {p.name || `_${i}`} @@ -124,6 +165,7 @@ function AbiEntryCard({ entry }: { entry: AbiEntry }) { {outputs.map((p, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: param index is stable {p.name || `_${i}`} @@ -147,7 +189,7 @@ function AbiEntryCard({ entry }: { entry: AbiEntry }) { function AbiTab({ abi }: { abi: AbiEntry[] }) { if (abi.length === 0) { - return

No ABI entries found.

; + return

No ABI entries published.

; } const grouped = new Map(); @@ -157,7 +199,6 @@ function AbiTab({ abi }: { abi: AbiEntry[] }) { grouped.get(key)!.push(entry); } - // Order: constructor first, then functions, then everything else const order = ["constructor", "function", "event", "error", "fallback", "receive"]; const sortedKeys = [...grouped.keys()].sort((a, b) => { const ai = order.indexOf(a); @@ -177,46 +218,232 @@ function AbiTab({ abi }: { abi: AbiEntry[] }) { return (
{sortedKeys.map((key) => ( -
+

{sectionLabels[key] ?? key}

- {grouped.get(key)!.map((entry, i) => ( - - ))} -
+
+ {grouped.get(key)!.map((entry, i) => ( + + ))} +
+
))}
); } +interface InstallBlockProps { + command: string; +} + +function InstallBlock({ command }: InstallBlockProps) { + const [copied, setCopied] = useState(false); + return ( + + ); +} + +interface AddressLineProps { + address: string; +} + +function AddressLine({ address }: AddressLineProps) { + const [copied, setCopied] = useState(false); + return ( + + ); +} + +interface PackageBodyProps { + pkg: Package; + activeTab: TabName; + setActiveTab: (tab: TabName) => void; +} + +function PackageBody({ pkg, activeTab, setActiveTab }: PackageBodyProps) { + const depEntries = Object.entries(pkg.dependencies ?? {}); + const versions = pkg.versions ?? []; + const hasVersions = versions.length > 0; + const metadataPending = !pkg.metadataLoaded && !!pkg.metadataUri; + + const tabs: { key: TabName; label: string; count?: number }[] = [ + { key: "readme", label: "Readme" }, + { key: "abi", label: "ABI", count: pkg.abi?.length }, + { key: "dependencies", label: "Dependencies", count: depEntries.length }, + { key: "versions", label: "Versions" }, + ]; + + return ( +
+ + +
+ {activeTab === "readme" && + (pkg.readme ? ( +
+ ) : metadataPending ? ( +
+ + + + + +
+ ) : ( +

No readme published.

+ ))} + + {activeTab === "abi" && + (pkg.abi && pkg.abi.length > 0 ? ( + + ) : metadataPending ? ( +
+ {Array.from({ length: 5 }).map((_, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: fixed decorative array + + ))} +
+ ) : ( +

No ABI published.

+ ))} + + {activeTab === "dependencies" && + (depEntries.length === 0 ? ( +

No dependencies.

+ ) : ( +
    + {depEntries.map(([depName, version]) => ( +
  • + + {depName} + {version} + +
  • + ))} +
+ ))} + + {activeTab === "versions" && ( +
    + {(hasVersions ? versions : [{ version: pkg.version, date: "" }]).map( + (v) => ( +
  • + v{v.version} + {v.date && ( + {v.date} + )} + {v.version === pkg.version && ( + current + )} +
  • + ), + )} +
+ )} +
+
+ ); +} + +function PackageSkeleton() { + return ( + + + + ); +} + export default function PackagePage() { const params = useParams(); const name = params["*"]; const [activeTab, setActiveTab] = useState("readme"); - const [copied, setCopied] = useState(false); - const [addrCopied, setAddrCopied] = useState(false); - const { pkg, loading, notFound, error, network } = usePackage(name); + const { pkg, loading, notFound, error, networkConfig } = usePackage(name); if (loading) { - return ( - -
-

Connecting to {network}...

-
-
- ); + return ; } if (error) { return ( -
-

Connection Error

-

- Could not connect to {network}. Check your connection - settings. +

+

Connection Error

+

+ Could not connect to {networkConfig.label}. Check your + connection settings.

-

{error}

+

{error}

); @@ -225,199 +452,110 @@ export default function PackagePage() { if (notFound || !pkg) { return ( -
-

404 - Contract Not Found

-

The contract “{name}” could not be found.

- Go back to home +
+

Contract not found

+

+ The contract {name} doesn’t exist on{" "} + {networkConfig.label}. +

+ + ← Back to registry +
); } - const namedPresets = ["polkadot", "paseo", "preview-net"]; - const installCmd = namedPresets.includes(network) - ? `cdm i -n ${network} ${pkg.name}` - : `cdm i ${pkg.name}`; - const handleCopy = () => { - navigator.clipboard.writeText(installCmd); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - - const depEntries = Object.entries(pkg.dependencies ?? {}); - const versions = pkg.versions ?? []; - const hasVersions = versions.length > 0; + const installCmd = `cdm i -n ${networkConfig.installName} ${pkg.name}`; + const split = splitName(pkg.name); + const metadataPending = !pkg.metadataLoaded && !!pkg.metadataUri; return ( -
+
-

{pkg.name}

- v{pkg.version} -
- -
- {installCmd} - -
- -
- {(["readme", "abi", "dependencies", "versions"] as TabName[]).map((tab) => ( - - ))} +

+ {split.prefix} + {split.leaf} +

+ v{pkg.version}
- {activeTab === "readme" && - (pkg.readme ? ( -
- ) : !pkg.metadataLoaded && pkg.metadataUri ? ( -

Loading readme...

- ) : ( -

No readme published yet.

- ))} - - {activeTab === "abi" && - (pkg.abi && pkg.abi.length > 0 ? ( - - ) : !pkg.metadataLoaded && pkg.metadataUri ? ( -

Loading ABI...

- ) : ( -

No ABI published yet.

- ))} + {pkg.description ? ( +

{pkg.description}

+ ) : metadataPending ? ( +
+ + +
+ ) : null} - {activeTab === "dependencies" && ( - <> - {depEntries.length === 0 ? ( -

This contract has no dependencies.

- ) : ( -
    - {depEntries.map(([depName, version]) => ( -
  • - {depName} - {version} -
  • - ))} -
- )} - - )} + - {activeTab === "versions" && - (hasVersions ? ( - - - - - - - - - {versions.map((v) => ( - - - - - ))} - -
VersionDate
v{v.version}{v.date}
- ) : ( - - - - - - - - - - - -
Version
v{pkg.version}
- ))} +
-
+
); } diff --git a/src/apps/frontend/src/pages/SearchPage.css b/src/apps/frontend/src/pages/SearchPage.css index fbf2cd4..d2667e6 100644 --- a/src/apps/frontend/src/pages/SearchPage.css +++ b/src/apps/frontend/src/pages/SearchPage.css @@ -1,7 +1,7 @@ .search-page { - max-width: 1000px; + max-width: 1140px; margin: 0 auto; - padding: 32px 20px; + padding: 32px calc(32px + 22px); } .search-header { diff --git a/src/apps/frontend/src/pages/SearchPage.tsx b/src/apps/frontend/src/pages/SearchPage.tsx index fceaf59..71972d9 100644 --- a/src/apps/frontend/src/pages/SearchPage.tsx +++ b/src/apps/frontend/src/pages/SearchPage.tsx @@ -2,7 +2,8 @@ import { useState, useMemo } from "react"; import { useSearchParams } from "react-router-dom"; import Layout from "../components/Layout"; import PackageCard from "../components/PackageCard"; -import { useNetwork } from "../context/NetworkContext"; +import { SkeletonCard } from "../components/SkeletonCard"; +import { useNetwork } from "../context/useNetwork"; import { useRegistry } from "../hooks/useRegistry"; import "./SearchPage.css"; @@ -12,7 +13,7 @@ export default function SearchPage() { const [searchParams] = useSearchParams(); const query = searchParams.get("q") || ""; const [sort, setSort] = useState("name"); - const { network, connecting, error: networkError } = useNetwork(); + const { networkConfig, connecting, error: networkError } = useNetwork(); const { packages, loading, error: registryError } = useRegistry(); const error = networkError || registryError; @@ -72,14 +73,17 @@ export default function SearchPage() {

Connection Error

- Could not connect to {network}. Check your connection - settings. + Could not connect to {networkConfig.label}. Check your + connection settings.

{error}

) : connecting || (loading && packages.length === 0) ? ( -
-

Connecting to {network}...

+
+ {Array.from({ length: 6 }).map((_, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: fixed-length decorative array + + ))}
) : results.length === 0 ? (
diff --git a/src/apps/frontend/src/pages/WidgetPage.tsx b/src/apps/frontend/src/pages/WidgetPage.tsx index 983bdd5..c4e93c2 100644 --- a/src/apps/frontend/src/pages/WidgetPage.tsx +++ b/src/apps/frontend/src/pages/WidgetPage.tsx @@ -1,17 +1,20 @@ import { useEffect } from "react"; import { useSearchParams } from "react-router-dom"; import ContractGrid from "../components/ContractGrid"; -import { useNetwork } from "../context/NetworkContext"; +import { useNetwork } from "../context/useNetwork"; import { useRegistry } from "../hooks/useRegistry"; +import { resolveNetworkKey } from "../config/networks"; + export default function WidgetPage() { const [searchParams] = useSearchParams(); - const { network, setNetwork, connecting, error: networkError } = useNetwork(); + const { networkConfig, setNetwork, connecting, error: networkError } = useNetwork(); const { packages, loading, error: registryError, hasMore, loadMore } = useRegistry(); const requestedNetwork = searchParams.get("network"); useEffect(() => { - if (requestedNetwork) { - setNetwork(requestedNetwork); + const nextNetwork = resolveNetworkKey(requestedNetwork); + if (nextNetwork) { + setNetwork(nextNetwork); } }, [requestedNetwork, setNetwork]); @@ -22,7 +25,7 @@ export default function WidgetPage() { loading={loading} hasMore={hasMore} loadMore={loadMore} - network={network} + network={networkConfig.label} connecting={connecting} error={networkError || registryError} linkTarget="_blank" diff --git a/src/apps/frontend/src/styles/global.css b/src/apps/frontend/src/styles/global.css index 968dbea..e07125d 100644 --- a/src/apps/frontend/src/styles/global.css +++ b/src/apps/frontend/src/styles/global.css @@ -1,36 +1,37 @@ /* Global Reset & Base Styles — adapted from Polkadot docs */ :root { /* Fonts */ - --font-serif: "DM Serif Display", serif; - --font-sans: "DM Sans", sans-serif; - --font-code: "Courier New", monospace; + --font-serif: "DM Serif Display", Georgia, "Times New Roman", serif; + --font-sans: "DM Sans", "Inter", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif; + --font-code: "Geist Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; /* Greyscale (from Polkadot docs) */ - --grey-50: #fafaf9; + --grey-50: #ffffff; --grey-100: #f5f5f4; --grey-200: #e7e5e4; --grey-300: #d6d3d1; - --grey-400: #a8a29e; - --grey-500: #78716c; - --grey-600: #57534e; - --grey-700: #44403c; - --grey-800: #292524; - --grey-900: #1c1917; - --grey-950: #0f0f0f; + --grey-400: #a1a1a6; + --grey-500: #71717a; + --grey-600: #52525b; + --grey-700: rgba(255, 255, 255, 0.16); + --grey-800: rgba(255, 255, 255, 0.08); + --grey-900: #141414; + --grey-950: #000000; /* Accent */ - --accent: #e6007a; - --accent-hover: #cc0066; + --accent: #ec4899; + --accent-hover: #f472b6; /* Dark theme (matching Polkadot docs custom-dark) */ - --color-bg: var(--grey-950); - --color-surface: var(--grey-900); - --color-surface-secondary: var(--grey-800); - --color-text-primary: var(--grey-50); - --color-text-secondary: var(--grey-400); - --color-text-tertiary: var(--grey-600); - --color-border: var(--grey-800); - --color-border-strong: var(--grey-700); + --color-bg: #000000; + --color-surface: #0f0f0f; + --color-surface-secondary: #141414; + --color-text-primary: #ffffff; + --color-text-secondary: #a1a1a6; + --color-text-tertiary: #71717a; + --color-text-quaternary: #52525b; + --color-border: rgba(255, 255, 255, 0.08); + --color-border-strong: rgba(255, 255, 255, 0.16); /* Semantic */ --success: #059669; @@ -48,16 +49,18 @@ body { font-family: var(--font-sans); color: var(--color-text-secondary); - background: var(--color-bg); + background: #000000; line-height: 1.625; min-width: 320px; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; } h1, h2, h3, h4, h5, h6 { font-family: var(--font-serif); font-weight: 400; color: var(--color-text-primary); - letter-spacing: -0.02em; + letter-spacing: 0; } a { diff --git a/src/lib/env/package.json b/src/lib/env/package.json index 4764a01..db75b4e 100644 --- a/src/lib/env/package.json +++ b/src/lib/env/package.json @@ -8,6 +8,10 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" + }, + "./registry": { + "types": "./dist/registry.d.ts", + "import": "./dist/registry.js" } }, "files": ["dist"], @@ -15,7 +19,7 @@ "access": "public" }, "scripts": { - "build": "tsup src/index.ts --format esm --dts --clean", + "build": "tsup src/index.ts src/registry.ts --format esm --dts --clean", "clean": "rm -rf dist" }, "dependencies": { diff --git a/src/lib/env/src/index.ts b/src/lib/env/src/index.ts index 5047f47..a1a43d5 100644 --- a/src/lib/env/src/index.ts +++ b/src/lib/env/src/index.ts @@ -2,9 +2,10 @@ export type { ChainPreset, ChainFaucet, KnownChainName, - ProductSdkEnvironment, } from "./known_chains"; export { getChainPreset, isKnownChainPreset } from "./known_chains"; +export type { ProductSdkEnvironment } from "./registry"; +export { getRegistryAddress } from "./registry"; export { DEFAULT_NODE_URL, REGISTRY_ADDRESS } from "@dotdm/utils"; export type { diff --git a/src/lib/env/src/known_chains.ts b/src/lib/env/src/known_chains.ts index 5500d54..2a4c9bb 100644 --- a/src/lib/env/src/known_chains.ts +++ b/src/lib/env/src/known_chains.ts @@ -1,12 +1,13 @@ import { BULLETIN_RPCS } from "@parity/product-sdk-host"; import { REGISTRY_ADDRESS } from "@dotdm/utils"; +import { getRegistryAddress, type ProductSdkEnvironment } from "./registry"; export interface ChainFaucet { label: string; url: string; } -export type ProductSdkEnvironment = "paseo" | "previewnet"; +export type { ProductSdkEnvironment }; export interface ChainPreset { assethubUrl: string; @@ -17,8 +18,6 @@ export interface ChainPreset { faucets?: readonly ChainFaucet[]; } -const PASEO_V2_REGISTRY_ADDRESS = "0x5c7b23d386ff622c7f7a4e7a95d5c7a67b10a00d"; -const PREVIEW_NET_REGISTRY_ADDRESS = "0x5c7b23d386ff622c7f7a4e7a95d5c7a67b10a00d"; // Keep these aligned with product-sdk's `getChainAPI("paseo")` preset. Product-sdk // exports Bulletin RPCs, but not the Asset Hub RPC or HTTP gateway constants. const PASEO_ASSET_HUB_URL = "wss://paseo-asset-hub-next-rpc.polkadot.io"; @@ -35,7 +34,7 @@ const KNOWN_CHAINS = { assethubUrl: PASEO_ASSET_HUB_URL, bulletinUrl: BULLETIN_RPCS.paseo[0], ipfsGatewayUrl: PASEO_IPFS_GATEWAY_URL, - registryAddress: PASEO_V2_REGISTRY_ADDRESS, + registryAddress: getRegistryAddress("paseo"), productSdkEnvironment: "paseo", faucets: [ { label: "Asset Hub", url: "https://faucet.polkadot.io/" }, @@ -51,7 +50,7 @@ const KNOWN_CHAINS = { // so CDM stores preview-net metadata on Paseo Bulletin for now. bulletinUrl: BULLETIN_RPCS.paseo[0], ipfsGatewayUrl: PASEO_IPFS_GATEWAY_URL, - registryAddress: PREVIEW_NET_REGISTRY_ADDRESS, + registryAddress: getRegistryAddress("preview-net"), productSdkEnvironment: "previewnet", }, local: { diff --git a/src/lib/env/src/registry.ts b/src/lib/env/src/registry.ts new file mode 100644 index 0000000..8ff81f2 --- /dev/null +++ b/src/lib/env/src/registry.ts @@ -0,0 +1,14 @@ +export type ProductSdkEnvironment = "paseo" | "previewnet"; + +const PASEO_V2_REGISTRY_ADDRESS = "0x5c7b23d386ff622c7f7a4e7a95d5c7a67b10a00d"; +const PREVIEW_NET_REGISTRY_ADDRESS = "0x5c7b23d386ff622c7f7a4e7a95d5c7a67b10a00d"; + +export function getRegistryAddress(name: string): string | undefined { + if (name === "paseo" || name === "paseo-next-v2" || name === "paseo-v2") { + return PASEO_V2_REGISTRY_ADDRESS; + } + if (name === "preview-net" || name === "previewnet") { + return PREVIEW_NET_REGISTRY_ADDRESS; + } + return undefined; +}