Merge pull request #244 from sambecker/key-commmands

Introduce new key commands
This commit is contained in:
Sam Becker 2025-04-27 00:01:11 -05:00 committed by GitHub
commit 866c9fb273
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 833 additions and 443 deletions

View File

@ -20,6 +20,7 @@
"exif",
"exiftool",
"favicons",
"Favoriting",
"favs",
"ghijklmnopqrstuv",
"GPSH",
@ -53,6 +54,7 @@
"thephotoblog",
"trpc",
"Turbopack",
"Unfavoriting",
"unnest",
"upstash",
"UsKSGcbt",

View File

@ -9,12 +9,12 @@
"analyze": "ANALYZE=true next build"
},
"dependencies": {
"@ai-sdk/openai": "^1.3.16",
"@ai-sdk/openai": "^1.3.18",
"@aws-sdk/client-s3": "3.787.0",
"@aws-sdk/s3-request-presigner": "3.787.0",
"@radix-ui/react-dialog": "^1.1.10",
"@radix-ui/react-dropdown-menu": "^2.1.11",
"@radix-ui/react-tooltip": "^1.2.3",
"@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-tooltip": "^1.2.4",
"@radix-ui/react-visually-hidden": "^1.2.0",
"@upstash/ratelimit": "^2.0.5",
"@upstash/redis": "^1.34.8",
@ -28,12 +28,12 @@
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"fast-deep-equal": "^3.1.3",
"framer-motion": "^12.7.4",
"framer-motion": "^12.9.1",
"nanoid": "^5.1.5",
"next": "15.3.1",
"next-auth": "5.0.0-beta.25",
"next-themes": "^0.4.6",
"pg": "^8.14.1",
"pg": "^8.15.5",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-icons": "^5.5.0",
@ -57,12 +57,12 @@
"@testing-library/react": "^16.3.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.14.1",
"@types/pg": "^8.11.13",
"@types/pg": "^8.11.14",
"@types/react": "19.1.2",
"@types/react-dom": "19.1.2",
"@types/sanitize-html": "^2.15.0",
"cross-fetch": "^4.1.0",
"eslint": "9.25.0",
"eslint": "9.25.1",
"eslint-config-next": "15.3.1",
"eslint-plugin-react-hooks": "^5.2.0",
"jest": "^29.7.0",

249
pnpm-lock.yaml generated
View File

@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@ai-sdk/openai':
specifier: ^1.3.16
version: 1.3.16(zod@3.24.2)
specifier: ^1.3.18
version: 1.3.18(zod@3.24.2)
'@aws-sdk/client-s3':
specifier: 3.787.0
version: 3.787.0
@ -18,14 +18,14 @@ importers:
specifier: 3.787.0
version: 3.787.0
'@radix-ui/react-dialog':
specifier: ^1.1.10
version: 1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
specifier: ^1.1.11
version: 1.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-dropdown-menu':
specifier: ^2.1.11
version: 2.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
specifier: ^2.1.12
version: 2.1.12(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-tooltip':
specifier: ^1.2.3
version: 1.2.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
specifier: ^1.2.4
version: 1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-visually-hidden':
specifier: ^1.2.0
version: 1.2.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@ -66,8 +66,8 @@ importers:
specifier: ^3.1.3
version: 3.1.3
framer-motion:
specifier: ^12.7.4
version: 12.7.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
specifier: ^12.9.1
version: 12.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
nanoid:
specifier: ^5.1.5
version: 5.1.5
@ -81,8 +81,8 @@ importers:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
pg:
specifier: ^8.14.1
version: 8.14.1
specifier: ^8.15.5
version: 8.15.5
react:
specifier: 19.1.0
version: 19.1.0
@ -148,8 +148,8 @@ importers:
specifier: ^22.14.1
version: 22.14.1
'@types/pg':
specifier: ^8.11.13
version: 8.11.13
specifier: ^8.11.14
version: 8.11.14
'@types/react':
specifier: 19.1.2
version: 19.1.2
@ -163,14 +163,14 @@ importers:
specifier: ^4.1.0
version: 4.1.0
eslint:
specifier: 9.25.0
version: 9.25.0(jiti@2.4.2)
specifier: 9.25.1
version: 9.25.1(jiti@2.4.2)
eslint-config-next:
specifier: 15.3.1
version: 15.3.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
version: 15.3.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
eslint-plugin-react-hooks:
specifier: ^5.2.0
version: 5.2.0(eslint@9.25.0(jiti@2.4.2))
version: 5.2.0(eslint@9.25.1(jiti@2.4.2))
jest:
specifier: ^29.7.0
version: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3))
@ -195,8 +195,8 @@ packages:
'@adobe/css-tools@4.4.2':
resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==}
'@ai-sdk/openai@1.3.16':
resolution: {integrity: sha512-pjtiBKt1GgaSKZryTbM3tqgoegJwgAUlp1+X5uN6T+VPnI4FLSymV65tyloWzDlyqZmi9HXnnSRPu76VoL5D5g==}
'@ai-sdk/openai@1.3.18':
resolution: {integrity: sha512-gqOHTOu62Tm2r4yDQx/Z5tWAgUrcTK8wXnC4A8zF/VOCzIjJDxxPsqJRTtQTMgIdGXhwmsv2sZ2PzvvuLeZeEg==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
@ -610,8 +610,8 @@ packages:
resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@9.25.0':
resolution: {integrity: sha512-iWhsUS8Wgxz9AXNfvfOPFSW4VfMXdVhp1hjkZVhXCrpgh/aLcc45rX6MPu+tIVUWDw0HfNwth7O28M1xDxNf9w==}
'@eslint/js@9.25.1':
resolution: {integrity: sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/object-schema@2.1.6':
@ -1005,8 +1005,8 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-dialog@1.1.10':
resolution: {integrity: sha512-m6pZb0gEM5uHPSb+i2nKKGQi/HMSVjARMsLMWQfKDP+eJ6B+uqryHnXhpnohTWElw+vEcMk/o4wJODtdRKHwqg==}
'@radix-ui/react-dialog@1.1.11':
resolution: {integrity: sha512-yI7S1ipkP5/+99qhSI6nthfo/tR6bL6Zgxi/+1UO6qPa6UeM6nlafWcQ65vB4rU2XjgjMfMhI3k9Y5MztA62VQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@ -1040,8 +1040,8 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-dropdown-menu@2.1.11':
resolution: {integrity: sha512-wbPE3cFBfLl+S+LCxChWQGX0k14zUxgvep1HEnLhJ9mNhjyO3ETzRviAeKZ3XomT/iVRRZAWFsnFZ3N0wI8OmA==}
'@radix-ui/react-dropdown-menu@2.1.12':
resolution: {integrity: sha512-VJoMs+BWWE7YhzEQyVwvF9n22Eiyr83HotCVrMQzla/OwRovXCgah7AcaEr4hMNj4gJxSdtIbcHGvmJXOoJVHA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@ -1093,8 +1093,8 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-menu@2.1.11':
resolution: {integrity: sha512-sbFI4Qaw02J0ogmR9tOMsSqsdrGNpUanlPYAqTE2JJafow8ecHtykg4fSTjNHBdDl4deiKMK+RhTEwyVhP7UDA==}
'@radix-ui/react-menu@2.1.12':
resolution: {integrity: sha512-+qYq6LfbiGo97Zz9fioX83HCiIYYFNs8zAsVCMQrIakoNYylIzWuoD/anAD3UzvvR6cnswmfRFJFq/zYYq/k7Q==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@ -1132,8 +1132,8 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-presence@1.1.3':
resolution: {integrity: sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==}
'@radix-ui/react-presence@1.1.4':
resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@ -1202,8 +1202,8 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-tooltip@1.2.3':
resolution: {integrity: sha512-0KX7jUYFA02np01Y11NWkk6Ip6TqMNmD4ijLelYAzeIndl2aVeltjJFJ2gwjNa1P8U/dgjQ+8cr9Y3Ni+ZNoRA==}
'@radix-ui/react-tooltip@1.2.4':
resolution: {integrity: sha512-DyW8VVeeMSSLFvAmnVnCwvI3H+1tpJFHT50r+tdOoMse9XqYDBCcyux8u3G2y+LOpt7fPQ6KKH0mhs+ce1+Z5w==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@ -1724,8 +1724,8 @@ packages:
'@types/node@22.14.1':
resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==}
'@types/pg@8.11.13':
resolution: {integrity: sha512-6kXByGkvRvwXLuyaWzsebs2du6+XuAB2CuMsuzP7uaihQahshVgSmB22Pmh0vQMkQ1h5+PZU0d+Di1o+WpVWJg==}
'@types/pg@8.11.14':
resolution: {integrity: sha512-qyD11E5R3u0eJmd1lB0WnWKXJGA7s015nyARWljfz5DcX83TKAIlY+QrmvzQTsbIe+hkiFtkyL2gHC6qwF6Fbg==}
'@types/react-dom@19.1.2':
resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==}
@ -2498,8 +2498,8 @@ packages:
resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint@9.25.0:
resolution: {integrity: sha512-MsBdObhM4cEwkzCiraDv7A6txFXEqtNXOb877TsSp2FCkBNl8JfVQrmiuDqC1IkejT6JLPzYBXx/xAiYhyzgGA==}
eslint@9.25.1:
resolution: {integrity: sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
hasBin: true
peerDependencies:
@ -2611,8 +2611,8 @@ packages:
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
engines: {node: '>= 6'}
framer-motion@12.7.4:
resolution: {integrity: sha512-jX0bPsTmU0oPZTYz/dVyD0dmOyEOEJvdn0TaZBE5I8g2GvVnnQnW9f65cJnoVfUkY3WZWNXGXnPbVA9YnaIfVA==}
framer-motion@12.9.1:
resolution: {integrity: sha512-dZBp2TO0a39Cc24opshlLoM0/OdTZVKzcXWuhntfwy2Qgz3t9+N4sTyUqNANyHaRFiJUWbwwsXeDvQkEBPky+g==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
@ -3342,11 +3342,11 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
motion-dom@12.7.4:
resolution: {integrity: sha512-1ZUHAoSUMMxP6jPqyxlk9XUfb6NxMsnWPnH2YGhrOhTURLcXWbETi6eemoKb60Pe32NVJYduL4B62VQSO5Jq8Q==}
motion-dom@12.9.1:
resolution: {integrity: sha512-xqXEwRLDYDTzOgXobSoWtytRtGlf7zdkRfFbrrdP7eojaGQZ5Go4OOKtgnx7uF8sAkfr1ZjMvbCJSCIT2h6fkQ==}
motion-utils@12.7.2:
resolution: {integrity: sha512-XhZwqctxyJs89oX00zn3OGCuIIpVevbTa+u82usWBC6pSHUd2AoNWiYa7Du8tJxJy9TFbZ82pcn5t7NOm1PHAw==}
motion-utils@12.8.3:
resolution: {integrity: sha512-GYVauZEbca8/zOhEiYOY9/uJeedYQld6co/GJFKOy//0c/4lDqk0zB549sBYqqV2iMuX+uHrY1E5zd8A2L+1Lw==}
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
@ -3543,11 +3543,11 @@ packages:
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
pg-cloudflare@1.1.1:
resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==}
pg-cloudflare@1.2.5:
resolution: {integrity: sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==}
pg-connection-string@2.7.0:
resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==}
pg-connection-string@2.8.5:
resolution: {integrity: sha512-Ni8FuZ8yAF+sWZzojvtLE2b03cqjO5jNULcHFfM9ZZ0/JXrgom5pBREbtnAw7oxsxJqHw9Nz/XWORUEL3/IFow==}
pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
@ -3557,14 +3557,17 @@ packages:
resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==}
engines: {node: '>=4'}
pg-pool@3.8.0:
resolution: {integrity: sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==}
pg-pool@3.9.5:
resolution: {integrity: sha512-DxyAlOgvUzRFpFAZjbCc8fUfG7BcETDHgepFPf724B0i08k9PAiZV1tkGGgQIL0jbMEuR9jW1YN7eX+WgXxCsQ==}
peerDependencies:
pg: '>=8.0'
pg-protocol@1.8.0:
resolution: {integrity: sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==}
pg-protocol@1.9.5:
resolution: {integrity: sha512-DYTWtWpfd5FOro3UnAfwvhD8jh59r2ig8bPtc9H8Ds7MscE/9NYruUQWFAOuraRl29jwcT2kyMFQ3MxeaVjUhg==}
pg-types@2.2.0:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
@ -3573,8 +3576,8 @@ packages:
resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==}
engines: {node: '>=10'}
pg@8.14.1:
resolution: {integrity: sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==}
pg@8.15.5:
resolution: {integrity: sha512-EpAhHFQc+aH9VfeffWIVC+XXk6lmAhS9W1FxtxcPXs94yxhrI1I6w/zkWfIOII/OkBv3Be04X3xMOj0kQ78l6w==}
engines: {node: '>= 8.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
@ -4357,7 +4360,7 @@ snapshots:
'@adobe/css-tools@4.4.2': {}
'@ai-sdk/openai@1.3.16(zod@3.24.2)':
'@ai-sdk/openai@1.3.18(zod@3.24.2)':
dependencies:
'@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.7(zod@3.24.2)
@ -5086,9 +5089,9 @@ snapshots:
tslib: 2.8.1
optional: true
'@eslint-community/eslint-utils@4.4.1(eslint@9.25.0(jiti@2.4.2))':
'@eslint-community/eslint-utils@4.4.1(eslint@9.25.1(jiti@2.4.2))':
dependencies:
eslint: 9.25.0(jiti@2.4.2)
eslint: 9.25.1(jiti@2.4.2)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {}
@ -5121,7 +5124,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@eslint/js@9.25.0': {}
'@eslint/js@9.25.1': {}
'@eslint/object-schema@2.1.6': {}
@ -5532,7 +5535,7 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.2
'@radix-ui/react-dialog@1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
'@radix-ui/react-dialog@1.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
@ -5542,7 +5545,7 @@ snapshots:
'@radix-ui/react-focus-scope': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-portal': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-presence': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0)
@ -5573,13 +5576,13 @@ snapshots:
'@types/react': 19.1.2
'@types/react-dom': 19.1.2(@types/react@19.1.2)
'@radix-ui/react-dropdown-menu@2.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
'@radix-ui/react-dropdown-menu@2.1.12(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-menu': 2.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-menu': 2.1.12(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0)
react: 19.1.0
@ -5619,7 +5622,7 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.2
'@radix-ui/react-menu@2.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
'@radix-ui/react-menu@2.1.12(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-collection': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@ -5632,7 +5635,7 @@ snapshots:
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-popper': 1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-portal': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-presence': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-roving-focus': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0)
@ -5673,7 +5676,7 @@ snapshots:
'@types/react': 19.1.2
'@types/react-dom': 19.1.2(@types/react@19.1.2)
'@radix-ui/react-presence@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
'@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0)
@ -5732,7 +5735,7 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.2
'@radix-ui/react-tooltip@1.2.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
'@radix-ui/react-tooltip@1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
@ -5741,7 +5744,7 @@ snapshots:
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-popper': 1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-portal': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-presence': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0)
@ -6346,7 +6349,7 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@types/pg@8.11.13':
'@types/pg@8.11.14':
dependencies:
'@types/node': 22.14.1
pg-protocol: 1.8.0
@ -6374,15 +6377,15 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
'@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/parser': 8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/scope-manager': 8.24.1
'@typescript-eslint/type-utils': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/utils': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/type-utils': 8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/utils': 8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.24.1
eslint: 9.25.0(jiti@2.4.2)
eslint: 9.25.1(jiti@2.4.2)
graphemer: 1.4.0
ignore: 5.3.2
natural-compare: 1.4.0
@ -6391,14 +6394,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
'@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.24.1
'@typescript-eslint/types': 8.24.1
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.24.1
debug: 4.4.0
eslint: 9.25.0(jiti@2.4.2)
eslint: 9.25.1(jiti@2.4.2)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
@ -6408,12 +6411,12 @@ snapshots:
'@typescript-eslint/types': 8.24.1
'@typescript-eslint/visitor-keys': 8.24.1
'@typescript-eslint/type-utils@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
'@typescript-eslint/type-utils@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.3)
'@typescript-eslint/utils': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/utils': 8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
debug: 4.4.0
eslint: 9.25.0(jiti@2.4.2)
eslint: 9.25.1(jiti@2.4.2)
ts-api-utils: 2.0.1(typescript@5.8.3)
typescript: 5.8.3
transitivePeerDependencies:
@ -6435,13 +6438,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
'@typescript-eslint/utils@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.25.0(jiti@2.4.2))
'@eslint-community/eslint-utils': 4.4.1(eslint@9.25.1(jiti@2.4.2))
'@typescript-eslint/scope-manager': 8.24.1
'@typescript-eslint/types': 8.24.1
'@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.3)
eslint: 9.25.0(jiti@2.4.2)
eslint: 9.25.1(jiti@2.4.2)
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
@ -6795,7 +6798,7 @@ snapshots:
cmdk@1.1.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-dialog': 1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-dialog': 1.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-id': 1.1.0(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
@ -7126,19 +7129,19 @@ snapshots:
optionalDependencies:
source-map: 0.6.1
eslint-config-next@15.3.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3):
eslint-config-next@15.3.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3):
dependencies:
'@next/eslint-plugin-next': 15.3.1
'@rushstack/eslint-patch': 1.10.5
'@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/parser': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.25.0(jiti@2.4.2)
'@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/parser': 8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.25.1(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.25.0(jiti@2.4.2))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.0(jiti@2.4.2))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.25.0(jiti@2.4.2))
eslint-plugin-react: 7.37.4(eslint@9.25.0(jiti@2.4.2))
eslint-plugin-react-hooks: 5.2.0(eslint@9.25.0(jiti@2.4.2))
eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.1(jiti@2.4.2))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.25.1(jiti@2.4.2))
eslint-plugin-react: 7.37.4(eslint@9.25.1(jiti@2.4.2))
eslint-plugin-react-hooks: 5.2.0(eslint@9.25.1(jiti@2.4.2))
optionalDependencies:
typescript: 5.8.3
transitivePeerDependencies:
@ -7154,33 +7157,33 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.8.1(eslint-plugin-import@2.31.0)(eslint@9.25.0(jiti@2.4.2)):
eslint-import-resolver-typescript@3.8.1(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.0
enhanced-resolve: 5.18.1
eslint: 9.25.0(jiti@2.4.2)
eslint: 9.25.1(jiti@2.4.2)
get-tsconfig: 4.10.0
is-bun-module: 1.3.0
stable-hash: 0.0.4
tinyglobby: 0.2.11
optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.0(jiti@2.4.2))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.1(jiti@2.4.2))
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.0(jiti@2.4.2)):
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.1(jiti@2.4.2)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.25.0(jiti@2.4.2)
'@typescript-eslint/parser': 8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.25.1(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.25.0(jiti@2.4.2))
eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2))
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.0(jiti@2.4.2)):
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.1(jiti@2.4.2)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8
@ -7189,9 +7192,9 @@ snapshots:
array.prototype.flatmap: 1.3.3
debug: 3.2.7
doctrine: 2.1.0
eslint: 9.25.0(jiti@2.4.2)
eslint: 9.25.1(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.0(jiti@2.4.2))
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.1(jiti@2.4.2))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@ -7203,13 +7206,13 @@ snapshots:
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
optionalDependencies:
'@typescript-eslint/parser': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/parser': 8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-jsx-a11y@6.10.2(eslint@9.25.0(jiti@2.4.2)):
eslint-plugin-jsx-a11y@6.10.2(eslint@9.25.1(jiti@2.4.2)):
dependencies:
aria-query: 5.3.2
array-includes: 3.1.8
@ -7219,7 +7222,7 @@ snapshots:
axobject-query: 4.1.0
damerau-levenshtein: 1.0.8
emoji-regex: 9.2.2
eslint: 9.25.0(jiti@2.4.2)
eslint: 9.25.1(jiti@2.4.2)
hasown: 2.0.2
jsx-ast-utils: 3.3.5
language-tags: 1.0.9
@ -7228,11 +7231,11 @@ snapshots:
safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1
eslint-plugin-react-hooks@5.2.0(eslint@9.25.0(jiti@2.4.2)):
eslint-plugin-react-hooks@5.2.0(eslint@9.25.1(jiti@2.4.2)):
dependencies:
eslint: 9.25.0(jiti@2.4.2)
eslint: 9.25.1(jiti@2.4.2)
eslint-plugin-react@7.37.4(eslint@9.25.0(jiti@2.4.2)):
eslint-plugin-react@7.37.4(eslint@9.25.1(jiti@2.4.2)):
dependencies:
array-includes: 3.1.8
array.prototype.findlast: 1.2.5
@ -7240,7 +7243,7 @@ snapshots:
array.prototype.tosorted: 1.1.4
doctrine: 2.1.0
es-iterator-helpers: 1.2.1
eslint: 9.25.0(jiti@2.4.2)
eslint: 9.25.1(jiti@2.4.2)
estraverse: 5.3.0
hasown: 2.0.2
jsx-ast-utils: 3.3.5
@ -7263,15 +7266,15 @@ snapshots:
eslint-visitor-keys@4.2.0: {}
eslint@9.25.0(jiti@2.4.2):
eslint@9.25.1(jiti@2.4.2):
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.25.0(jiti@2.4.2))
'@eslint-community/eslint-utils': 4.4.1(eslint@9.25.1(jiti@2.4.2))
'@eslint-community/regexpp': 4.12.1
'@eslint/config-array': 0.20.0
'@eslint/config-helpers': 0.2.1
'@eslint/core': 0.13.0
'@eslint/eslintrc': 3.3.1
'@eslint/js': 9.25.0
'@eslint/js': 9.25.1
'@eslint/plugin-kit': 0.2.8
'@humanfs/node': 0.16.6
'@humanwhocodes/module-importer': 1.0.1
@ -7421,10 +7424,10 @@ snapshots:
es-set-tostringtag: 2.1.0
mime-types: 2.1.35
framer-motion@12.7.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
framer-motion@12.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
motion-dom: 12.7.4
motion-utils: 12.7.2
motion-dom: 12.9.1
motion-utils: 12.8.3
tslib: 2.8.1
optionalDependencies:
react: 19.1.0
@ -8324,11 +8327,11 @@ snapshots:
minimist@1.2.8: {}
motion-dom@12.7.4:
motion-dom@12.9.1:
dependencies:
motion-utils: 12.7.2
motion-utils: 12.8.3
motion-utils@12.7.2: {}
motion-utils@12.8.3: {}
mrmime@2.0.1: {}
@ -8506,21 +8509,23 @@ snapshots:
path-parse@1.0.7: {}
pg-cloudflare@1.1.1:
pg-cloudflare@1.2.5:
optional: true
pg-connection-string@2.7.0: {}
pg-connection-string@2.8.5: {}
pg-int8@1.0.1: {}
pg-numeric@1.0.2: {}
pg-pool@3.8.0(pg@8.14.1):
pg-pool@3.9.5(pg@8.15.5):
dependencies:
pg: 8.14.1
pg: 8.15.5
pg-protocol@1.8.0: {}
pg-protocol@1.9.5: {}
pg-types@2.2.0:
dependencies:
pg-int8: 1.0.1
@ -8539,15 +8544,15 @@ snapshots:
postgres-interval: 3.0.0
postgres-range: 1.1.4
pg@8.14.1:
pg@8.15.5:
dependencies:
pg-connection-string: 2.7.0
pg-pool: 3.8.0(pg@8.14.1)
pg-protocol: 1.8.0
pg-connection-string: 2.8.5
pg-pool: 3.9.5(pg@8.15.5)
pg-protocol: 1.9.5
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.1.1
pg-cloudflare: 1.2.5
pgpass@1.0.5:
dependencies:

View File

@ -24,9 +24,7 @@ import IconRecipe from '@/components/icons/IconRecipe';
import IconTag from '@/components/icons/IconTag';
import IconFolder from '@/components/icons/IconFolder';
import IconSignOut from '@/components/icons/IconSignOut';
import IconLock from '@/components/icons/IconLock';
import { IoMdCheckboxOutline } from 'react-icons/io';
import Spinner from '@/components/Spinner';
import IconBroom from '@/components/icons/IconBroom';
import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
import MoreMenuItem from '@/components/more/MoreMenuItem';
@ -34,10 +32,14 @@ import MoreMenuItem from '@/components/more/MoreMenuItem';
export default function AdminAppMenu({
active,
animateMenuClose,
isOpen,
setIsOpen,
className,
}: {
active?: boolean
animateMenuClose?: boolean
isOpen?: boolean
setIsOpen?: (isOpen: boolean) => void
className?: string
}) {
const {
@ -46,8 +48,6 @@ export default function AdminAppMenu({
uploadsCount = 0,
tagsCount = 0,
recipesCount = 0,
hasAdminData,
isLoadingAdminData,
selectedPhotoIds,
startUpload,
setSelectedPhotoIds,
@ -80,7 +80,7 @@ export default function AdminAppMenu({
annotation: `${uploadsCount}`,
icon: <IconFolder
size={16}
className="translate-x-[1px] translate-y-[0.5px]"
className="translate-x-[1px] translate-y-[1px]"
/>,
href: PATH_ADMIN_UPLOADS,
});
@ -93,13 +93,13 @@ export default function AdminAppMenu({
{photosCountNeedSync}
</span>
<InsightsIndicatorDot
className="inline-block translate-y-[-0.5px]"
className="inline-block translate-y-[-1px]"
size="small"
/>
</>,
icon: <IconBroom
size={17}
className="translate-y-[0.5px]"
size={18}
className="translate-y-[-0.5px]"
/>,
href: PATH_ADMIN_PHOTOS_UPDATES,
});
@ -112,7 +112,7 @@ export default function AdminAppMenu({
},
icon: <IconPhoto
size={15}
className="translate-x-[-0.5px] translate-y-[0.5px]"
className="translate-x-[-0.5px] translate-y-[1px]"
/>,
href: PATH_ADMIN_PHOTOS,
});
@ -123,7 +123,7 @@ export default function AdminAppMenu({
annotation: `${tagsCount}`,
icon: <IconTag
size={15}
className="translate-y-[0.5px]"
className="translate-y-[1.5px]"
/>,
href: PATH_ADMIN_TAGS,
});
@ -134,7 +134,7 @@ export default function AdminAppMenu({
annotation: `${recipesCount}`,
icon: <IconRecipe
size={17}
className="translate-x-[-0.5px] translate-y-[0.5px]"
className="translate-x-[-0.5px] translate-y-[1px]"
/>,
href: PATH_ADMIN_RECIPES,
});
@ -147,7 +147,7 @@ export default function AdminAppMenu({
icon: isSelecting
? <IoCloseSharp
size={18}
className="translate-x-[-1px] translate-y-[0.5px]"
className="translate-x-[-1px] translate-y-[1px]"
/>
: <IoMdCheckboxOutline
size={16}
@ -174,7 +174,7 @@ export default function AdminAppMenu({
: 'App Configuration',
icon: <AdminAppInfoIcon
size="small"
className="translate-x-[-0.5px] translate-y-[-0.5px]"
className="translate-x-[-0.5px] translate-y-[0.5px]"
/>,
href: showAppInsightsLink
? PATH_ADMIN_INSIGHTS
@ -189,21 +189,7 @@ export default function AdminAppMenu({
return (
<MoreMenu
header={<div className="flex items-center select-none">
<span className="inline-flex items-center justify-center w-5 mr-2">
{!hasAdminData && isLoadingAdminData
? <Spinner
className="translate-x-[1px] translate-y-[1px]"
size={13}
/>
:<IconLock
size={16}
className="translate-x-[1px] translate-y-[0.5px]"
narrow
/>}
</span>
<span className="grow">Admin menu</span>
</div>}
{...{ isOpen, setIsOpen }}
icon={<div className={clsx(
'w-[28px] h-[28px]',
'overflow-hidden',
@ -226,7 +212,7 @@ export default function AdminAppMenu({
'border-medium',
className,
)}
buttonClassName={clsx(
classNameButton={clsx(
'p-0!',
'w-full h-full',
'flex items-center justify-center',
@ -237,7 +223,7 @@ export default function AdminAppMenu({
? 'text-black dark:text-white'
: 'text-gray-400 dark:text-gray-600',
)}
buttonClassNameOpen={clsx(
classNameButtonOpen={clsx(
'bg-dim text-main!',
'[&>*>*]:translate-y-[6px]',
!animateMenuClose && '[&>*>*]:duration-300',

View File

@ -25,16 +25,19 @@ import InsightsIndicatorDot from './insights/InsightsIndicatorDot';
import IconFavs from '@/components/icons/IconFavs';
import IconEdit from '@/components/icons/IconEdit';
import { photoNeedsToBeSynced } from '@/photo/sync';
import { KEY_COMMANDS } from '@/photo/key-commands';
export default function AdminPhotoMenu({
photo,
revalidatePhoto,
includeFavorite = true,
showKeyCommands,
...props
}: Omit<ComponentProps<typeof MoreMenu>, 'sections'> & {
photo: Photo
revalidatePhoto?: RevalidatePhoto
includeFavorite?: boolean
showKeyCommands?: boolean
}) {
const { isUserSignedIn, registerAdminUpdate } = useAppState();
@ -48,32 +51,39 @@ export default function AdminPhotoMenu({
label: 'Edit',
icon: <IconEdit
size={15}
className="translate-x-[0.5px] translate-y-[-0.5px]"
className="translate-x-[0.5px]"
/>,
href: pathForAdminPhotoEdit(photo.id),
...showKeyCommands && { keyCommand: KEY_COMMANDS.edit },
}];
if (includeFavorite) {
sectionMain.push({
label: isFav ? 'Unfavorite' : 'Favorite',
icon: <IconFavs
size={14}
className="translate-x-[-1px]"
className="translate-x-[-1px] translate-y-[0.5px]"
highlight={isFav}
/>,
action: () => toggleFavoritePhotoAction(
photo.id,
shouldRedirectFav,
).then(() => revalidatePhoto?.(photo.id)),
...showKeyCommands && {
keyCommand: isFav
? KEY_COMMANDS.unfavorite
: KEY_COMMANDS.favorite,
},
});
}
sectionMain.push({
label: 'Download',
icon: <MdOutlineFileDownload
size={17}
className="translate-x-[-1px] translate-y-[-0.5px]"
className="translate-x-[-1px]"
/>,
href: photo.url,
hrefDownloadName: downloadFileNameForPhoto(photo),
...showKeyCommands && { keyCommand: KEY_COMMANDS.download },
});
sectionMain.push({
label: 'Sync',
@ -86,17 +96,21 @@ export default function AdminPhotoMenu({
size="small"
/>}
</span>,
icon: <IconGrSync className="translate-x-[-1px]" />,
icon: <IconGrSync
className="translate-x-[-1px] translate-y-[0.5px]"
/>,
action: () => syncPhotoAction(photo.id)
.then(() => revalidatePhoto?.(photo.id)),
...showKeyCommands && { keyCommand: KEY_COMMANDS.sync },
});
const sectionDelete = [{
const sectionDelete: ComponentProps<typeof MoreMenuItem>[] = [{
label: 'Delete',
icon: <BiTrash
size={15}
className="translate-x-[-1px]"
/>,
className: 'text-error *:hover:text-error',
color: 'red',
action: () => {
if (confirm(deleteConfirmationTextForPhoto(photo))) {
return deletePhotoAction(
@ -109,10 +123,15 @@ export default function AdminPhotoMenu({
});
}
},
...showKeyCommands && {
keyCommandModifier: KEY_COMMANDS.delete[0],
keyCommand: KEY_COMMANDS.delete[1],
},
}];
return [sectionMain, sectionDelete];
}, [
photo,
showKeyCommands,
includeFavorite,
isFav,
shouldRedirectFav,

127
src/app/AppViewSwitcher.tsx Normal file
View File

@ -0,0 +1,127 @@
import Switcher from '@/components/Switcher';
import SwitcherItem from '@/components/SwitcherItem';
import IconFeed from '@/components/icons/IconFeed';
import IconGrid from '@/components/icons/IconGrid';
import {
PATH_FEED_INFERRED,
PATH_GRID_INFERRED,
} from '@/app/paths';
import IconSearch from '../components/icons/IconSearch';
import { useAppState } from '@/state/AppState';
import { GRID_HOMEPAGE_ENABLED } from './config';
import AdminAppMenu from '@/admin/AdminAppMenu';
import Spinner from '@/components/Spinner';
import clsx from 'clsx/lite';
import { useCallback, useRef, useState } from 'react';
import useKeydownHandler from '@/utility/useKeydownHandler';
import { usePathname } from 'next/navigation';
import { KEY_COMMANDS } from '@/photo/key-commands';
export type SwitcherSelection = 'feed' | 'grid' | 'admin';
export default function AppViewSwitcher({
currentSelection,
className,
}: {
currentSelection?: SwitcherSelection
className?: string
}) {
const pathname = usePathname();
const {
isUserSignedIn,
isUserSignedInEager,
setIsCommandKOpen,
} = useAppState();
const refHrefFeed = useRef<HTMLAnchorElement>(null);
const refHrefGrid = useRef<HTMLAnchorElement>(null);
const onKeyDown = useCallback((e: KeyboardEvent) => {
switch (e.key.toLocaleUpperCase()) {
case KEY_COMMANDS.feed:
if (pathname !== PATH_FEED_INFERRED) { refHrefFeed.current?.click(); }
break;
case KEY_COMMANDS.grid:
if (pathname !== PATH_GRID_INFERRED) { refHrefGrid.current?.click(); }
break;
case KEY_COMMANDS.admin:
if (isUserSignedIn) { setIsAdminMenuOpen(true); }
break;
}
}, [pathname, isUserSignedIn]);
useKeydownHandler({ onKeyDown });
const [isAdminMenuOpen, setIsAdminMenuOpen] = useState(false);
const renderItemFeed =
<SwitcherItem
icon={<IconFeed includeTitle={false} />}
href={PATH_FEED_INFERRED}
hrefRef={refHrefFeed}
active={currentSelection === 'feed'}
tooltip={{
content: 'Feed',
keyCommand: KEY_COMMANDS.feed,
}}
noPadding
/>;
const renderItemGrid =
<SwitcherItem
icon={<IconGrid includeTitle={false} />}
href={PATH_GRID_INFERRED}
hrefRef={refHrefGrid}
active={currentSelection === 'grid'}
tooltip={{
content: 'Grid',
keyCommand: KEY_COMMANDS.grid,
}}
noPadding
/>;
return (
<div
className={clsx(
'flex gap-1 sm:gap-2',
className,
)}
>
<Switcher>
{GRID_HOMEPAGE_ENABLED ? renderItemGrid : renderItemFeed}
{GRID_HOMEPAGE_ENABLED ? renderItemFeed : renderItemGrid}
{/* Show spinner if admin is suspected to be logged in */}
{(isUserSignedInEager && !isUserSignedIn) &&
<SwitcherItem
icon={<Spinner />}
isInteractive={false}
noPadding
tooltip={{ content: 'Admin Menu' }}
/>}
{isUserSignedIn &&
<SwitcherItem
icon={<AdminAppMenu
isOpen={isAdminMenuOpen}
setIsOpen={setIsAdminMenuOpen}
/>}
tooltip={{
content: !isAdminMenuOpen ? 'Admin Menu' : undefined,
keyCommand: !isAdminMenuOpen ? KEY_COMMANDS.admin : undefined,
}}
noPadding
/>}
</Switcher>
<Switcher type="borderless">
<SwitcherItem
icon={<IconSearch includeTitle={false} />}
onClick={() => setIsCommandKOpen?.(true)}
tooltip={{
content: 'Search',
keyCommandModifier: KEY_COMMANDS.search[0],
keyCommand: KEY_COMMANDS.search[1],
}}
/>
</Switcher>
</div>
);
}

View File

@ -4,7 +4,7 @@ import { clsx } from 'clsx/lite';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import AppGrid from '../components/AppGrid';
import ViewSwitcher, { SwitcherSelection } from '@/app/ViewSwitcher';
import AppViewSwitcher, { SwitcherSelection } from '@/app/AppViewSwitcher';
import {
PATH_ROOT,
isPathAdmin,
@ -80,7 +80,7 @@ export default function Nav({
'md:w-[calc(100%+8px)] md:translate-x-[-4px] md:px-[4px]',
classNameStickyNav,
)}>
<ViewSwitcher
<AppViewSwitcher
currentSelection={switcherSelectionForPath()}
/>
<div className={clsx(

View File

@ -22,22 +22,22 @@ export default function ThemeSwitcher () {
return (
<Switcher>
<SwitcherItem
title="System"
icon={<BiDesktop size={16} />}
onClick={() => setTheme('system')}
active={theme === 'system'}
tooltip={{ content: 'System' }}
/>
<SwitcherItem
title="Light"
icon={<BiSun size={18} />}
onClick={() => setTheme('light')}
active={theme === 'light'}
tooltip={{ content: 'Light Mode' }}
/>
<SwitcherItem
title="Dark"
icon={<BiMoon size={16} />}
onClick={() => setTheme('dark')}
active={theme === 'dark'}
tooltip={{ content: 'Dark Mode' }}
/>
</Switcher>
);

View File

@ -1,77 +0,0 @@
import Switcher from '@/components/Switcher';
import SwitcherItem from '@/components/SwitcherItem';
import IconFeed from '@/components/icons/IconFeed';
import IconGrid from '@/components/icons/IconGrid';
import {
PATH_FEED_INFERRED,
PATH_GRID_INFERRED,
} from '@/app/paths';
import IconSearch from '../components/icons/IconSearch';
import { useAppState } from '@/state/AppState';
import { GRID_HOMEPAGE_ENABLED } from './config';
import AdminAppMenu from '@/admin/AdminAppMenu';
import Spinner from '@/components/Spinner';
import clsx from 'clsx/lite';
export type SwitcherSelection = 'feed' | 'grid' | 'admin';
export default function ViewSwitcher({
currentSelection,
className,
}: {
currentSelection?: SwitcherSelection
className?: string
}) {
const {
isUserSignedIn,
isUserSignedInEager,
setIsCommandKOpen,
} = useAppState();
const renderItemFeed =
<SwitcherItem
icon={<IconFeed />}
href={PATH_FEED_INFERRED}
active={currentSelection === 'feed'}
noPadding
/>;
const renderItemGrid =
<SwitcherItem
icon={<IconGrid />}
href={PATH_GRID_INFERRED}
active={currentSelection === 'grid'}
noPadding
/>;
return (
<div className={clsx(
'flex gap-1 sm:gap-2',
className,
)}>
<Switcher>
{GRID_HOMEPAGE_ENABLED ? renderItemGrid : renderItemFeed}
{GRID_HOMEPAGE_ENABLED ? renderItemFeed : renderItemGrid}
{/* Show spinner if admin is suspected to be logged in */}
{(isUserSignedInEager && !isUserSignedIn) &&
<SwitcherItem
icon={<Spinner />}
isInteractive={false}
noPadding
/>}
{isUserSignedIn &&
<SwitcherItem
className="p-0!"
icon={<AdminAppMenu />}
noPadding
/>}
</Switcher>
<Switcher type="borderless">
<SwitcherItem
icon={<IconSearch />}
onClick={() => setIsCommandKOpen?.(true)}
/>
</Switcher>
</div>
);
}

View File

@ -61,7 +61,10 @@ export default function Modal({
},
});
useEscapeHandler(onClose, true);
useEscapeHandler({
onKeyDown: onClose,
ignoreShouldRespondToKeyboardCommands: true,
});
return (
<motion.div

View File

@ -1,34 +1,40 @@
import { clsx } from 'clsx/lite';
import { SHOULD_PREFETCH_ALL_LINKS } from '@/app/config';
import { ReactNode } from 'react';
import { ComponentProps, ReactNode, RefObject } from 'react';
import Spinner from './Spinner';
import LinkWithIconLoader from './LinkWithIconLoader';
import Tooltip from './Tooltip';
const WIDTH_CLASS = 'w-[42px]';
export default function SwitcherItem({
icon,
title,
href,
hrefRef,
className: classNameProp,
onClick,
active,
isInteractive = true,
noPadding,
prefetch = SHOULD_PREFETCH_ALL_LINKS,
tooltip,
}: {
icon: ReactNode
title?: string
href?: string
hrefRef?: RefObject<HTMLAnchorElement | null>
className?: string
onClick?: () => void
active?: boolean
isInteractive?: boolean
noPadding?: boolean
prefetch?: boolean
tooltip?: ComponentProps<typeof Tooltip>
}) {
const className = clsx(
'flex items-center justify-center',
'w-[42px] h-full',
'py-0.5 px-1.5',
`${WIDTH_CLASS} h-[28px]`,
isInteractive && 'cursor-pointer',
isInteractive && 'hover:bg-gray-100/60 active:bg-gray-100',
isInteractive && 'dark:hover:bg-gray-900/75 dark:active:bg-gray-900',
@ -50,18 +56,29 @@ export default function SwitcherItem({
{icon}
</div>;
const content = href
? <LinkWithIconLoader {...{
href,
ref: hrefRef,
title,
className,
prefetch,
icon: renderIcon(),
loader: <Spinner />,
}} />
: <div {...{ title, onClick, className }}>
{renderIcon()}
</div>;
return (
href
? <LinkWithIconLoader {...{
title,
href,
className,
prefetch,
icon: renderIcon(),
loader: <Spinner />,
}} />
: <div {...{ title, onClick, className }}>
{renderIcon()}
</div>
tooltip
? <Tooltip
{...tooltip}
classNameTrigger={WIDTH_CLASS}
delayDuration={500}
>
{content}
</Tooltip>
: content
);
};

View File

@ -6,9 +6,11 @@ const INTRINSIC_HEIGHT = 24;
export default function IconFeed({
width = INTRINSIC_WIDTH,
includeTitle = true,
className,
}: {
width?: number
includeTitle?: boolean
className?: string
}) {
return (
<svg
@ -18,6 +20,7 @@ export default function IconFeed({
fill="none"
stroke="currentColor"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
{includeTitle && <title>Full Frame</title>}
<rect x="5.625" y="6.625" width="16.75" height="10.75" rx="1" strokeWidth="1.25"/>

View File

@ -6,9 +6,11 @@ const INTRINSIC_HEIGHT = 24;
export default function IconGrid({
width = INTRINSIC_WIDTH,
includeTitle = true,
className,
}: {
width?: number
includeTitle?: boolean
className?: string
}) {
return (
<svg
@ -18,6 +20,7 @@ export default function IconGrid({
fill="none"
stroke="currentColor"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
{includeTitle && <title>Grid</title>}
<rect x="5.625" y="6.625" width="16.75" height="10.75" rx="1" strokeWidth="1.25"/>

View File

@ -19,7 +19,6 @@ export default function ZoomControls({
selectImageElement?:
(container: HTMLElement | null) => HTMLImageElement | null
isEnabled?: boolean
shouldZoomOnFKeydown?: boolean
}) {
const refImageContainer = useRef<HTMLDivElement>(null);

View File

@ -1,6 +1,5 @@
import useMetaThemeColor from '@/utility/useMetaThemeColor';
import { useAppState } from '@/state/AppState';
import useKeydownHandler from '@/utility/useKeydownHandler';
import {
ComponentProps,
RefObject,
@ -16,7 +15,6 @@ export default function useImageZoomControls({
refImageContainer,
selectImageElement,
isEnabled,
shouldZoomOnFKeydown,
} : {
refImageContainer: RefObject<HTMLElement | null>
} & Omit<ComponentProps<typeof ZoomControls>, 'ref' | 'children'>) {
@ -46,12 +44,6 @@ export default function useImageZoomControls({
viewerRef.current?.reset();
}, []);
// On 'F' keydown, toggle fullscreen
const handleKeyDown = useCallback(() => {
if (shouldZoomOnFKeydown) { open(); }
}, [shouldZoomOnFKeydown, open]);
useKeydownHandler(handleKeyDown, ['F']);
useEffect(() => {
if (isEnabled) {
const imageRef = (

View File

@ -15,12 +15,14 @@ export default function MoreMenu({
icon,
header,
className,
buttonClassName,
buttonClassNameOpen,
classNameButton,
classNameButtonOpen,
ariaLabel,
align = 'end',
// Prevent errant clicks from trigger being too close to menu
sideOffset = 6,
isOpen: isOpenProp,
setIsOpen: setIsOpenProp,
onOpen,
...props
}: {
@ -28,12 +30,17 @@ export default function MoreMenu({
icon?: ReactNode
header?: ReactNode
className?: string
buttonClassName?: string
buttonClassNameOpen?: string
classNameButton?: string
classNameButtonOpen?: string
ariaLabel: string
isOpen?: boolean
setIsOpen?: (isOpen: boolean) => void
onOpen?: () => void
} & ComponentProps<typeof DropdownMenu.Content>){
const [isOpen, setIsOpen] = useState(false);
const [isOpenInternal, setIsOpenInternal] = useState(isOpenProp ?? false);
const isOpen = isOpenProp ?? isOpenInternal;
const setIsOpen = setIsOpenProp ?? setIsOpenInternal;
const dismissMenu = useCallback(() => {
setIsOpen(false);
@ -44,7 +51,10 @@ export default function MoreMenu({
}, [isOpen, onOpen]);
return (
<DropdownMenu.Root open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenu.Root
open={isOpen}
onOpenChange={setIsOpen}
>
<DropdownMenu.Trigger asChild>
<button
className={clsx(
@ -54,8 +64,8 @@ export default function MoreMenu({
'dark:hover:bg-gray-800/75 dark:active:bg-gray-900',
'text-dim',
'outline-none',
buttonClassName,
isOpen && buttonClassNameOpen,
classNameButton,
isOpen && classNameButtonOpen,
)}
aria-label={ariaLabel}
>
@ -65,6 +75,7 @@ export default function MoreMenu({
<DropdownMenu.Portal>
<DropdownMenu.Content
{...props}
onCloseAutoFocus={e => e.preventDefault()}
align={align}
sideOffset={sideOffset}
className={clsx(

View File

@ -2,33 +2,46 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { clsx } from 'clsx/lite';
import { ReactNode, useEffect, useState, useTransition } from 'react';
import {
ComponentProps,
ReactNode,
useEffect,
useState,
useTransition,
} from 'react';
import LoaderButton from '../primitives/LoaderButton';
import { usePathname, useRouter } from 'next/navigation';
import { downloadFileFromBrowser } from '@/utility/url';
import KeyCommand from '../primitives/KeyCommand';
export default function MoreMenuItem({
label,
labelComplex,
annotation,
icon,
color = 'grey',
href,
hrefDownloadName,
className,
action,
dismissMenu,
shouldPreventDefault = true,
keyCommand,
keyCommandModifier,
}: {
label: string
labelComplex?: ReactNode
annotation?: ReactNode
icon?: ReactNode
color?: 'grey' | 'red'
href?: string
hrefDownloadName?: string
className?: string
action?: () => Promise<void | boolean> | void
dismissMenu?: () => void
shouldPreventDefault?: boolean
keyCommand?: string
keyCommandModifier?: ComponentProps<typeof KeyCommand>['modifier']
}) {
const router = useRouter();
@ -44,18 +57,33 @@ export default function MoreMenuItem({
if (transitionDidStart && !isPending) {
dismissMenu?.();
setTransitionDidStart(false);
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}
}, [isPending, dismissMenu, transitionDidStart]);
const getColorClasses = () => {
switch (color) {
case 'grey': return clsx(
'hover:bg-gray-100/90 active:bg-gray-200/75',
'dark:hover:bg-gray-800/60 dark:active:bg-gray-900/80',
);
case 'red': return clsx(
'hover:bg-red-100/50 active:bg-red-100/75',
'dark:hover:bg-red-950/55 dark:active:bg-red-950/80',
);
}
};
return (
<DropdownMenu.Item
disabled={isLoading}
className={clsx(
'flex items-center h-9',
'pl-2 pr-3 py-2 rounded-sm',
'flex items-center h-8.5 gap-4',
'px-2 py-2 rounded-sm',
'select-none hover:outline-hidden',
'hover:bg-gray-100/90 active:bg-gray-200/75',
'dark:hover:bg-gray-800/60 dark:active:bg-gray-900/80',
getColorClasses(),
'whitespace-nowrap',
isLoading
? 'cursor-not-allowed opacity-50'
@ -108,7 +136,8 @@ export default function MoreMenuItem({
isLoading={isLoading || isPending}
hideTextOnMobile={false}
styleAs="link-without-hover"
className="translate-y-[1px]"
className="translate-y-[0.5px] text-sm grow"
classNameIcon="translate-y-[-0.5px]!"
>
<span>
{labelComplex ?? label}
@ -118,6 +147,10 @@ export default function MoreMenuItem({
{annotation}
</span>}
</LoaderButton>
{keyCommand &&
<KeyCommand modifier={keyCommandModifier}>
{keyCommand}
</KeyCommand>}
</DropdownMenu.Item>
);
}

View File

@ -0,0 +1,45 @@
import clsx from 'clsx/lite';
import { useMemo } from 'react';
import { HiMiniBackspace } from 'react-icons/hi2';
import { PiCommandBold } from 'react-icons/pi';
export default function KeyCommand({
children,
modifier,
className,
}: {
children: string
modifier?: '⌘' | '⌥' | '⇧' | '⌃' | '⏎'
className?: string
}) {
const keys = useMemo(() => {
const childrenFormatted = children === 'BACKSPACE'
? '⌫'
: children;
return modifier ? [modifier, ...childrenFormatted] : [...childrenFormatted];
}, [modifier, children]);
return (
<span className={clsx('inline-flex items-center gap-0.5', className)}>
{keys.map((key) => (
<span
key={key}
className={clsx(
'inline-flex items-center justify-center',
'px-1 h-4 rounded-sm text-xs font-medium',
'text-gray-500/90 bg-gray-200/70',
'dark:text-gray-300/90 dark:bg-gray-600/50',
)}
>
{key === '⌘'
? <PiCommandBold />
: key === '⌫'
? <HiMiniBackspace
className="text-[13px] opacity-80"
/>
: key}
</span>
))}
</span>
);
}

View File

@ -13,6 +13,7 @@ import Tooltip from '../Tooltip';
export default function LoaderButton({
ref,
children,
classNameIcon,
isLoading,
icon,
spinnerColor,
@ -32,6 +33,7 @@ export default function LoaderButton({
...rest
}: {
ref?: RefObject<HTMLButtonElement | null>
classNameIcon?: string
isLoading?: boolean
icon?: ReactNode
spinnerColor?: SpinnerColor
@ -79,6 +81,7 @@ export default function LoaderButton({
'min-w-[1.25rem] max-h-5',
styleAs === 'button' ? 'translate-y-[-0.5px]' : 'translate-y-[0.5px]',
'inline-flex justify-center shrink-0',
classNameIcon,
)}>
{isLoading
? <Spinner

View File

@ -6,23 +6,31 @@ import MenuSurface from './MenuSurface';
import useSupportsHover from '@/utility/useSupportsHover';
import clsx from 'clsx/lite';
import useClickInsideOutside from '@/utility/useClickInsideOutside';
import KeyCommand from './KeyCommand';
export default function TooltipPrimitive({
content,
content: contentProp,
children,
className,
classNameTrigger: classNameTriggerProp,
sideOffset = 10,
delayDuration = 100,
skipDelayDuration = 300,
supportMobile,
color,
children,
keyCommand,
keyCommandModifier,
}: {
content?: ReactNode
children: ReactNode
className?: string
classNameTrigger?: string
sideOffset?: number
delayDuration?: number
skipDelayDuration?: number
supportMobile?: boolean
color?: ComponentProps<typeof MenuSurface>['color']
children: ReactNode
keyCommand?: string
keyCommandModifier?: ComponentProps<typeof KeyCommand>['modifier']
}) {
const refTrigger = useRef<HTMLButtonElement>(null);
const refContent = useRef<HTMLDivElement>(null);
@ -41,12 +49,22 @@ export default function TooltipPrimitive({
});
const classNameTrigger = clsx(
'cursor-default inline-block',
'cursor-default inline-flex',
classNameTriggerProp,
);
const content = keyCommand
? <div className="-mr-0.5 whitespace-nowrap">
{contentProp}
{' '}
<KeyCommand {...{ modifier: keyCommandModifier }}>
{keyCommand}
</KeyCommand>
</div>
: contentProp;
return (
<Tooltip.Provider delayDuration={100}>
<Tooltip.Provider {...{ delayDuration, skipDelayDuration }}>
<Tooltip.Root open={includeButton ? isOpen : undefined}>
<Tooltip.Trigger asChild>
{includeButton

View File

@ -0,0 +1,62 @@
import { toastWaiting } from '@/toast';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useRef, useTransition } from 'react';
import { FiCheckSquare } from 'react-icons/fi';
import { toast } from 'sonner';
export default function useNavigateOrRunActionWithToast({
pathOrAction,
toastMessage = 'Loading...',
dismissDelay = 1500,
}: {
pathOrAction?: string | (() => Promise<any> | undefined)
toastMessage?: string
dismissDelay?: number
}) {
const router = useRouter();
const toastId = useRef<string | number>(undefined);
const [isPending, startTransition] = useTransition();
const dismissToast = useCallback(() => {
if (toastId.current) {
toast(toastMessage, {
id: toastId.current,
icon: <FiCheckSquare size={16} />,
});
return setTimeout(() => {
toast.dismiss(toastId.current);
}, dismissDelay);
}
}, [dismissDelay, toastMessage]);
useEffect(() => {
if (!isPending) {
const timeout = dismissToast();
return () => clearTimeout(timeout);
}
return () => {
dismissToast();
};
}, [isPending, dismissDelay, dismissToast]);
const navigateOrRunAction = useCallback(() => {
if (typeof pathOrAction === 'string') {
startTransition(() => {
router.push(pathOrAction);
toastId.current = toastWaiting(toastMessage);
});
} else if (typeof pathOrAction === 'function') {
const result = pathOrAction();
if (result instanceof Promise) {
toastId.current = toastWaiting(toastMessage);
result.finally(() => {
dismissToast();
});
}
}
}, [dismissToast, pathOrAction, router, toastMessage]);
return navigateOrRunAction;
}

View File

@ -139,6 +139,7 @@ export default function PhotoDetailPage({
shouldShareRecipe={recipe !== undefined}
shouldShareFocalLength={focal !== undefined}
includeFavoriteInAdminMenu={includeFavoriteInAdminMenu}
showAdminKeyCommands
/>,
]}
/>

View File

@ -12,11 +12,11 @@ export default function PhotoEscapeHandler() {
const escapePath = getEscapePath(pathname);
const escapeHandler = useCallback(() => {
const onKeyDown = useCallback(() => {
if (escapePath) { router.push(escapePath, { scroll: false }); }
}, [escapePath, router]);
useEscapeHandler(escapeHandler);
useEscapeHandler({ onKeyDown });
return null;
}

View File

@ -12,7 +12,7 @@ import ShareButton from '@/share/ShareButton';
import AnimateItems from '@/components/AnimateItems';
import { ReactNode } from 'react';
import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid';
import PhotoPrevNext from './PhotoPrevNext';
import PhotoPrevNextActions from './PhotoPrevNextActions';
import PhotoLink from './PhotoLink';
import ResponsiveText from '@/components/primitives/ResponsiveText';
import { useAppState } from '@/state/AppState';
@ -59,7 +59,7 @@ export default function PhotoHeader({
: 'photo-detail';
const renderPrevNext =
<PhotoPrevNext {...{
<PhotoPrevNextActions {...{
photo: selectedPhoto,
photos,
...categories,

View File

@ -76,6 +76,7 @@ export default function PhotoLarge({
shouldShareFocalLength,
includeFavoriteInAdminMenu,
onVisible,
showAdminKeyCommands,
}: {
photo: Photo
className?: string
@ -101,6 +102,7 @@ export default function PhotoLarge({
shouldShareFocalLength?: boolean
includeFavoriteInAdminMenu?: boolean
onVisible?: () => void
showAdminKeyCommands?: boolean
}) {
const ref = useRef<HTMLDivElement>(null);
const refZoomControls = useRef<ZoomControlsRef>(null);
@ -252,6 +254,7 @@ export default function PhotoLarge({
revalidatePhoto,
includeFavorite: includeFavoriteInAdminMenu,
ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`,
showKeyCommands: showAdminKeyCommands,
}} />;
const largePhotoContainerClassName = clsx(

View File

@ -1,6 +1,6 @@
'use client';
import { ReactNode, ComponentProps } from 'react';
import { ReactNode, ComponentProps, RefObject } from 'react';
import { Photo, titleForPhoto } from '@/photo';
import { PhotoSetCategory } from '@/category';
import { AnimationConfig } from '../components/AnimateItems';
@ -12,6 +12,7 @@ import Spinner from '@/components/Spinner';
import LinkWithLoaderBackground from '@/components/LinkWithLoaderBackground';
export default function PhotoLink({
ref,
photo,
scroll,
prefetch,
@ -21,6 +22,7 @@ export default function PhotoLink({
loaderType = 'spinner',
...categories
}: {
ref?: RefObject<HTMLAnchorElement | null>
photo?: Photo
scroll?: boolean
prefetch?: boolean
@ -35,6 +37,7 @@ export default function PhotoLink({
Omit<ComponentProps<typeof LinkWithStatus>, 'children'> |
undefined = photo
? {
ref,
className,
href: pathForPhoto({ photo, ...categories }),
onClick: () => {

View File

@ -1,122 +0,0 @@
'use client';
import { useEffect } from 'react';
import {
Photo,
getNextPhoto,
getPreviousPhoto,
} from '@/photo';
import { PhotoSetCategory } from '../category';
import PhotoLink from './PhotoLink';
import { useRouter } from 'next/navigation';
import { pathForPhoto } from '@/app/paths';
import { useAppState } from '@/state/AppState';
import { AnimationConfig } from '@/components/AnimateItems';
import { clsx } from 'clsx/lite';
import { FiChevronLeft, FiChevronRight } from 'react-icons/fi';
const LISTENER_KEYUP = 'keyup';
const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 };
const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 };
export default function PhotoPrevNext({
photo,
photos = [],
className,
...categories
}: {
photo?: Photo
photos?: Photo[]
className?: string
} & PhotoSetCategory) {
const router = useRouter();
const {
setNextPhotoAnimation,
shouldRespondToKeyboardCommands,
} = useAppState();
const previousPhoto = photo ? getPreviousPhoto(photo, photos) : undefined;
const nextPhoto = photo ? getNextPhoto(photo, photos) : undefined;
const pathPrevious = previousPhoto
? pathForPhoto({ photo: previousPhoto, ...categories })
: undefined;
const pathNext = nextPhoto
? pathForPhoto({ photo: nextPhoto, ...categories })
: undefined;
useEffect(() => {
if (shouldRespondToKeyboardCommands) {
const onKeyUp = (e: KeyboardEvent) => {
switch (e.key.toUpperCase()) {
case 'ARROWLEFT':
case 'J':
if (pathPrevious) {
setNextPhotoAnimation?.(ANIMATION_RIGHT);
router.push(pathPrevious, { scroll: false });
}
break;
case 'ARROWRIGHT':
case 'L':
if (pathNext) {
setNextPhotoAnimation?.(ANIMATION_LEFT);
router.push(pathNext, { scroll: false });
}
break;
};
};
window.addEventListener(LISTENER_KEYUP, onKeyUp);
return () => window.removeEventListener(LISTENER_KEYUP, onKeyUp);
}
}, [
router,
shouldRespondToKeyboardCommands,
setNextPhotoAnimation,
pathPrevious,
pathNext,
]);
return (
<div className={clsx(
'flex items-center',
className,
)}>
<div className={clsx(
'h-4',
'flex gap-2 select-none',
// Fixes alignment issue when switching from chevrons to text
'items-center sm:items-start',
'*:select-none',
)}>
<PhotoLink
{...categories}
photo={previousPhoto}
nextPhotoAnimation={ANIMATION_RIGHT}
scroll={false}
loaderType="badge"
prefetch
>
<FiChevronLeft className="sm:hidden text-[1.1rem]" />
<span className="hidden sm:inline-block">PREV</span>
</PhotoLink>
<span className="text-extra-extra-dim">
/
</span>
<PhotoLink
{...categories}
photo={nextPhoto}
nextPhotoAnimation={ANIMATION_LEFT}
scroll={false}
loaderType="badge"
prefetch
>
<FiChevronRight className="sm:hidden text-[1.1rem]" />
<span className="hidden sm:inline-block">NEXT</span>
</PhotoLink>
</div>
</div>
);
};

View File

@ -0,0 +1,226 @@
'use client';
import { useCallback, useRef } from 'react';
import {
Photo,
downloadFileNameForPhoto,
getNextPhoto,
getPreviousPhoto,
} from '@/photo';
import { PhotoSetCategory } from '../category';
import PhotoLink from './PhotoLink';
import { pathForAdminPhotoEdit, pathForPhoto } from '@/app/paths';
import { useAppState } from '@/state/AppState';
import { AnimationConfig } from '@/components/AnimateItems';
import { clsx } from 'clsx/lite';
import { FiChevronLeft, FiChevronRight } from 'react-icons/fi';
import useNavigateOrRunActionWithToast
from '@/components/useNavigateOrRunActionWithToast';
import {
deletePhotoAction,
syncPhotoAction,
toggleFavoritePhotoAction,
} from './actions';
import { isPhotoFav } from '@/tag';
import Tooltip from '@/components/Tooltip';
import { ALLOW_PUBLIC_DOWNLOADS } from '@/app/config';
import { downloadFileFromBrowser } from '@/utility/url';
import useKeydownHandler from '@/utility/useKeydownHandler';
import { KEY_COMMANDS } from './key-commands';
const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 };
const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 };
export default function PhotoPrevNextActions({
photo,
photos = [],
className,
...categories
}: {
photo?: Photo
photos?: Photo[]
className?: string
} & PhotoSetCategory) {
const { setNextPhotoAnimation, isUserSignedIn } = useAppState();
const photoTitle = photo
? photo.title
? `'${photo.title}'`
: 'photo'
: undefined;
const downloadUrl = photo?.url;
const downloadFileName = photo
? downloadFileNameForPhoto(photo)
: undefined;
const toggleFavorite = useCallback(() => {
if (photo?.id) { return toggleFavoritePhotoAction(photo.id); }
}, [photo?.id]);
const navigateToPhotoEdit = useNavigateOrRunActionWithToast({
pathOrAction: photo ? pathForAdminPhotoEdit(photo) : undefined,
toastMessage: `Editing ${photoTitle} ...`,
});
const favoritePhoto = useNavigateOrRunActionWithToast({
pathOrAction: toggleFavorite,
toastMessage: `Favoriting ${photoTitle} ...`,
});
const unfavoritePhoto = useNavigateOrRunActionWithToast({
pathOrAction: toggleFavorite,
toastMessage: `Unfavoriting ${photoTitle} ...`,
});
const syncPhoto = useNavigateOrRunActionWithToast({
pathOrAction: useCallback(() => {
if (photo?.id) { return syncPhotoAction(photo.id); }
}, [photo?.id]),
toastMessage: `Syncing ${photoTitle} ...`,
});
const deletePhoto = useNavigateOrRunActionWithToast({
pathOrAction: useCallback(() => {
if (photo?.id && photo.url) {
return deletePhotoAction(photo.id, photo.url, true);
}
}, [photo?.id, photo?.url]),
toastMessage: `Deleting ${photoTitle} ...`,
});
const refPrevious = useRef<HTMLAnchorElement | null>(null);
const refNext = useRef<HTMLAnchorElement | null>(null);
const previousPhoto = photo ? getPreviousPhoto(photo, photos) : undefined;
const nextPhoto = photo ? getNextPhoto(photo, photos) : undefined;
const pathPrevious = previousPhoto
? pathForPhoto({ photo: previousPhoto, ...categories })
: undefined;
const pathNext = nextPhoto
? pathForPhoto({ photo: nextPhoto, ...categories })
: undefined;
const onKeyDown = useCallback((e: KeyboardEvent) => {
switch (e.key.toUpperCase()) {
// Public commands
case KEY_COMMANDS.prev[0]:
case KEY_COMMANDS.prev[1]:
if (pathPrevious) {
setNextPhotoAnimation?.(ANIMATION_RIGHT);
refPrevious.current?.click();
}
break;
case KEY_COMMANDS.next[0]:
case KEY_COMMANDS.next[1]:
if (pathNext) {
setNextPhotoAnimation?.(ANIMATION_LEFT);
refNext.current?.click();
}
break;
// Admin commands
case KEY_COMMANDS.edit:
if (isUserSignedIn) {
navigateToPhotoEdit();
}
break;
case KEY_COMMANDS.favorite:
if (isUserSignedIn && photo && !isPhotoFav(photo)) {
favoritePhoto();
}
break;
case KEY_COMMANDS.unfavorite:
if (isUserSignedIn && photo && isPhotoFav(photo)) {
unfavoritePhoto();
}
break;
case KEY_COMMANDS.download:
if (
(isUserSignedIn || ALLOW_PUBLIC_DOWNLOADS) &&
downloadUrl &&
downloadFileName
) {
downloadFileFromBrowser(downloadUrl, downloadFileName);
}
break;
case KEY_COMMANDS.sync:
if (isUserSignedIn) {
syncPhoto();
}
break;
case KEY_COMMANDS.delete[1]:
if (e.metaKey && isUserSignedIn) {
deletePhoto();
}
break;
};
}, [
setNextPhotoAnimation,
pathPrevious,
pathNext,
isUserSignedIn,
navigateToPhotoEdit,
photo,
favoritePhoto,
unfavoritePhoto,
downloadUrl,
downloadFileName,
syncPhoto,
deletePhoto,
]);
useKeydownHandler({ onKeyDown });
return (
<div className={clsx(
'flex items-center',
className,
)}>
<div className={clsx(
'h-4',
'flex gap-2 select-none',
// Fixes alignment issue when switching from chevrons to text
'items-center sm:items-start',
'*:select-none',
)}>
<Tooltip
content={previousPhoto ? 'Previous' : undefined}
keyCommand={previousPhoto ? KEY_COMMANDS.prev[0] : undefined}
>
<PhotoLink
{...categories}
ref={refPrevious}
photo={previousPhoto}
nextPhotoAnimation={ANIMATION_RIGHT}
scroll={false}
loaderType="badge"
prefetch
>
<FiChevronLeft className="sm:hidden text-[1.1rem]" />
<span className="hidden sm:inline-block">PREV</span>
</PhotoLink>
</Tooltip>
<span className="text-extra-extra-dim">
/
</span>
<Tooltip
content={nextPhoto ? 'Next' : undefined}
keyCommand={nextPhoto ? KEY_COMMANDS.next[0] : undefined}
>
<PhotoLink
{...categories}
ref={refNext}
photo={nextPhoto}
nextPhotoAnimation={ANIMATION_LEFT}
scroll={false}
loaderType="badge"
prefetch
>
<FiChevronRight className="sm:hidden text-[1.1rem]" />
<span className="hidden sm:inline-block">NEXT</span>
</PhotoLink>
</Tooltip>
</div>
</div>
);
};

View File

@ -44,7 +44,7 @@ import {
extractImageDataFromBlobPath,
propagateRecipeTitleIfNecessary,
} from './server';
import { TAG_FAVS, isTagFavs } from '@/tag';
import { TAG_FAVS, isPhotoFav, isTagFavs } from '@/tag';
import { convertPhotoToPhotoDbInsert, Photo } from '.';
import { runAuthenticatedAdminServerAction } from '@/auth/server';
import { AiImageQuery, getAiImageQuery } from './ai';
@ -254,7 +254,7 @@ export const toggleFavoritePhotoAction = async (
const photo = await getPhoto(photoId);
if (photo) {
const { tags } = photo;
photo.tags = tags.some(tag => tag === TAG_FAVS)
photo.tags = isPhotoFav(photo)
? tags.filter(tag => !isTagFavs(tag))
: [...tags, TAG_FAVS];
await updatePhoto(convertPhotoToPhotoDbInsert(photo));

View File

@ -213,17 +213,18 @@ export const translatePhotoId = (id: string) =>
export const titleForPhoto = (
photo: Photo,
preferDateOverUntitled = true,
useDateAsTitle = true,
fallback = 'Untitled',
) => {
if (photo.title) {
return photo.title;
} else if (preferDateOverUntitled && (photo.takenAt || photo.createdAt)) {
} else if (useDateAsTitle && (photo.takenAt || photo.createdAt)) {
return formatDate({
date: photo.takenAt || photo.createdAt,
length: 'tiny',
}).toLocaleUpperCase();
} else {
return 'Untitled';
return fallback;
}
};

14
src/photo/key-commands.ts Normal file
View File

@ -0,0 +1,14 @@
export const KEY_COMMANDS = {
feed: 'F',
grid: 'G',
admin: 'A',
prev: ['J', 'ARROWLEFT'],
next: ['L', 'ARROWRIGHT'],
edit: 'E',
favorite: 'P',
unfavorite: 'X',
download: 'D',
sync: 'S',
search: ['⌘', 'K'],
delete: ['⌘', 'BACKSPACE'],
} as const;

View File

@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
import { PiWarningBold } from 'react-icons/pi';
import { FiCheckSquare } from 'react-icons/fi';
import { toast } from 'sonner';
import Spinner from '@/components/Spinner';
const DEFAULT_DURATION = 4000;
@ -24,3 +25,13 @@ export const toastWarning = (
duration,
},
);
export const toastWaiting = (
message: ReactNode,
duration = Infinity,
) => toast(
message, {
icon: <Spinner size={16} />,
duration,
},
);

View File

@ -1,12 +1,10 @@
import useKeydownHandler from '@/utility/useKeydownHandler';
export default function useEscapeHandler(
onEscape?: () => void,
ignoreShouldRespondToKeyboardCommands?: boolean,
args: Omit<Parameters<typeof useKeydownHandler>[0], 'keys'>,
) {
useKeydownHandler(
onEscape,
['ESCAPE'],
ignoreShouldRespondToKeyboardCommands,
);
useKeydownHandler({
...args,
keys: ['ESCAPE'],
});
}

View File

@ -3,30 +3,34 @@ import { useCallback, useEffect } from 'react';
const LISTENER_KEYDOWN = 'keydown';
export default function useKeydownHandler(
onKeydown?: (e: KeyboardEvent) => void,
keys: string[] = [],
ignoreShouldRespondToKeyboardCommands?: boolean,
) {
export default function useKeydownHandler({
onKeyDown: onKeyDownArg,
keys,
ignoreShouldRespondToKeyboardCommands = false,
}: {
onKeyDown?: (e: KeyboardEvent) => void
keys?: string[]
ignoreShouldRespondToKeyboardCommands?: boolean
}) {
const { shouldRespondToKeyboardCommands } = useAppState();
const onKeyUp = useCallback((e: KeyboardEvent) => {
if (keys.some(key => key.toUpperCase() === e.key?.toUpperCase())) {
onKeydown?.(e);
const onKeyDown = useCallback((e: KeyboardEvent) => {
if (!keys || keys.some(key => key.toUpperCase() === e.key?.toUpperCase())) {
onKeyDownArg?.(e);
}
}, [onKeydown, keys]);
}, [onKeyDownArg, keys]);
useEffect(() => {
if (
shouldRespondToKeyboardCommands ||
ignoreShouldRespondToKeyboardCommands
) {
window.addEventListener(LISTENER_KEYDOWN, onKeyUp);
return () => window.removeEventListener(LISTENER_KEYDOWN, onKeyUp);
window.addEventListener(LISTENER_KEYDOWN, onKeyDown);
return () => window.removeEventListener(LISTENER_KEYDOWN, onKeyDown);
}
}, [
shouldRespondToKeyboardCommands,
ignoreShouldRespondToKeyboardCommands,
onKeyUp,
onKeyDown,
]);
}