diff --git a/assets/dribbble.png b/assets/dribbble.png
new file mode 100644
index 000000000..885a722dc
Binary files /dev/null and b/assets/dribbble.png differ
diff --git a/package-lock.json b/package-lock.json
index fbd3747a8..706ebeb93 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -53,6 +53,267 @@
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/@babel/code-frame": {
+ "version": "7.26.2",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+ "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.26.2",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz",
+ "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
+ "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.26.0",
+ "@babel/generator": "^7.26.0",
+ "@babel/helper-compilation-targets": "^7.25.9",
+ "@babel/helper-module-transforms": "^7.26.0",
+ "@babel/helpers": "^7.26.0",
+ "@babel/parser": "^7.26.0",
+ "@babel/template": "^7.25.9",
+ "@babel/traverse": "^7.25.9",
+ "@babel/types": "^7.26.0",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.26.2",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz",
+ "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.26.2",
+ "@babel/types": "^7.26.0",
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz",
+ "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.25.9",
+ "@babel/helper-validator-option": "^7.25.9",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
+ "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.25.9",
+ "@babel/types": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
+ "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "@babel/traverse": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz",
+ "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+ "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+ "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
+ "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
+ "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.25.9",
+ "@babel/types": "^7.26.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.26.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
+ "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.26.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz",
+ "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz",
+ "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/runtime": {
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz",
@@ -64,6 +325,64 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/template": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
+ "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.25.9",
+ "@babel/parser": "^7.25.9",
+ "@babel/types": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz",
+ "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.25.9",
+ "@babel/generator": "^7.25.9",
+ "@babel/parser": "^7.25.9",
+ "@babel/template": "^7.25.9",
+ "@babel/types": "^7.25.9",
+ "debug": "^4.3.1",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
+ "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -448,14 +767,64 @@
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.11.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz",
- "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==",
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
+ "node_modules/@eslint/config-array": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz",
+ "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.4",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz",
+ "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
"node_modules/@eslint/eslintrc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
@@ -510,6 +879,29 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
+ "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz",
+ "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
"node_modules/@floating-ui/core": {
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
@@ -584,6 +976,44 @@
"node": ">= 0.12"
}
},
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.6",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+ "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -641,6 +1071,20 @@
"deprecated": "Use @eslint/object-schema instead",
"dev": true
},
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
+ "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
"node_modules/@icons/material": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
@@ -2753,20 +3197,22 @@
}
},
"node_modules/@tanstack/query-core": {
- "version": "5.56.2",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.56.2.tgz",
- "integrity": "sha512-gor0RI3/R5rVV3gXfddh1MM+hgl0Z4G7tj6Xxpq6p2I03NGPaJ8dITY9Gz05zYYb/EJq9vPas/T4wn9EaDPd4Q==",
+ "version": "5.61.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.61.5.tgz",
+ "integrity": "sha512-iG5vqurEOEbv+paP6kW3zPENa99kSIrd1THISJMaTwVlJ+N5yjVDNOUwp9McK2DWqWCXM3v13ubBbAyhxT78UQ==",
+ "license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
- "version": "5.56.2",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.56.2.tgz",
- "integrity": "sha512-SR0GzHVo6yzhN72pnRhkEFRAHMsUo5ZPzAxfTMvUxFIDVS6W9LYUp6nXW3fcHVdg0ZJl8opSH85jqahvm6DSVg==",
+ "version": "5.61.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.61.5.tgz",
+ "integrity": "sha512-rjy8aqPgBBEz/rjJnpnuhi8TVkVTorMUsJlM3lMvrRb5wK6yzfk34Er0fnJ7w/4qyF01SnXsLB/QsTBsLF5PaQ==",
+ "license": "MIT",
"dependencies": {
- "@tanstack/query-core": "5.56.2"
+ "@tanstack/query-core": "5.61.5"
},
"funding": {
"type": "github",
@@ -2796,14 +3242,59 @@
"integrity": "sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q==",
"dev": true
},
- "node_modules/@types/cacheable-request": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
- "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@types/http-cache-semantics": "*",
- "@types/keyv": "^3.1.4",
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.6.8",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz",
+ "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.20.6",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz",
+ "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.20.7"
+ }
+ },
+ "node_modules/@types/cacheable-request": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
+ "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
+ "dev": true,
+ "dependencies": {
+ "@types/http-cache-semantics": "*",
+ "@types/keyv": "^3.1.4",
"@types/node": "*",
"@types/responselike": "^1.0.0"
}
@@ -3555,6 +4046,26 @@
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
"dev": true
},
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
+ "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.26.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.25.9",
+ "@babel/plugin-transform-react-jsx-source": "^7.25.9",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.14.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
+ }
+ },
"node_modules/@vitejs/plugin-react-swc": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz",
@@ -3568,10 +4079,11 @@
}
},
"node_modules/acorn": {
- "version": "8.12.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
- "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"dev": true,
+ "license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
@@ -3771,6 +4283,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"browserslist": "^4.23.3",
"caniuse-lite": "^1.0.30001646",
@@ -3996,9 +4509,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.23.3",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
- "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
+ "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==",
"dev": true,
"funding": [
{
@@ -4014,11 +4527,12 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
- "caniuse-lite": "^1.0.30001646",
- "electron-to-chromium": "^1.5.4",
+ "caniuse-lite": "^1.0.30001669",
+ "electron-to-chromium": "^1.5.41",
"node-releases": "^2.0.18",
- "update-browserslist-db": "^1.1.0"
+ "update-browserslist-db": "^1.1.1"
},
"bin": {
"browserslist": "cli.js"
@@ -4126,9 +4640,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001660",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz",
- "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==",
+ "version": "1.0.30001684",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz",
+ "integrity": "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==",
"dev": true,
"funding": [
{
@@ -4143,7 +4657,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
- ]
+ ],
+ "license": "CC-BY-4.0"
},
"node_modules/chalk": {
"version": "4.1.2",
@@ -4226,7 +4741,8 @@
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
- "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+ "license": "MIT"
},
"node_modules/cliui": {
"version": "8.0.1",
@@ -4397,11 +4913,19 @@
"node": ">= 0.6"
}
},
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -4836,6 +5360,10 @@
"resolved": "plugins/doodles",
"link": true
},
+ "node_modules/dribbble": {
+ "resolved": "plugins/dribbble",
+ "link": true
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -4843,10 +5371,11 @@
"dev": true
},
"node_modules/electron-to-chromium": {
- "version": "1.5.24",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.24.tgz",
- "integrity": "sha512-0x0wLCmpdKFCi9ulhvYZebgcPmHTkFVUfU2wzDykadkslKwT4oAmDTHEKLnlrDsMGZe4B+ksn8quZfZjYsBetA==",
- "dev": true
+ "version": "1.5.65",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.65.tgz",
+ "integrity": "sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==",
+ "dev": true,
+ "license": "ISC"
},
"node_modules/emoji-regex": {
"version": "9.2.2",
@@ -5589,6 +6118,16 @@
"node": ">=10"
}
},
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -6077,6 +6616,19 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsesc": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
+ "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -6100,6 +6652,19 @@
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true
},
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -7164,9 +7729,10 @@
"link": true
},
"node_modules/picocolors": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
- "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -7226,9 +7792,9 @@
"link": true
},
"node_modules/postcss": {
- "version": "8.4.47",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
- "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
+ "version": "8.4.49",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+ "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"funding": [
{
"type": "opencollective",
@@ -7243,9 +7809,10 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
- "picocolors": "^1.1.0",
+ "picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
@@ -7587,9 +8154,10 @@
}
},
"node_modules/react-error-boundary": {
- "version": "4.0.13",
- "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz",
- "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==",
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz",
+ "integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==",
+ "license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
@@ -7627,6 +8195,16 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "node_modules/react-refresh": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
+ "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/react-remove-scroll": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz",
@@ -8875,33 +9453,34 @@
}
},
"node_modules/tailwindcss": {
- "version": "3.4.12",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz",
- "integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==",
+ "version": "3.4.15",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.15.tgz",
+ "integrity": "sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
- "chokidar": "^3.5.3",
+ "chokidar": "^3.6.0",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
- "fast-glob": "^3.3.0",
+ "fast-glob": "^3.3.2",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
- "jiti": "^1.21.0",
+ "jiti": "^1.21.6",
"lilconfig": "^2.1.0",
- "micromatch": "^4.0.5",
+ "micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
- "picocolors": "^1.0.0",
- "postcss": "^8.4.23",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
- "postcss-load-config": "^4.0.1",
- "postcss-nested": "^6.0.1",
- "postcss-selector-parser": "^6.0.11",
- "resolve": "^1.22.2",
- "sucrase": "^3.32.0"
+ "postcss-load-config": "^4.0.2",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
},
"bin": {
"tailwind": "lib/cli.js",
@@ -9177,6 +9756,244 @@
"node": ">=14.17"
}
},
+ "node_modules/typescript-eslint": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.16.0.tgz",
+ "integrity": "sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.16.0",
+ "@typescript-eslint/parser": "8.16.0",
+ "@typescript-eslint/utils": "8.16.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz",
+ "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.16.0",
+ "@typescript-eslint/type-utils": "8.16.0",
+ "@typescript-eslint/utils": "8.16.0",
+ "@typescript-eslint/visitor-keys": "8.16.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.3.1",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
+ "eslint": "^8.57.0 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz",
+ "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.16.0",
+ "@typescript-eslint/types": "8.16.0",
+ "@typescript-eslint/typescript-estree": "8.16.0",
+ "@typescript-eslint/visitor-keys": "8.16.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz",
+ "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.16.0",
+ "@typescript-eslint/visitor-keys": "8.16.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz",
+ "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "8.16.0",
+ "@typescript-eslint/utils": "8.16.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz",
+ "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz",
+ "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "8.16.0",
+ "@typescript-eslint/visitor-keys": "8.16.0",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz",
+ "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "8.16.0",
+ "@typescript-eslint/types": "8.16.0",
+ "@typescript-eslint/typescript-estree": "8.16.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz",
+ "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.16.0",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/typescript-eslint/node_modules/eslint-visitor-keys": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
@@ -9192,9 +10009,9 @@
"link": true
},
"node_modules/update-browserslist-db": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
- "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
+ "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
"dev": true,
"funding": [
{
@@ -9210,9 +10027,10 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
- "escalade": "^3.1.2",
- "picocolors": "^1.0.1"
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.0"
},
"bin": {
"update-browserslist-db": "cli.js"
@@ -10270,6 +11088,303 @@
"vite-plugin-framer": "^1.0.1"
}
},
+ "plugins/dribbble": {
+ "version": "0.0.0",
+ "dependencies": {
+ "@tanstack/react-query": "^5.61.5",
+ "classnames": "^2.5.1",
+ "framer-plugin": "^2",
+ "p-limit": "^6.1.0",
+ "react": "^18",
+ "react-dom": "^18",
+ "react-error-boundary": "^4.1.2",
+ "vite-plugin-mkcert": "^1"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "@vitejs/plugin-react": "^4.3.1",
+ "@vitejs/plugin-react-swc": "^3",
+ "autoprefixer": "^10.4.20",
+ "eslint": "^9.9.0",
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
+ "eslint-plugin-react-refresh": "^0.4.9",
+ "globals": "^15.9.0",
+ "postcss": "^8.4.49",
+ "tailwindcss": "^3.4.15",
+ "typescript": "^5.3",
+ "typescript-eslint": "^8.0.1",
+ "vite": "^5",
+ "vite-plugin-framer": "^1"
+ }
+ },
+ "plugins/dribbble/node_modules/@eslint/eslintrc": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
+ "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "plugins/dribbble/node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "plugins/dribbble/node_modules/@eslint/js": {
+ "version": "9.15.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz",
+ "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "plugins/dribbble/node_modules/@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "plugins/dribbble/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "plugins/dribbble/node_modules/eslint": {
+ "version": "9.15.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz",
+ "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.19.0",
+ "@eslint/core": "^0.9.0",
+ "@eslint/eslintrc": "^3.2.0",
+ "@eslint/js": "9.15.0",
+ "@eslint/plugin-kit": "^0.2.3",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.1",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.5",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.2.0",
+ "eslint-visitor-keys": "^4.2.0",
+ "espree": "^10.3.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "plugins/dribbble/node_modules/eslint-plugin-react-hooks": {
+ "version": "5.1.0-rc-fb9a90fa48-20240614",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0-rc-fb9a90fa48-20240614.tgz",
+ "integrity": "sha512-xsiRwaDNF5wWNC4ZHLut+x/YcAxksUd9Rizt7LaEn3bV8VyYRpXnRJQlLOfYaVy9esk4DFP4zPPnoNVjq5Gc0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "plugins/dribbble/node_modules/eslint-scope": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
+ "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "plugins/dribbble/node_modules/eslint-visitor-keys": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "plugins/dribbble/node_modules/espree": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
+ "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "plugins/dribbble/node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "plugins/dribbble/node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "plugins/dribbble/node_modules/globals": {
+ "version": "15.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz",
+ "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "plugins/dribbble/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "plugins/dribbble/node_modules/p-limit": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.1.0.tgz",
+ "integrity": "sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==",
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "plugins/dribbble/node_modules/yocto-queue": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz",
+ "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"plugins/flip-image": {
"version": "0.0.0",
"dependencies": {
diff --git a/plugins/dribbble/.gitignore b/plugins/dribbble/.gitignore
new file mode 100644
index 000000000..255194527
--- /dev/null
+++ b/plugins/dribbble/.gitignore
@@ -0,0 +1,33 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+
+/node_modules
+/.pnp
+.pnp.js
+.yarn
+
+# misc
+
+.DS_Store
+\*.pem
+
+# files
+
+my-plugin
+dev-plugin
+dist
+
+# debug
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log\*
+
+# local env files
+
+.env\*.local
+
+# Packed plugin
+
+plugin.zip
diff --git a/plugins/dribbble/README.md b/plugins/dribbble/README.md
new file mode 100644
index 000000000..02cdb4e37
--- /dev/null
+++ b/plugins/dribbble/README.md
@@ -0,0 +1,7 @@
+# Dribbble Plugin
+
+Sync shots from Dribbble
+
+**By:** @sakib25800
+
+
diff --git a/plugins/dribbble/eslint.config.js b/plugins/dribbble/eslint.config.js
new file mode 100644
index 000000000..6e64b68bd
--- /dev/null
+++ b/plugins/dribbble/eslint.config.js
@@ -0,0 +1,25 @@
+import js from "@eslint/js"
+import globals from "globals"
+import reactHooks from "eslint-plugin-react-hooks"
+import reactRefresh from "eslint-plugin-react-refresh"
+import tseslint from "typescript-eslint"
+
+export default tseslint.config(
+ { ignores: ["dist"] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ["**/*.{ts,tsx}"],
+ languageOptions: {
+ ecmaVersion: 2022,
+ globals: globals.browser,
+ },
+ plugins: {
+ "react-hooks": reactHooks,
+ "react-refresh": reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
+ },
+ }
+)
diff --git a/plugins/dribbble/framer.json b/plugins/dribbble/framer.json
new file mode 100644
index 000000000..f3d299b3d
--- /dev/null
+++ b/plugins/dribbble/framer.json
@@ -0,0 +1,6 @@
+{
+ "id": "357ab0",
+ "name": "Dribbble",
+ "modes": ["canvas", "configureManagedCollection", "syncManagedCollection"],
+ "icon": "/icon.svg"
+}
diff --git a/plugins/dribbble/index.html b/plugins/dribbble/index.html
new file mode 100644
index 000000000..564cb9568
--- /dev/null
+++ b/plugins/dribbble/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Dribbble
+
+
+
+
+
+
+
diff --git a/plugins/dribbble/package.json b/plugins/dribbble/package.json
new file mode 100644
index 000000000..905e51d7f
--- /dev/null
+++ b/plugins/dribbble/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "dribbble",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview",
+ "pack": "npx framer-plugin-tools@latest pack"
+ },
+ "dependencies": {
+ "@tanstack/react-query": "^5.61.5",
+ "classnames": "^2.5.1",
+ "framer-plugin": "^2",
+ "p-limit": "^6.1.0",
+ "react": "^18",
+ "react-dom": "^18",
+ "react-error-boundary": "^4.1.2",
+ "vite-plugin-mkcert": "^1"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "@vitejs/plugin-react": "^4.3.1",
+ "@vitejs/plugin-react-swc": "^3",
+ "autoprefixer": "^10.4.20",
+ "eslint": "^9.9.0",
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
+ "eslint-plugin-react-refresh": "^0.4.9",
+ "globals": "^15.9.0",
+ "postcss": "^8.4.49",
+ "tailwindcss": "^3.4.15",
+ "typescript": "^5.3",
+ "typescript-eslint": "^8.0.1",
+ "vite": "^5",
+ "vite-plugin-framer": "^1"
+ }
+}
diff --git a/plugins/dribbble/postcss.config.js b/plugins/dribbble/postcss.config.js
new file mode 100644
index 000000000..d41ad6355
--- /dev/null
+++ b/plugins/dribbble/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/plugins/dribbble/public/icon.svg b/plugins/dribbble/public/icon.svg
new file mode 100644
index 000000000..98c01e88c
--- /dev/null
+++ b/plugins/dribbble/public/icon.svg
@@ -0,0 +1,15 @@
+
diff --git a/plugins/dribbble/src/App.tsx b/plugins/dribbble/src/App.tsx
new file mode 100644
index 000000000..d51a178b5
--- /dev/null
+++ b/plugins/dribbble/src/App.tsx
@@ -0,0 +1,45 @@
+import { useEffect, useState } from "react"
+import { getPluginContext, PluginContext, useSyncShotsMutation } from "./sync"
+import { framer } from "framer-plugin"
+import auth from "./auth"
+import Authenticate from "./pages/Authenticate"
+import MapFields from "./pages/MapFields"
+
+export function AuthenticatedApp({ context }: { context: PluginContext }) {
+ const syncMutation = useSyncShotsMutation({
+ onSuccess: result => (result.status === "success" ? framer.closePlugin("Synchronization successful") : null),
+ onError: e => framer.notify(e.message, { variant: "error" }),
+ })
+
+ useEffect(() => {
+ framer.showUI({
+ width: 340,
+ height: 410,
+ })
+ }, [])
+
+ return
+}
+
+export function App({ context }: { context: PluginContext }) {
+ const [pluginContext, setPluginContext] = useState(context)
+
+ useEffect(() => {
+ if (!auth.isAuthenticated()) {
+ framer.showUI({
+ width: 260,
+ height: 345,
+ })
+ }
+ }, [])
+
+ const handleAuthenticated = async () => {
+ setPluginContext(await getPluginContext())
+ }
+
+ if (!auth.isAuthenticated()) {
+ return
+ }
+
+ return
+}
diff --git a/plugins/dribbble/src/api.ts b/plugins/dribbble/src/api.ts
new file mode 100644
index 000000000..41672b66e
--- /dev/null
+++ b/plugins/dribbble/src/api.ts
@@ -0,0 +1,209 @@
+import auth from "./auth"
+import pLimit from "p-limit"
+
+type RequestQueryParams = Record
+
+interface RequestOptions {
+ path: string
+ method?: string
+ query?: RequestQueryParams
+ // eslint-disable-next-line
+ body?: any
+}
+
+interface PaginationParams extends Record {
+ page: number
+ per_page: number
+}
+
+export interface Shot {
+ id: number
+ title: string
+ description: string
+ width: number
+ height: number
+ images: {
+ hidpi: string | null
+ normal: string
+ teaser: string
+ }
+ low_profile: boolean
+ published_at: string
+ updated_at: string
+ html_url: string
+ animated: boolean
+ tags: string[]
+ attachments: Array<{
+ id: number
+ url: string
+ thumbnail_url: string
+ size: number
+ content_type: string
+ created_at: string
+ }>
+ projects: Array<{
+ id: number
+ name: string
+ description: string
+ shots_count: number
+ created_at: string
+ updated_at: string
+ }>
+ team?: {
+ id: number
+ name: string
+ login: string
+ html_url: string
+ avatar_url: string
+ bio: string
+ location: string
+ links: {
+ web: string
+ twitter: string
+ }
+ type: string
+ created_at: string
+ updated_at: string
+ }
+ video?: {
+ id: number
+ duration: number
+ width: number
+ height: number
+ url: string
+ small_preview_url: string
+ large_preview_url: string
+ xlarge_preview_url: string
+ }
+}
+
+const DRIBBLE_BASE_URL = "https://api.dribbble.com/v2"
+const MAX_PAGE_SIZE = 100 // v2 pagination limit
+const CONCURRENCY_LIMIT = 10 // v2 limit: 60 requests per minute
+
+const request = async ({
+ path,
+ method,
+ query,
+ body,
+}: RequestOptions): Promise<{ data: T; headers: Headers }> => {
+ const tokens = auth.tokens.getOrThrow()
+ const url = new URL(`${DRIBBLE_BASE_URL}${path}`)
+
+ if (query) {
+ for (const [key, value] of Object.entries(query)) {
+ if (value !== undefined) {
+ if (Array.isArray(value)) {
+ value.forEach(val => url.searchParams.append(key, decodeURIComponent(val)))
+ } else {
+ url.searchParams.append(key, String(value))
+ }
+ }
+ }
+ }
+
+ const res = await fetch(url, {
+ method: method?.toUpperCase() ?? "GET",
+ body: body ? JSON.stringify(body) : undefined,
+ headers: {
+ Authorization: `Bearer ${tokens.accessToken}`,
+ },
+ })
+
+ if (method === "delete" && res.status === 204) {
+ return { data: {} as T, headers: res.headers }
+ }
+
+ if (!res.ok) {
+ throw new Error(`Failed to fetch Dribbble API: ${res.status}`)
+ }
+
+ const data = await res.json()
+ return { data: data as T, headers: res.headers }
+}
+
+const parseLinkHeader = (headers: Headers): { next?: string } => {
+ const linkHeader = headers.get("Link")
+ if (!linkHeader) return {}
+
+ const links = linkHeader.split(",").reduce(
+ (acc, part) => {
+ const match = part.match(/<(.+)>;\s*rel="(\w+)"/)
+ if (match) {
+ const [, url, rel] = match
+ return { ...acc, [rel]: url }
+ }
+ return acc
+ },
+ {} as Record
+ )
+
+ return { next: links["next"] }
+}
+
+const paginatedRequest = async (options: RequestOptions): Promise<{ data: T; nextUrl?: string }> => {
+ const { data, headers } = await request(options)
+ const { next } = parseLinkHeader(headers)
+
+ return {
+ data,
+ nextUrl: next,
+ }
+}
+
+export const fetchShots = async (params: PaginationParams): Promise<{ shots: Shot[]; nextUrl?: string }> => {
+ const response = await paginatedRequest({
+ path: "/user/shots",
+ query: params,
+ })
+
+ return {
+ shots: response.data,
+ nextUrl: response.nextUrl,
+ }
+}
+
+export const fetchAllShots = async (max: number): Promise => {
+ const limit = pLimit(CONCURRENCY_LIMIT)
+ let allShots: Shot[] = []
+
+ // Get initial page
+ const { shots: firstPage, nextUrl } = await fetchShots({
+ page: 1,
+ per_page: MAX_PAGE_SIZE,
+ })
+
+ allShots = [...firstPage]
+
+ const urlsToFetch: string[] = []
+ let currentNextUrl = nextUrl
+
+ // Collect all URLs to fetch
+ while (currentNextUrl && allShots.length + urlsToFetch.length * MAX_PAGE_SIZE < max) {
+ urlsToFetch.push(currentNextUrl)
+ const { nextUrl } = await fetchShots({
+ page: Number(new URL(currentNextUrl).searchParams.get("page")),
+ per_page: MAX_PAGE_SIZE,
+ })
+ currentNextUrl = nextUrl
+ }
+
+ const results = await Promise.all(
+ urlsToFetch.map(url =>
+ limit(() =>
+ fetchShots({
+ page: Number(new URL(url).searchParams.get("page")),
+ per_page: MAX_PAGE_SIZE,
+ })
+ )
+ )
+ )
+
+ allShots = allShots.concat(...results.map(r => r.shots))
+
+ if (allShots.length > max) {
+ allShots = allShots.slice(0, max)
+ }
+
+ return allShots
+}
diff --git a/plugins/dribbble/src/auth.ts b/plugins/dribbble/src/auth.ts
new file mode 100644
index 000000000..7feb58774
--- /dev/null
+++ b/plugins/dribbble/src/auth.ts
@@ -0,0 +1,103 @@
+export interface Tokens {
+ access_token: string
+ token_type: "bearer"
+ scope: string
+}
+
+export interface StoredTokens {
+ createdAt: number
+ accessToken: string
+ scope: string
+}
+
+export interface Authorize {
+ url: string
+ writeKey: string
+ readKey: string
+}
+
+const PLUGIN_TOKENS_KEY = "dribbbleTokens"
+const isLocal = () => window.location.hostname.includes("localhost")
+export const AUTH_URI = isLocal() ? "https://localhost:8787" : "https://oauth.fetch.tools/dribbble-plugin"
+
+class Auth {
+ storedTokens?: StoredTokens | null
+
+ async authorize() {
+ const res = await fetch(`${AUTH_URI}/authorize`, {
+ method: "POST",
+ })
+
+ if (res.status !== 200) {
+ throw new Error("Failed to generate OAuth URL.")
+ }
+
+ return (await res.json()) as Authorize
+ }
+
+ async fetchTokens(readKey: string) {
+ const res = await fetch(`${AUTH_URI}/poll?readKey=${readKey}`, {
+ method: "POST",
+ })
+
+ if (res.status !== 200) {
+ throw new Error("Something went wrong polling for tokens.")
+ }
+
+ const tokens = (await res.json()) as Tokens
+
+ this.tokens.save(tokens)
+ return tokens
+ }
+
+ isAuthenticated() {
+ const tokens = this.tokens.get()
+ if (!tokens) return false
+
+ return true
+ }
+
+ logout() {
+ this.tokens.clear()
+ }
+
+ public readonly tokens = {
+ save: (tokens: Tokens) => {
+ const storedTokens: StoredTokens = {
+ createdAt: Date.now(),
+ accessToken: tokens.access_token,
+ scope: tokens.scope,
+ }
+
+ this.storedTokens = storedTokens
+ window.localStorage.setItem(PLUGIN_TOKENS_KEY, JSON.stringify(storedTokens))
+
+ return storedTokens
+ },
+ get: (): StoredTokens | null => {
+ if (this.storedTokens) return this.storedTokens
+
+ const serializedTokens = window.localStorage.getItem(PLUGIN_TOKENS_KEY)
+ if (!serializedTokens) return null
+
+ const storedTokens = JSON.parse(serializedTokens) as StoredTokens
+
+ this.storedTokens = storedTokens
+ return storedTokens
+ },
+ getOrThrow: (): StoredTokens => {
+ const tokens = this.tokens.get()
+ if (!tokens) {
+ throw new Error("Dribbble API token missing")
+ }
+
+ return tokens
+ },
+ clear: () => {
+ this.storedTokens = null
+ window.localStorage.removeItem(PLUGIN_TOKENS_KEY)
+ },
+ }
+}
+
+export default new Auth()
diff --git a/plugins/dribbble/src/cms.ts b/plugins/dribbble/src/cms.ts
new file mode 100644
index 000000000..3bc2d7cc8
--- /dev/null
+++ b/plugins/dribbble/src/cms.ts
@@ -0,0 +1,139 @@
+import { framer, ManagedCollectionField } from "framer-plugin"
+import { useEffect } from "react"
+
+export const MAX_CMS_ITEMS = 10_000
+export const PLUGIN_LOG_SYNC_KEY = "dribbbleLogSyncResult"
+export const FIELD_DELIMITER = "rfa4Emr21pUgs0in"
+export const SLUG_COMPATIBLE_FIELDS = ["string", "formattedText"]
+
+export interface ItemResult {
+ fieldName?: string
+ message: string
+}
+
+export interface SyncStatus {
+ errors: ItemResult[]
+ warnings: ItemResult[]
+ info: ItemResult[]
+}
+
+export interface SyncResult extends SyncStatus {
+ status: "success" | "completed_with_errors"
+}
+
+export type FieldsById = Map
+
+const isLoggingEnabled = () => {
+ return localStorage.getItem(PLUGIN_LOG_SYNC_KEY) === "true"
+}
+
+export function logSyncResult(result: SyncResult, collectionItems?: Record[]) {
+ if (!isLoggingEnabled()) return
+
+ if (collectionItems) {
+ console.table(collectionItems)
+ }
+
+ if (result.errors.length > 0) {
+ console.log("Completed errors:")
+ console.table(result.errors)
+ }
+
+ if (result.warnings.length > 0) {
+ console.log("Completed warnings:")
+ console.table(result.warnings)
+ }
+
+ console.log("Completed info:")
+ console.table(result.info)
+}
+
+export const useLoggingToggle = () => {
+ useEffect(() => {
+ const isLoggingEnabled = () => localStorage.getItem(PLUGIN_LOG_SYNC_KEY) === "true"
+
+ const toggle = () => {
+ const newState = !isLoggingEnabled()
+ localStorage.setItem(PLUGIN_LOG_SYNC_KEY, newState ? "true" : "false")
+ framer.notify(`Logging ${newState ? "enabled" : "disabled"}`, { variant: "info" })
+ }
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.ctrlKey && e.shiftKey && e.key === "L") {
+ e.preventDefault()
+ toggle()
+ }
+ }
+
+ document.addEventListener("keydown", handleKeyDown)
+
+ return () => {
+ document.removeEventListener("keydown", handleKeyDown)
+ }
+ }, [])
+}
+
+// Match everything except for letters, numbers and parentheses.
+const nonSlugCharactersRegExp = /[^\p{Letter}\p{Number}()]+/gu
+// Match leading/trailing dashes, for trimming purposes.
+const trimSlugRegExp = /^-+|-+$/gu
+
+export function getPossibleSlugFields(fields: ManagedCollectionField[]) {
+ return fields.filter(field => SLUG_COMPATIBLE_FIELDS.includes(field.type))
+}
+
+/**
+ * Takes a freeform string and removes all characters except letters, numbers,
+ * and parentheses. Also makes it lower case, and separates words by dashes.
+ * This makes the value URL safe.
+ */
+export function slugify(value: string): string {
+ return value.toLowerCase().replace(nonSlugCharactersRegExp, "-").replace(trimSlugRegExp, "")
+}
+
+/**
+ * Generates an 8-character unique ID from a text using the djb2 hash function.
+ * Converts the 32-bit hash to an unsigned integer and then to a hex string.
+ */
+export function generateHash(text: string): string {
+ let hash = 5381
+ for (let i = 0, len = text.length; i < len; i++) {
+ hash = (hash * 33) ^ text.charCodeAt(i)
+ }
+ // Convert to unsigned 32-bit integer
+ const unsignedHash = hash >>> 0
+ return unsignedHash.toString(16).padStart(8, "0")
+}
+
+/**
+ * Creates a consistent hash from an array of field IDs
+ */
+export function createFieldSetHash(fieldIds: string[]): string {
+ // Ensure consistent ordering
+ const sortedIds = [...fieldIds].sort()
+ return generateHash(sortedIds.join(FIELD_DELIMITER))
+}
+
+/**
+ * Processes a field set to determine the complementary fields
+ */
+export function computeFieldSets(params: {
+ currentFields: ManagedCollectionField[]
+ allPossibleFieldIds: string[]
+ storedHash: string
+}) {
+ const { currentFields, allPossibleFieldIds, storedHash } = params
+ const currentFieldIds = currentFields.map(field => field.id)
+
+ const includedFieldIds = currentFieldIds
+
+ const excludedFieldIds = allPossibleFieldIds.filter(id => !currentFieldIds.includes(id))
+
+ const currentHash = createFieldSetHash(includedFieldIds)
+
+ return {
+ includedFieldIds,
+ excludedFieldIds,
+ hasHashChanged: storedHash !== currentHash,
+ }
+}
diff --git a/plugins/dribbble/src/components/Button.tsx b/plugins/dribbble/src/components/Button.tsx
new file mode 100644
index 000000000..313ef979b
--- /dev/null
+++ b/plugins/dribbble/src/components/Button.tsx
@@ -0,0 +1,13 @@
+import cx from "classnames"
+import { Spinner } from "./Spinner"
+
+interface Props extends React.ButtonHTMLAttributes {
+ variant?: "primary" | "secondary" | "destructive"
+ isLoading?: boolean
+}
+
+export const Button = ({ variant = "primary", children, className, isLoading = false, disabled, ...rest }: Props) => (
+
+)
diff --git a/plugins/dribbble/src/components/CenteredSpinner.tsx b/plugins/dribbble/src/components/CenteredSpinner.tsx
new file mode 100644
index 000000000..1f2704655
--- /dev/null
+++ b/plugins/dribbble/src/components/CenteredSpinner.tsx
@@ -0,0 +1,8 @@
+import { Spinner, SpinnerProps } from "./Spinner"
+import cx from "classnames"
+
+export const CenteredSpinner = ({ className, size }: { className?: string; size?: SpinnerProps["size"] }) => (
+
+
+
+)
diff --git a/plugins/dribbble/src/components/CheckboxTextField.tsx b/plugins/dribbble/src/components/CheckboxTextField.tsx
new file mode 100644
index 000000000..70c2f2ea7
--- /dev/null
+++ b/plugins/dribbble/src/components/CheckboxTextField.tsx
@@ -0,0 +1,45 @@
+import { useState } from "react"
+import cx from "classnames"
+
+interface Props {
+ value: string
+ disabled: boolean
+ checked: boolean
+ onChange: () => void
+}
+
+export function CheckboxTextfield({ value, disabled, checked: initialChecked, onChange }: Props) {
+ const [checked, setChecked] = useState(initialChecked)
+
+ const toggle = () => {
+ if (disabled) return
+
+ setChecked(!checked)
+ onChange()
+ }
+
+ return (
+
+
+
+
+ )
+}
diff --git a/plugins/dribbble/src/components/ErrorBoundaryFallback.tsx b/plugins/dribbble/src/components/ErrorBoundaryFallback.tsx
new file mode 100644
index 000000000..fe05b1053
--- /dev/null
+++ b/plugins/dribbble/src/components/ErrorBoundaryFallback.tsx
@@ -0,0 +1,12 @@
+import { QueryErrorResetBoundary } from "@tanstack/react-query"
+
+export const ErrorBoundaryFallback = () => (
+
+ {({ reset }) => (
+
+ Something went wrong...
+
+
+ )}
+
+)
diff --git a/plugins/dribbble/src/components/FieldMapper.tsx b/plugins/dribbble/src/components/FieldMapper.tsx
new file mode 100644
index 000000000..c72db8e27
--- /dev/null
+++ b/plugins/dribbble/src/components/FieldMapper.tsx
@@ -0,0 +1,111 @@
+import { CheckboxTextfield } from "../components/CheckboxTextField"
+import { IconChevron } from "../components/Icons"
+import { ScrollFadeContainer } from "../components/ScrollFadeContainer"
+import { assert } from "../utils"
+import { ManagedCollectionField } from "framer-plugin"
+import { Fragment, useMemo } from "react"
+import cx from "classnames"
+
+export interface ManagedCollectionFieldConfig {
+ field: ManagedCollectionField | null
+ originalFieldName: string
+}
+
+interface FieldMapperProps {
+ collectionFieldConfig: ManagedCollectionFieldConfig[]
+ fieldNameOverrides: Record
+ isFieldSelected: (fieldId: string) => boolean
+ onFieldToggle: (fieldId: string) => void
+ onFieldNameChange: (fieldId: string, value: string) => void
+ fromLabel?: string
+ toLabel?: string
+ className?: string
+ height?: number
+}
+const getInitialSortedFields = (
+ fields: ManagedCollectionFieldConfig[],
+ isFieldSelected: (fieldId: string) => boolean
+): ManagedCollectionFieldConfig[] => {
+ return [...fields].sort((a, b) => {
+ const aIsSelected = a.field && isFieldSelected(a.field.id)
+ const bIsSelected = b.field && isFieldSelected(b.field.id)
+
+ // Sort based on whether the fields are selected
+ if (aIsSelected && !bIsSelected) return -1
+ if (!aIsSelected && bIsSelected) return 1
+
+ // Then sort by whether they are supported fields
+ if (a.field && !b.field) return -1
+ if (!a.field && b.field) return 1
+
+ return 0
+ })
+}
+
+export const FieldMapper = ({
+ collectionFieldConfig,
+ fieldNameOverrides,
+ isFieldSelected,
+ onFieldToggle,
+ onFieldNameChange,
+ fromLabel = "Column",
+ toLabel = "Field",
+ height,
+ className,
+}: FieldMapperProps) => {
+ // We only want to sort on initial render
+ const sortedCollectionFieldConfig = useMemo(
+ () => getInitialSortedFields(collectionFieldConfig, isFieldSelected),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [collectionFieldConfig]
+ )
+
+ return (
+
+
+
{fromLabel}
+
{toLabel}
+ {sortedCollectionFieldConfig.map((fieldConfig, i) => {
+ const isUnsupported = !fieldConfig.field
+ const isSelected = fieldConfig.field ? isFieldSelected(fieldConfig.field.id) : false
+
+ return (
+
+ {
+ assert(fieldConfig.field)
+ onFieldToggle(fieldConfig.field.id)
+ }}
+ />
+
+
+
+ {
+ assert(fieldConfig.field)
+ onFieldNameChange(fieldConfig.field.id, e.target.value)
+ }}
+ />
+
+ )
+ })}
+
+
+ )
+}
diff --git a/plugins/dribbble/src/components/Icons.tsx b/plugins/dribbble/src/components/Icons.tsx
new file mode 100644
index 000000000..3bb1e087e
--- /dev/null
+++ b/plugins/dribbble/src/components/Icons.tsx
@@ -0,0 +1,12 @@
+import cx from "classnames"
+
+export const IconChevron = ({ className }: { className?: string }) => (
+
+)
diff --git a/plugins/dribbble/src/components/ScrollFadeContainer.tsx b/plugins/dribbble/src/components/ScrollFadeContainer.tsx
new file mode 100644
index 000000000..8ce83ae2a
--- /dev/null
+++ b/plugins/dribbble/src/components/ScrollFadeContainer.tsx
@@ -0,0 +1,81 @@
+import React, { useEffect, useRef, useState, useCallback } from "react"
+
+interface Props {
+ children: React.ReactNode
+ className?: string
+ fadeHeight?: number
+ height: number
+}
+
+export const ScrollFadeContainer = ({ children, className, fadeHeight = 45, height }: Props) => {
+ const [showTopFade, setShowTopFade] = useState(false)
+ const [showBottomFade, setShowBottomFade] = useState(false)
+ const containerRef = useRef(null)
+ const scrollTimeout = useRef()
+
+ const checkScroll = useCallback(() => {
+ const element = containerRef.current
+ if (!element) return
+
+ if (scrollTimeout.current) {
+ clearTimeout(scrollTimeout.current)
+ }
+
+ scrollTimeout.current = setTimeout(() => {
+ requestAnimationFrame(() => {
+ if (!element) return
+ const { scrollTop, scrollHeight, clientHeight } = element
+ const scrollBottom = scrollHeight - scrollTop - clientHeight
+
+ setShowTopFade(scrollTop > 10)
+ setShowBottomFade(scrollBottom > 10)
+ })
+ }, 50)
+ }, [])
+
+ useEffect(() => {
+ const element = containerRef.current
+ if (!element) return
+
+ requestAnimationFrame(() => {
+ if (!element) return
+ const { scrollHeight, clientHeight } = element
+ setShowBottomFade(scrollHeight > clientHeight)
+ checkScroll()
+ })
+
+ element.addEventListener("scroll", checkScroll, { passive: true })
+
+ return () => {
+ if (scrollTimeout.current) {
+ clearTimeout(scrollTimeout.current)
+ }
+
+ element.removeEventListener("scroll", checkScroll)
+ }
+ }, [checkScroll])
+
+ return (
+
+ )
+}
diff --git a/plugins/dribbble/src/components/Spinner.tsx b/plugins/dribbble/src/components/Spinner.tsx
new file mode 100644
index 000000000..594ad9d98
--- /dev/null
+++ b/plugins/dribbble/src/components/Spinner.tsx
@@ -0,0 +1,40 @@
+import cx from "classnames"
+import styles from "./spinner.module.css"
+
+export interface SpinnerProps {
+ /** Size of the spinner */
+ size?: "normal" | "medium" | "large"
+ /** Set the spinner to have a static position inline with other content */
+ inline?: boolean
+ className?: string
+ inheritColor?: boolean
+}
+
+function styleForSize(size: SpinnerProps["size"]) {
+ switch (size) {
+ case "normal":
+ return styles.normalStyle
+ case "medium":
+ return styles.mediumStyle
+ case "large":
+ return styles.largeStyle
+ }
+}
+
+function spinnerClassNames(size: SpinnerProps["size"] = "normal") {
+ return cx(styles.spin, styles.baseStyle, styleForSize(size))
+}
+
+export const Spinner = ({ size, inline = false, inheritColor, className, ...rest }: SpinnerProps) => {
+ return (
+
+ )
+}
diff --git a/plugins/dribbble/src/components/spinner.module.css b/plugins/dribbble/src/components/spinner.module.css
new file mode 100644
index 000000000..a0d1a7ac3
--- /dev/null
+++ b/plugins/dribbble/src/components/spinner.module.css
@@ -0,0 +1,60 @@
+.baseStyle {
+ --spinner-translate: 0;
+ background-color: #fff;
+}
+
+.buttonWithDepthSpinner {
+ background-color: currentColor;
+}
+
+.normalStyle {
+ width: 12px;
+ height: 12px;
+ -webkit-mask: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAApNJREFUSA2tlUtLlFEYgEe7mtFFQ8NLFBG1SJAwahtiLVy5ceVSEPQH1LZf0Lp9FO1bdYNyI7gyBFcS2kAzilYq3sfxeYZ55Zv6FGfohWfOd97vnPd23u9MXeZoaeb1Q7gPHXAFzsAS5GACxiEPqVKXqs1kmtAPQi+cAtedToz1PJ8ExxPwDl7CAlRImoN7rBiF86ABHTiGAzMIh8n3O+ifwWc4EL0n5TGTYQijvjNSxbUadozInUeGZ3nuh1WYgpIkM+hGMwYajAgdrfUn+AaLoLTCAxiAOxCZxL4RdB+gZMjxEjwHo4hS8Jh5DR9hz0mKaNizegoNEI7Wee6FXJTISG6AUgDr+QImoAiHie+mwZJow/PRps4uwnsnRj8Epqc4voVJJ8eULOtW4BFYYjPpgjc+3IWI2sh/wFeoVl6xYQ48dDGLfjOwc/ygQqz595hUMVouS9QHBm4l6k3HWpnBLqicgVrlCxujSbR1Swd+UDpQYRS/oVaZZ2MWwlZBB9Zdxf+QTYw8SRgq6sAvz7vH6JULEB9USVHFjzZskgPRQR4ayxoXXIdaHdg9HrB2rErByWxZERnYtrWUzD0esDbtTmXPhzXwHrKLxEUbYGbVyDkWWwkdadfqbPmwDXZSCyh2VAf8BM/nOOJH5Y0QopMt2I5U/KO4DZZJB47XwEX+ex0mGroMV8sLwp56gyuGwiyW4SZ4c8bt2cZzO7jBNeqtsRHbeZ1g9krsMbhfYPv/c5gecA+4yAVuitFszE5Hjva872OMdXagF19JIoOYWyozMTI3J0tmA6gLvXPfO1cc/XOqOLe/HbjwD8yCXRD3VNJROEgatiRZMJsKsbZHia3nAdphtq8fkiWyXJbBe8uM1aXKPouNnz2Bm1kwAAAAAElFTkSuQmCC");
+ mask: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAApNJREFUSA2tlUtLlFEYgEe7mtFFQ8NLFBG1SJAwahtiLVy5ceVSEPQH1LZf0Lp9FO1bdYNyI7gyBFcS2kAzilYq3sfxeYZ55Zv6FGfohWfOd97vnPd23u9MXeZoaeb1Q7gPHXAFzsAS5GACxiEPqVKXqs1kmtAPQi+cAtedToz1PJ8ExxPwDl7CAlRImoN7rBiF86ABHTiGAzMIh8n3O+ifwWc4EL0n5TGTYQijvjNSxbUadozInUeGZ3nuh1WYgpIkM+hGMwYajAgdrfUn+AaLoLTCAxiAOxCZxL4RdB+gZMjxEjwHo4hS8Jh5DR9hz0mKaNizegoNEI7Wee6FXJTISG6AUgDr+QImoAiHie+mwZJow/PRps4uwnsnRj8Epqc4voVJJ8eULOtW4BFYYjPpgjc+3IWI2sh/wFeoVl6xYQ48dDGLfjOwc/ygQqz595hUMVouS9QHBm4l6k3HWpnBLqicgVrlCxujSbR1Swd+UDpQYRS/oVaZZ2MWwlZBB9Zdxf+QTYw8SRgq6sAvz7vH6JULEB9USVHFjzZskgPRQR4ayxoXXIdaHdg9HrB2rErByWxZERnYtrWUzD0esDbtTmXPhzXwHrKLxEUbYGbVyDkWWwkdadfqbPmwDXZSCyh2VAf8BM/nOOJH5Y0QopMt2I5U/KO4DZZJB47XwEX+ex0mGroMV8sLwp56gyuGwiyW4SZ4c8bt2cZzO7jBNeqtsRHbeZ1g9krsMbhfYPv/c5gecA+4yAVuitFszE5Hjva872OMdXagF19JIoOYWyozMTI3J0tmA6gLvXPfO1cc/XOqOLe/HbjwD8yCXRD3VNJROEgatiRZMJsKsbZHia3nAdphtq8fkiWyXJbBe8uM1aXKPouNnz2Bm1kwAAAAAElFTkSuQmCC");
+ -webkit-mask-size: 12px;
+ mask-size: 12px;
+}
+
+.mediumStyle {
+ width: 24px;
+ height: 24px;
+ -webkit-mask: url("data:image/png;base64,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");
+ mask: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAAEgBckRAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAMKADAAQAAAABAAAAMAAAAAD4/042AAAIYUlEQVRoBc2ZWYiVZRjHZ8x1NLdcyn3FiIoKJSsqU6LowqiMSioRpJUIjEIwtAW6aBEv1AK7yMSLLoQ2tEVbKMlAwsqF3Mol3EYTx93R6fd7x+f4nTNnHOfMqPPAf573e95nfbfv/c6UlWWopqZmR+axrAzBnhDQbguGtVJQXl7eKzpon6B9Wu2lIazD6RyRFfI8IblC+BIPk+2EPwfraDsRgqWgbTyH8GAI6BwYwrUhDE7nSGP8G4IM72zHgYwgmp1TA9M5IaE9xXZ5COQID8HMrDWwT8t2PjMiu+CpQ8WNtCsQdlJYQFU+o3ONvBWNd+FVKPdVUB/Rv46+bhaxAeRPW31WtSmmcIuI9FF9evR1BRPB0MKi38CoPTgOjgGnXnx7JiWaBYSX8aASdAR5DvNU6fwRHAPVQHoHdAG9gcN8lhBsAfvB8rPS2hayfmAQ6JAkNsBWMKZQOZ7pGwGu9bmcxmp4R4oaHgrFOHo3Ia8yN0dkRjGlApmjV6GBi3hZQWexRw3KnGnH+ykfGiCLbquBEzO6AWW7XbXrWlHsQzROUFRu7dubJfoe57kNutuMIL0PWtMxG1yWJPyh3R88TbMtyp8oz009HRq71E3RuoQjeALlufBEOYMQBMdBBe1twF2nMxH6O2k7b46ycp0ehtehMMh14Hg7D+5UjYUlOVVTwUqwCUgOmvAIMEibM3wfwTwaEuUC4PhzJDcADVSegOL38PMi7AegaECDSRuwr91OdP6KoAvQ+e90PAAvifB1PYYmLtZ4Jt1Bw6FwsvY2xTn20p/AVW01I3X8MnD2T4NHQJPIYYHSttGRASqBY14Dak94Gk2kCFBjOEtxkRvgNBm4k0om/PXE+FbgiJxyazo8C4H8OAofw0sibEdgOBa4WNrhe0l2mXZHOAucAtXAap6FN0g4vg6lUUBbMz+C7WJ4bmfaToTyYBqvgJNAZbmGWW4CAXVSQvBDOF4Ez1Gugpwk0yCYx8XD4GoQAbOOPX+W4dTXamlEEF8ys8BG4BzNBF6aXIGlEw6mAR2eBKfAaRDUnoYvVt+T3UG9I1EnC5T7kNZfIM6U+rL0ENSxer2wc1HsLVTOc4KS4/0HcOLcFwEnzuV3OdDGeZBsG0hY1UB4HuVKo/NGepYAd7UG8rvJ6hd4vYTdFXR2ABEIk/LNYZACoGSnQofMTeI6HgQ/L8LeAFcB/VjVcez/gacH+Q/Ate6QnGyMc/S9fR+F7QYGMMlOBDVQukJ6wFmmAU6h3BfeaMLuMEYe+QYwULobGWUmiJ1Y/5UfpYaIIFvQ0WcMdWrcjsDsDfICaCpFAK8l3X2IM8bhieXXlCDbMbYCV2EfG655KzBYc5A3Cv1KXWzEpckV1BzkRDvJUisDmL3UXAFclQ6PdMQAjrvOmyvAAHxZgf4qHfcjwCo8MdPapd0UGoixFZj8BgN8AKzC1fQqKJlIsAfGOtav7/v00v+OBwOk1YTSPbRLpXEYGiBlr5NYmvNpG0Q8RhDfCY0ibCZi4NBE9ut1kAJQynLa+4ATY5AZGIyFnxehOwVFT2En1+y/AIly7wOfUPwQ5oRHIIPNIYFN8DxC1+TuA/2A+tqp/xP6m+GJ8gIowfBtmL9QqBxG8jhSgtsfOsoM8inO98BzVCeAPQS5DebrMxsgnBVzXIXuApy7UPKoaIDQINAo2uOBY2uWaaVl2l4OvsGxx03zEsG7gSfBYlAF3KhxvclecRAn8ucALwbesypAZ+DVpx1Ii62UDM85QlmHBHGUJoHpwPe7QbUvBsR1rr07kQ0B+pFczVnbkPvmPcqou2YapAYLIPGReFkI3NsR0OQNGPaFxdCV+kzCu94C8CZwmbjXtdNGshApeMSQV1LI8dRbzx+VihKJT6BjLohgJmzQSDYK8ON6HviZYG6QBgnf+jRhP+T8PvA5EH6jQOUW4karQ3bmEc6vRPAlcJnYH1OdTX46Di2u2Yi4Xqq7AguQoqAYMGdzK3GdxRzlFYCTR+l5C4SRzoTPvlXvxYEnwwUjcvDEd7nKJWMHme9uctifFaQ2hs/QeBFE0pG4/DWM5ifFi/SHfPyg7g2igOyM7CIf7/G1Gwfl+2k/DzyHVYwTQP4gyqvgF5UcZfJyAw8D2ZViuw991ejs80cQN9Iy4OsxO+q2J6F0zm8zdC4okZ97YziIGTBeDPJvblCXjkebr13JURefXerkTYYcfEkepOllMUsWMcACbgbubJN2CcUsLKbdUsh3iftBMvGgHhbQE5h8zEAorA2tFsAPk4OrpJA6WoDT0ynTYzGSp4BHZ0ugdiRhrpFb5FTtEbUOOPqFGBFaLYD7grMAZyHg8yEL8K1r8l4D3AuByWwe98MlJXIwRz9HTTj2pzKxw0+/lTRWAAvIzoLH61RwqWkUCfgLV8yAXHg/2p02LFW6B/w+NmlPojiRaKb/XMy0cbGJvMYR072Yzce2q8QPnRNx4vgZ5kaZDVxvkoVIMSvvYbCmVnRh/5KLF8k7C6LEwB5F/hW5pJtvrgCVMXRdvQ4GAw3CiGYqRON5GK9X0NxE/L74NHEvcg5cduTN5T/gv49tJ8orIIQ4Gkp7GnCt6UiSh6GONwK/sv+Gl0zE8vo+Gvii0n8kbgwpZF8Tq7JWdPZv0QKiG+e30H4CODPhMApyHUoGsM8PDmfGwg6cefYdI3mfcZ/J+wNn2FHW1qWgffhPSyPTt4LEN/NclM5ZQFhQiPvCO5PBLSCSNqjPwWnmEgqdSDD7rJ52UtY+ivC3E0fcN/A56bwKyHqgGEduDLgLFK7VKEReLOGsPBJHNf23bhV8NUnHzCpvkBpdQDGPFOXbcTgYAjz2ugGP5CjQe72j6SZ0HW8VjU0Wmzr0PxUW1kdl6s26AAAAAElFTkSuQmCC");
+ -webkit-mask-size: 24px;
+ mask-size: 24px;
+}
+
+.largeStyle {
+ width: 30px;
+ height: 30px;
+ -webkit-mask: url("data:image/png;base64,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");
+ mask: url("data:image/png;base64,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");
+ -webkit-mask-size: 30px;
+ mask-size: 30px;
+}
+
+.centeredStyle {
+ --spinner-translate: -50%;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(var(--spinner-translate), var(--spinner-translate));
+}
+
+.spin {
+ animation-duration: 800ms;
+ animation-iteration-count: infinite;
+ animation-name: spin;
+ animation-timing-function: linear;
+}
+
+@keyframes spin {
+ 0% {
+ transform: translate(var(--spinner-translate), var(--spinner-translate)) rotate(0deg);
+ }
+
+ 100% {
+ transform: translate(var(--spinner-translate), var(--spinner-translate)) rotate(360deg);
+ }
+}
diff --git a/plugins/dribbble/src/constants.ts b/plugins/dribbble/src/constants.ts
new file mode 100644
index 000000000..82f8841aa
--- /dev/null
+++ b/plugins/dribbble/src/constants.ts
@@ -0,0 +1,66 @@
+import { ManagedCollectionField } from "framer-plugin"
+
+export const SHOT_FIELDS: ManagedCollectionField[] = [
+ {
+ id: "id",
+ name: "Id",
+ type: "number",
+ userEditable: false,
+ },
+ {
+ id: "title",
+ name: "Title",
+ type: "string",
+ userEditable: false,
+ },
+ {
+ id: "images.hidpi",
+ name: "Image",
+ type: "image",
+ userEditable: false,
+ },
+ {
+ id: "low_profile",
+ name: "Low Profile",
+ type: "boolean",
+ userEditable: false,
+ },
+ {
+ id: "html_url",
+ name: "URL",
+ type: "link",
+ userEditable: false,
+ },
+ {
+ id: "published_at",
+ name: "Published At",
+ type: "date",
+ userEditable: false,
+ },
+ {
+ id: "updated_at",
+ name: "Updated At",
+ type: "date",
+ userEditable: false,
+ },
+ {
+ id: "animated",
+ name: "Animated",
+ type: "boolean",
+ userEditable: false,
+ },
+ // These fields SHOULD work but the Dribbble API
+ // returns null for some reason
+ // {
+ // id: "description",
+ // name: "Description",
+ // type: "formattedText",
+ // userEditable: false,
+ // },
+ // {
+ // id: "video.url",
+ // name: "Video",
+ // type: "image",
+ // userEditable: false,
+ // },
+]
diff --git a/plugins/dribbble/src/globals.css b/plugins/dribbble/src/globals.css
new file mode 100644
index 000000000..8cbe1cc82
--- /dev/null
+++ b/plugins/dribbble/src/globals.css
@@ -0,0 +1,64 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ h6 {
+ @apply font-semibold text-primary leading-[1.2];
+ }
+}
+
+@layer utilities {
+ /* Chrome, Safari and Opera */
+ .no-scrollbar::-webkit-scrollbar {
+ display: none;
+ }
+
+ .no-scrollbar {
+ -ms-overflow-style: none; /* IE and Edge */
+ scrollbar-width: none; /* Firefox */
+ }
+}
+
+@layer components {
+ .row {
+ display: flex;
+ flex-direction: row;
+ gap: 10px;
+ }
+
+ .col {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .row-lg {
+ display: flex;
+ flex-direction: row;
+ gap: 15px;
+ }
+
+ .col-lg {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ }
+}
+
+body,
+html,
+#root {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
+
+main {
+ width: 100%;
+ height: 100%;
+ padding: 15px;
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
diff --git a/plugins/dribbble/src/main.tsx b/plugins/dribbble/src/main.tsx
new file mode 100644
index 000000000..49bb6da58
--- /dev/null
+++ b/plugins/dribbble/src/main.tsx
@@ -0,0 +1,64 @@
+import "./globals.css"
+import "framer-plugin/framer.css"
+
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
+import React, { Suspense } from "react"
+import ReactDOM from "react-dom/client"
+import { App } from "./App.tsx"
+import { framer } from "framer-plugin"
+import { ErrorBoundary } from "react-error-boundary"
+import { CenteredSpinner } from "./components/CenteredSpinner.tsx"
+import { ErrorBoundaryFallback } from "./components/ErrorBoundaryFallback.tsx"
+import { getPluginContext, shouldSyncImmediately, syncShots } from "./sync.ts"
+import { assert } from "./utils.ts"
+
+export const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: 1,
+ staleTime: 1000 * 60 * 5,
+ refetchOnWindowFocus: false,
+ throwOnError: true,
+ },
+ },
+})
+
+function renderPlugin(app: React.ReactNode) {
+ const root = document.getElementById("root")
+ if (!root) throw new Error("Root element not found")
+
+ ReactDOM.createRoot(root).render(
+
+
+
+ }>{app}
+
+
+
+ )
+}
+
+async function runPlugin() {
+ const mode = framer.mode
+
+ try {
+ const pluginContext = await getPluginContext()
+
+ if (mode === "syncManagedCollection" && shouldSyncImmediately(pluginContext)) {
+ assert(pluginContext.slugFieldId)
+ return syncShots({
+ fields: pluginContext.collectionFields,
+ slugFieldId: pluginContext.slugFieldId,
+ includedFieldIds: pluginContext.includedFieldIds,
+ }).then(() => framer.closePlugin())
+ }
+ renderPlugin()
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error)
+ framer.closePlugin("An unexpected error ocurred: " + message, {
+ variant: "error",
+ })
+ }
+}
+
+runPlugin()
diff --git a/plugins/dribbble/src/pages/Authenticate.tsx b/plugins/dribbble/src/pages/Authenticate.tsx
new file mode 100644
index 000000000..16f835595
--- /dev/null
+++ b/plugins/dribbble/src/pages/Authenticate.tsx
@@ -0,0 +1,65 @@
+import { useRef, useState } from "react"
+import auth from "../auth"
+import { Button } from "../components/Button"
+import { getPluginContext, PluginContext } from "../sync"
+import { framer } from "framer-plugin"
+
+interface Props {
+ onAuthenticated: (context: PluginContext) => void
+}
+
+export default function Authenticate({ onAuthenticated }: Props) {
+ const [isLoading, setIsLoading] = useState(false)
+ const pollInterval = useRef>()
+
+ const pollForTokens = (readKey: string) => {
+ if (pollInterval.current) {
+ clearInterval(pollInterval.current)
+ }
+
+ return new Promise(
+ resolve =>
+ (pollInterval.current = setInterval(
+ () =>
+ auth.fetchTokens(readKey).then(tokens => {
+ clearInterval(pollInterval.current)
+ resolve(tokens)
+ }),
+ 1500
+ ))
+ )
+ }
+
+ const login = async () => {
+ setIsLoading(true)
+
+ try {
+ const authorization = await auth.authorize()
+ window.open(authorization.url)
+ await pollForTokens(authorization.readKey)
+
+ onAuthenticated(await getPluginContext())
+ } catch (e) {
+ framer.notify(e instanceof Error ? e.message : JSON.stringify(e), { variant: "error" })
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+

+
+
Connect to Dribbble
+
+ Sign in to Dribbble to sync shots and import your content.
+
+
+
+
+
+ )
+}
diff --git a/plugins/dribbble/src/pages/MapFields.tsx b/plugins/dribbble/src/pages/MapFields.tsx
new file mode 100644
index 000000000..5f82987d4
--- /dev/null
+++ b/plugins/dribbble/src/pages/MapFields.tsx
@@ -0,0 +1,115 @@
+import { useState } from "react"
+import { Button } from "../components/Button"
+import { SHOT_FIELDS } from "../constants"
+import { assert, isDefined } from "../utils"
+import { getPossibleSlugFields, useLoggingToggle } from "../cms"
+import { FieldMapper } from "../components/FieldMapper"
+import { PluginContext, SyncShotsMutationOptions } from "../sync"
+import { ManagedCollectionField } from "framer-plugin"
+
+interface Props {
+ context: PluginContext
+ onSubmit: (opts: SyncShotsMutationOptions) => void
+ isLoading: boolean
+}
+
+const getInitialSlugFieldId = (context: PluginContext, slugFields: ManagedCollectionField[]): string | null => {
+ if (context.type === "update" && context.slugFieldId) return context.slugFieldId
+
+ return slugFields.length > 0 ? slugFields[0].id : null
+}
+
+export default function MapFields({ context, onSubmit, isLoading }: Props) {
+ useLoggingToggle()
+
+ const [includedFieldIds, setIncludedFieldIds] = useState>(
+ () => new Set(context.type === "update" ? context.includedFieldIds : SHOT_FIELDS.map(field => field.id))
+ )
+
+ const slugFields = getPossibleSlugFields(SHOT_FIELDS).filter(field => includedFieldIds.has(field.id))
+ const [slugFieldId, setSlugFieldId] = useState(() => getInitialSlugFieldId(context, slugFields))
+
+ const [collectionFieldConfig] = useState(
+ SHOT_FIELDS.map(field => ({
+ field,
+ originalFieldName: field.name,
+ }))
+ )
+ const [fieldNameOverrides, setFieldNameOverrides] = useState>(() =>
+ context.type === "update"
+ ? Object.fromEntries(context.collectionFields.map(field => [field.id, field.name]))
+ : {}
+ )
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+ assert(slugFieldId)
+
+ const allFields = collectionFieldConfig
+ .filter(fieldConfig => fieldConfig.field && includedFieldIds.has(fieldConfig.field.id))
+ .map(fieldConfig => fieldConfig.field)
+ .filter(isDefined)
+ .map(field => {
+ // Create copy to prevent showing overriden name temporarily in the UI
+ const fieldCopy = { ...field }
+ if (fieldNameOverrides[field.id]) {
+ fieldCopy.name = fieldNameOverrides[field.id]
+ }
+
+ return field
+ })
+
+ onSubmit({ includedFieldIds: Array.from(includedFieldIds), fields: allFields, slugFieldId })
+ }
+
+ return (
+
+ )
+}
diff --git a/plugins/dribbble/src/sync.ts b/plugins/dribbble/src/sync.ts
new file mode 100644
index 000000000..88c78c5c5
--- /dev/null
+++ b/plugins/dribbble/src/sync.ts
@@ -0,0 +1,227 @@
+import { framer, ManagedCollection, ManagedCollectionField } from "framer-plugin"
+import { useMutation } from "@tanstack/react-query"
+import { fetchAllShots, Shot } from "./api"
+import { isDefined } from "./utils"
+import { SHOT_FIELDS } from "./constants"
+import {
+ computeFieldSets,
+ createFieldSetHash,
+ FieldsById,
+ logSyncResult,
+ MAX_CMS_ITEMS,
+ slugify,
+ SyncResult,
+ SyncStatus,
+} from "./cms"
+import auth from "./auth"
+
+const PLUGIN_INCLUDED_FIELDS_HASH = "dribbblePluginShotIncludedFieldsHash"
+const PLUGIN_SLUG_FIELD_ID_KEY = "dribbblePluginSlugFieldId"
+
+export interface SyncShotsMutationOptions {
+ fields: ManagedCollectionField[]
+ includedFieldIds: string[]
+ slugFieldId: string
+}
+
+export interface ProcessShotsParams {
+ fields: ManagedCollectionField[]
+ fieldsById: FieldsById
+ slugFieldId: string
+ unsyncedItemIds: Set
+}
+
+export interface ProcessShotParams extends ProcessShotsParams {
+ shot: Shot
+ status: SyncStatus
+}
+
+export interface PluginContextNew {
+ type: "new"
+ collection: ManagedCollection
+ isAuthenticated: boolean
+}
+
+export interface PluginContextUpdate {
+ type: "update"
+ collection: ManagedCollection
+ collectionFields: ManagedCollectionField[]
+ includedFieldIds: string[]
+ slugFieldId: string
+ isAuthenticated: boolean
+}
+
+export type PluginContext = PluginContextNew | PluginContextUpdate
+
+export function shouldSyncImmediately(pluginContext: PluginContext): pluginContext is PluginContextUpdate {
+ if (pluginContext.type !== "update") return false
+
+ return true
+}
+
+export async function getPluginContext(): Promise {
+ const collection = await framer.getManagedCollection()
+ const collectionFields = await collection.getFields()
+
+ const isAuthenticated = auth.isAuthenticated()
+ const allPossibleFieldIds = SHOT_FIELDS.map(field => field.id)
+
+ const [slugFieldId, rawIncludedFieldHash] = await Promise.all([
+ collection.getPluginData(PLUGIN_SLUG_FIELD_ID_KEY),
+ collection.getPluginData(PLUGIN_INCLUDED_FIELDS_HASH),
+ ])
+
+ if (!rawIncludedFieldHash || !slugFieldId) {
+ return {
+ type: "new",
+ collection,
+ isAuthenticated,
+ }
+ }
+
+ const { includedFieldIds } = computeFieldSets({
+ currentFields: collectionFields,
+ allPossibleFieldIds,
+ storedHash: rawIncludedFieldHash,
+ })
+
+ return {
+ type: "update",
+ collection,
+ collectionFields,
+ includedFieldIds,
+ isAuthenticated,
+ slugFieldId,
+ }
+}
+
+// eslint-disable-next-line
+function isShotPropertyValueMissing(value: any): boolean {
+ // Usual suspects
+ if (value === null || value === undefined || value === "") {
+ return true
+ }
+
+ // Empty array
+ if (Array.isArray(value)) {
+ return value.length === 0
+ }
+
+ // Empty object
+ if (typeof value === "object") {
+ return Object.keys(value).length === 0
+ }
+
+ return false
+}
+
+function processShot({ shot, slugFieldId, fieldsById, unsyncedItemIds, status }: ProcessShotParams) {
+ let slugValue: string | null = null
+ const fieldData: Record = {}
+ const fieldId = String(shot.id)
+ unsyncedItemIds.delete(fieldId)
+
+ for (const [propertyName, field] of fieldsById) {
+ // Allow for nested dot notation to access fields e.g. images.hidpi
+ let value: unknown
+ if (propertyName.includes(".")) {
+ value = propertyName.split(".").reduce((obj: unknown, key) => {
+ if (obj && typeof obj === "object") {
+ return (obj as Record)[key]
+ }
+ return undefined
+ }, shot)
+ } else {
+ value = shot[propertyName as keyof Shot]
+ }
+
+ if (field.id === slugFieldId && typeof value === "string") {
+ slugValue = slugify(value)
+ }
+
+ if (isShotPropertyValueMissing(value)) {
+ status.warnings.push({
+ fieldName: propertyName,
+ message: `Value is missing for field ${field.name}`,
+ })
+ }
+
+ fieldData[propertyName] = value
+ }
+
+ if (!slugValue) {
+ status.warnings.push({ message: "Slug missing. Skipping item." })
+ return null
+ }
+
+ return { id: fieldId, slug: slugValue, fieldData }
+}
+
+function processShots(shots: Shot[], processShotsParams: ProcessShotsParams) {
+ const seenItemIds = new Set()
+ const status: SyncStatus = {
+ info: [],
+ warnings: [],
+ errors: [],
+ }
+
+ const collectionItems = shots.map(shot => processShot({ shot, status, ...processShotsParams })).filter(isDefined)
+
+ return {
+ collectionItems,
+ status,
+ seenItemIds,
+ }
+}
+
+export async function syncShots({ fields, slugFieldId, includedFieldIds }: SyncShotsMutationOptions) {
+ const collection = await framer.getManagedCollection()
+ await collection.setFields(fields)
+
+ const fieldsById = new Map(fields.map(field => [field.id, field]))
+ const unsyncedItemIds = new Set(await collection.getItemIds())
+
+ const allShots = await fetchAllShots(MAX_CMS_ITEMS)
+
+ const { collectionItems, status } = processShots(allShots, {
+ fields,
+ fieldsById,
+ unsyncedItemIds,
+ slugFieldId,
+ })
+
+ await collection.addItems(collectionItems)
+
+ const itemsToDelete = Array.from(unsyncedItemIds)
+ await collection.removeItems(itemsToDelete)
+
+ await Promise.all([
+ await collection.setPluginData(PLUGIN_INCLUDED_FIELDS_HASH, createFieldSetHash(includedFieldIds)),
+ await collection.setPluginData(PLUGIN_SLUG_FIELD_ID_KEY, slugFieldId),
+ ])
+
+ const result: SyncResult = {
+ status: status.errors.length === 0 ? "success" : "completed_with_errors",
+ errors: status.errors,
+ info: status.info,
+ warnings: status.warnings,
+ }
+
+ logSyncResult(result, collectionItems)
+
+ return result
+}
+
+export const useSyncShotsMutation = ({
+ onSuccess,
+ onError,
+}: {
+ onSuccess?: (result: SyncResult) => void
+ onError?: (e: Error) => void
+}) => {
+ return useMutation({
+ mutationFn: (args: SyncShotsMutationOptions) => syncShots(args),
+ onSuccess,
+ onError,
+ })
+}
diff --git a/plugins/dribbble/src/utils.ts b/plugins/dribbble/src/utils.ts
new file mode 100644
index 000000000..a1e224f70
--- /dev/null
+++ b/plugins/dribbble/src/utils.ts
@@ -0,0 +1,25 @@
+export function assert(condition: unknown, ...msg: unknown[]): asserts condition {
+ if (condition) return
+
+ const e = Error("Assertion Error" + (msg.length > 0 ? ": " + msg.join(" ") : ""))
+ // Hack the stack so the assert call itself disappears. Works in jest and in chrome.
+ if (e.stack) {
+ try {
+ const lines = e.stack.split("\n")
+ if (lines[1]?.includes("assert")) {
+ lines.splice(1, 1)
+ e.stack = lines.join("\n")
+ } else if (lines[0]?.includes("assert")) {
+ lines.splice(0, 1)
+ e.stack = lines.join("\n")
+ }
+ } catch {
+ // nothing
+ }
+ }
+ throw e
+}
+
+export function isDefined(value: T): value is NonNullable {
+ return value !== undefined && value !== null
+}
diff --git a/plugins/dribbble/src/vite-env.d.ts b/plugins/dribbble/src/vite-env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/plugins/dribbble/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/plugins/dribbble/tailwind.config.js b/plugins/dribbble/tailwind.config.js
new file mode 100644
index 000000000..6bad2ded5
--- /dev/null
+++ b/plugins/dribbble/tailwind.config.js
@@ -0,0 +1,45 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ darkMode: ["class", "[data-framer-theme='dark']"],
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
+ theme: {
+ extend: {
+ backgroundColor: {
+ primary: "var(--framer-color-bg)",
+ secondary: "var(--framer-color-bg-secondary)",
+ tertiary: "var(--framer-color-bg-tertiary)",
+ tertiaryDimmedLight: "rgba(243, 243, 243, 0.75)",
+ tertiaryDimmedDark: "rgba(43, 43, 43, 0.75)",
+ divider: "var(--framer-color-divider)",
+ tint: "var(--framer-color-tint)",
+ tintDimmed: "var(--framer-color-tint-dimmed)",
+ tintDark: "var(--framer-color-tint-dark)",
+ blackDimmed: "rgba(0, 0, 0, 0.5)",
+ "dribbble-primary": "#EA4C89",
+ "framer-red": "#FF3366",
+ "framer-blue": "#0099FF",
+ },
+ colors: {
+ primary: "var(--framer-color-text)",
+ secondary: "var(--framer-color-text-secondary)",
+ tertiary: "var(--framer-color-text-tertiary)",
+ inverted: "var(--framer-color-text-inverted)",
+ tint: "var(--framer-color-tint)",
+ "dribbble-primary": "#EA4C89",
+ "framer-red": "#FF3366",
+ },
+ borderColor: {
+ divider: "var(--framer-color-divider)",
+ },
+ fontSize: {
+ "2xs": "10px",
+ },
+ padding: {
+ 15: "15px",
+ },
+ gridTemplateColumns: {
+ fieldPicker: "1fr 8px 1fr",
+ },
+ },
+ },
+}
diff --git a/plugins/dribbble/tsconfig.json b/plugins/dribbble/tsconfig.json
new file mode 100644
index 000000000..325606c67
--- /dev/null
+++ b/plugins/dribbble/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ES2022",
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/plugins/dribbble/vite.config.ts b/plugins/dribbble/vite.config.ts
new file mode 100644
index 000000000..7efb6ef85
--- /dev/null
+++ b/plugins/dribbble/vite.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from "vite"
+import react from "@vitejs/plugin-react-swc"
+import mkcert from "vite-plugin-mkcert"
+import framer from "vite-plugin-framer"
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react(), mkcert(), framer()],
+ build: {
+ target: "ES2022",
+ },
+})