diff --git a/astro.config.js b/astro.config.js index c927219f..c3eee30b 100644 --- a/astro.config.js +++ b/astro.config.js @@ -37,7 +37,13 @@ export default defineConfig({ [ rehypeExternalLinks, { - content: { type: 'text', value: '🔗' }, + content: { + type: 'element', + tagName: 'span', + properties: { + className: 'icon-[tabler--external-link]', + }, + }, target: '_blank', }, ], @@ -48,7 +54,7 @@ export default defineConfig({ mdx(), sitemap(), tailwind({ - // applyBaseStyles: false, + applyBaseStyles: false, }), react(), sentry({ diff --git a/components.json b/components.json index 1d57200e..b005180f 100644 --- a/components.json +++ b/components.json @@ -1,11 +1,11 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", + "style": "new-york", "rsc": false, "tsx": true, "tailwind": { "config": "tailwind.config.js", - "css": "styles/style.css", + "css": "src/styles/style.css", "baseColor": "zinc", "cssVariables": false, "prefix": "" diff --git a/package.json b/package.json index 3546e2f9..229218f2 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,10 @@ "@astrojs/tailwind": "^5.1.0", "@astrojs/vercel": "^7.5.4", "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-menubar": "^1.0.4", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", "@sentry/astro": "^7.113.0", "@shikijs/transformers": "^1.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 196b4563..6994538c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,9 +29,18 @@ importers: '@radix-ui/react-avatar': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-icons': + specifier: ^1.3.0 + version: 1.3.0(react@18.3.1) + '@radix-ui/react-menubar': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-toast': + specifier: ^1.1.5 + version: 1.1.5(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tooltip': specifier: ^1.0.7 version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -751,6 +760,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collection@1.0.3': + resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.0.1': resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: @@ -769,6 +791,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-direction@1.0.1': + resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dismissable-layer@1.0.5': resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} peerDependencies: @@ -782,6 +813,33 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-focus-guards@1.0.1': + resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.0.4': + resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-icons@1.3.0': + resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==} + peerDependencies: + react: ^16.x || ^17.x || ^18.x + '@radix-ui/react-id@1.0.1': resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==} peerDependencies: @@ -791,6 +849,32 @@ packages: '@types/react': optional: true + '@radix-ui/react-menu@2.0.6': + resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.0.4': + resolution: {integrity: sha512-bHgUo9gayKZfaQcWSSLr++LyS0rgh+MvD89DE4fJ6TkGHvjHgPaBZf44hdka7ogOxIOdj9163J+5xL2Dn4qzzg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popper@1.1.3': resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==} peerDependencies: @@ -843,6 +927,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.0.4': + resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.0.2': resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -852,6 +949,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-toast@1.1.5': + resolution: {integrity: sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tooltip@1.0.7': resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==} peerDependencies: @@ -1312,6 +1422,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.4: + resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + engines: {node: '>=10'} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -1538,6 +1652,9 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + deterministic-object-hash@2.0.2: resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==} engines: {node: '>=18'} @@ -1746,6 +1863,10 @@ packages: resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} engines: {node: '>=18'} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -1875,6 +1996,9 @@ packages: inline-style-parser@0.2.3: resolution: {integrity: sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + is-absolute-url@4.0.1: resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2621,6 +2745,36 @@ packages: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} + react-remove-scroll-bar@2.3.6: + resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.5.5: + resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.1: + resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -3014,6 +3168,26 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + use-callback-ref@1.3.2: + resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.2: + resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + use-sync-external-store@1.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: @@ -3800,6 +3974,19 @@ snapshots: '@types/react': 18.3.1 '@types/react-dom': 18.3.0 + '@radix-ui/react-collection@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.5 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.1 + '@types/react-dom': 18.3.0 + '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.1)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -3814,6 +4001,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 + '@radix-ui/react-direction@1.0.1(@types/react@18.3.1)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.5 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.1 + '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -3828,6 +4022,29 @@ snapshots: '@types/react': 18.3.1 '@types/react-dom': 18.3.0 + '@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.1)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.5 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.1 + + '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.5 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.1 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-icons@1.3.0(react@18.3.1)': + dependencies: + react: 18.3.1 + '@radix-ui/react-id@1.0.1(@types/react@18.3.1)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -3836,6 +4053,52 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 + '@radix-ui/react-menu@2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.5 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-direction': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.1)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.5(@types/react@18.3.1)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.1 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-menubar@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.5 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-direction': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-id': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.1 + '@types/react-dom': 18.3.0 + '@radix-ui/react-popper@1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -3886,6 +4149,24 @@ snapshots: '@types/react': 18.3.1 '@types/react-dom': 18.3.0 + '@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.5 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-direction': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-id': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.1 + '@types/react-dom': 18.3.0 + '@radix-ui/react-slot@1.0.2(@types/react@18.3.1)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -3894,6 +4175,27 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 + '@radix-ui/react-toast@1.1.5(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.5 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.1 + '@types/react-dom': 18.3.0 + '@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.5 @@ -4371,6 +4673,10 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.4: + dependencies: + tslib: 2.6.2 + aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -4647,6 +4953,8 @@ snapshots: detect-libc@2.0.3: {} + detect-node-es@1.1.0: {} + deterministic-object-hash@2.0.2: dependencies: base-64: 1.0.0 @@ -4870,6 +5178,8 @@ snapshots: get-east-asian-width@1.2.0: {} + get-nonce@1.0.1: {} + get-stream@8.0.1: {} github-slugger@2.0.0: {} @@ -5099,6 +5409,10 @@ snapshots: inline-style-parser@0.2.3: {} + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + is-absolute-url@4.0.1: {} is-alphabetical@2.0.1: {} @@ -6045,6 +6359,34 @@ snapshots: react-refresh@0.14.2: {} + react-remove-scroll-bar@2.3.6(@types/react@18.3.1)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.1(@types/react@18.3.1)(react@18.3.1) + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.1 + + react-remove-scroll@2.5.5(@types/react@18.3.1)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.1)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.1)(react@18.3.1) + tslib: 2.6.2 + use-callback-ref: 1.3.2(@types/react@18.3.1)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.1)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.1 + + react-style-singleton@2.2.1(@types/react@18.3.1)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + invariant: 2.2.4 + react: 18.3.1 + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.1 + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -6612,6 +6954,21 @@ snapshots: escalade: 3.1.2 picocolors: 1.0.0 + use-callback-ref@1.3.2(@types/react@18.3.1)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.1 + + use-sidecar@1.1.2(@types/react@18.3.1)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.1 + use-sync-external-store@1.2.0(react@18.3.1): dependencies: react: 18.3.1 diff --git a/src/components/post/PostActions.tsx b/src/components/post/PostActions.tsx new file mode 100644 index 00000000..cec4170d --- /dev/null +++ b/src/components/post/PostActions.tsx @@ -0,0 +1,214 @@ +import { + Menubar, + MenubarContent, + MenubarItem, + MenubarMenu, + MenubarShortcut, + MenubarTrigger, +} from '@/components/ui/menubar'; +import { Toaster } from '@/components/ui/toaster'; +import { useToast } from '@/components/ui/use-toast'; +import { copyTextToClipboard, getHref, throttle } from '@/lib/helper'; +import { usePostDetail } from '@/store/postDetail'; +import type { MarkdownHeading } from 'astro'; +import { useEffect, useState } from 'react'; + +type Props = { + headings: MarkdownHeading[]; +}; + +export function PostActions({ headings }: Props) { + return ( + <> + +
+ + + + + + + + + + + +
+ + ); +} + +function MenuSearch() { + const { toast } = useToast(); + const handleSelect = (description: string) => toast({ title: 'TODO', description }); + return ( + + + + + + handleSelect('别着急,在写了…')}> + + 搜索“人工智能” + + handleSelect('疯狂写代码中…')}> + + 搜索“JavaScript” + + handleSelect('坐和放宽…')}> + + 碰碰运气 + + + + ); +} + +function MenuLike() { + const { toast } = useToast(); + const { like } = usePostDetail(); + const handleClick = (description?: string) => { + like(getHref()).then(() => toast({ title: '谢谢', description })); + }; + return ( + + like(getHref())}> + + + + handleClick('感谢老铁的点赞')}> + + 点赞 + + handleClick('你很喜欢,我很开心')}> + + 喜欢 + + handleClick('就写、就写')}> + + 写的很好,不许再写了 + + + + ); +} + +function MenuOutline({ headings }: Props) { + const displayHeadings = headings.filter((heading) => heading.depth <= 3); + return ( + + + + + + {displayHeadings.map((heading) => { + return ( + (window.location.hash = `#${heading.slug}`)}> + {heading.depth === 2 && ( + <> + + {heading.text} + + )} + {heading.depth === 3 && ( + <> + + + {heading.text} + + + )} + + ); + })} + + + ); +} + +function MenuShare() { + const { toast } = useToast(); + const href = getHref(); + const handleShareTwitter = () => + window.open(`https://twitter.com/intent/tweet?url=${href}`, '_blank', 'noopener,noreferrer'); + const handleCopyLink = () => { + copyTextToClipboard(href).then(() => { + toast({ + description: 'Copied', + }); + }); + }; + const handlePrint = () => window.print(); + + useEffect(() => { + document.addEventListener('keydown', (e) => { + if (e.metaKey && e.shiftKey && e.key === 'c') { + handleCopyLink(); + } + }); + }, []); + return ( + + + + + + + + Twitter + + + + 复制链接 + ⌘+⇧+C + + + + 打印 + ⌘+P + + + + ); +} + +type Theme = 'light' | 'dark' | 'system'; +function MenuThemeToggle() { + const [theme, setThemeState] = useState('light'); + + useEffect(() => { + const isDarkMode = document.documentElement.classList.contains('dark'); + setThemeState(isDarkMode ? 'dark' : 'light'); + }, []); + + useEffect(() => { + const isDark = + theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); + document.documentElement.classList[isDark ? 'add' : 'remove']('dark'); + }, [theme]); + + function handleThemeChange(theme: Theme) { + setThemeState(theme); + } + return ( + + + + + + handleThemeChange('light')}> + + Light + + handleThemeChange('dark')}> + + Dark + + handleThemeChange('system')}> + + System + + + + ); +} diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index 2fa1726d..b12dc474 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; const alertVariants = cva( - 'relative w-full rounded-lg border border-zinc-200 p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-zinc-950 dark:border-zinc-800 dark:[&>svg]:text-zinc-50', + 'relative w-full rounded-lg border border-zinc-200 px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-zinc-950 [&>svg~*]:pl-7 dark:border-zinc-800 dark:[&>svg]:text-zinc-50', { variants: { variant: { diff --git a/src/components/ui/menubar.tsx b/src/components/ui/menubar.tsx new file mode 100644 index 00000000..ed35ab86 --- /dev/null +++ b/src/components/ui/menubar.tsx @@ -0,0 +1,216 @@ +import { CheckIcon, ChevronRightIcon, DotFilledIcon } from '@radix-ui/react-icons'; +import * as MenubarPrimitive from '@radix-ui/react-menubar'; +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +const MenubarMenu = MenubarPrimitive.Menu; + +const MenubarGroup = MenubarPrimitive.Group; + +const MenubarPortal = MenubarPrimitive.Portal; + +const MenubarSub = MenubarPrimitive.Sub; + +const MenubarRadioGroup = MenubarPrimitive.RadioGroup; + +const Menubar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Menubar.displayName = MenubarPrimitive.Root.displayName; + +const MenubarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName; + +const MenubarSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName; + +const MenubarSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName; + +const MenubarContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = 'start', alignOffset = -4, sideOffset = 8, ...props }, ref) => ( + + + +)); +MenubarContent.displayName = MenubarPrimitive.Content.displayName; + +const MenubarItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +MenubarItem.displayName = MenubarPrimitive.Item.displayName; + +const MenubarCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName; + +const MenubarRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName; + +const MenubarLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +MenubarLabel.displayName = MenubarPrimitive.Label.displayName; + +const MenubarSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName; + +const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return ( + + ); +}; +MenubarShortcut.displayname = 'MenubarShortcut'; + +export { + Menubar, + MenubarMenu, + MenubarTrigger, + MenubarContent, + MenubarItem, + MenubarSeparator, + MenubarLabel, + MenubarCheckboxItem, + MenubarRadioGroup, + MenubarRadioItem, + MenubarPortal, + MenubarSubContent, + MenubarSubTrigger, + MenubarGroup, + MenubarSub, + MenubarShortcut, +}; diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx index 84c27cec..fc256d42 100644 --- a/src/components/ui/skeleton.tsx +++ b/src/components/ui/skeleton.tsx @@ -1,7 +1,7 @@ import { cn } from '@/lib/utils'; function Skeleton({ className, ...props }: React.HTMLAttributes) { - return
; + return
; } export { Skeleton }; diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx new file mode 100644 index 00000000..1914d899 --- /dev/null +++ b/src/components/ui/toast.tsx @@ -0,0 +1,112 @@ +import { Cross2Icon } from '@radix-ui/react-icons'; +import * as ToastPrimitives from '@radix-ui/react-toast'; +import { type VariantProps, cva } from 'class-variance-authority'; +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +const ToastProvider = ToastPrimitives.Provider; + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastViewport.displayName = ToastPrimitives.Viewport.displayName; + +const toastVariants = cva( + 'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border border-zinc-200 p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full dark:border-zinc-800', + { + variants: { + variant: { + default: 'border bg-white text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50', + destructive: + 'destructive group border-red-500 bg-red-500 text-zinc-50 dark:border-red-900 dark:bg-red-900 dark:text-zinc-50', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & VariantProps +>(({ className, variant, ...props }, ref) => { + return ; +}); +Toast.displayName = ToastPrimitives.Root.displayName; + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastAction.displayName = ToastPrimitives.Action.displayName; + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +ToastClose.displayName = ToastPrimitives.Close.displayName; + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastTitle.displayName = ToastPrimitives.Title.displayName; + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastDescription.displayName = ToastPrimitives.Description.displayName; + +type ToastProps = React.ComponentPropsWithoutRef; + +type ToastActionElement = React.ReactElement; + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +}; diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx new file mode 100644 index 00000000..13ce23bb --- /dev/null +++ b/src/components/ui/toaster.tsx @@ -0,0 +1,24 @@ +import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from '@/components/ui/toast'; +import { useToast } from '@/components/ui/use-toast'; + +export function Toaster() { + const { toasts } = useToast(); + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && {description}} +
+ {action} + +
+ ); + })} + +
+ ); +} diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx index 3eccf7b2..d64289c1 100644 --- a/src/components/ui/tooltip.tsx +++ b/src/components/ui/tooltip.tsx @@ -17,7 +17,7 @@ const TooltipContent = React.forwardRef< ref={ref} sideOffset={sideOffset} className={cn( - 'z-50 overflow-hidden rounded-md border border-zinc-200 bg-white px-3 py-1.5 text-sm text-zinc-950 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50', + 'z-50 overflow-hidden rounded-md bg-zinc-900 px-3 py-1.5 text-xs text-zinc-50 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:bg-zinc-50 dark:text-zinc-900', className, )} {...props} diff --git a/src/components/ui/use-toast.ts b/src/components/ui/use-toast.ts new file mode 100644 index 00000000..79c57b4c --- /dev/null +++ b/src/components/ui/use-toast.ts @@ -0,0 +1,187 @@ +// Inspired by react-hot-toast library +import * as React from 'react'; + +import type { ToastActionElement, ToastProps } from '@/components/ui/toast'; + +const TOAST_LIMIT = 1; +const TOAST_REMOVE_DELAY = 1000000; + +type ToasterToast = ToastProps & { + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + action?: ToastActionElement; +}; + +const actionTypes = { + ADD_TOAST: 'ADD_TOAST', + UPDATE_TOAST: 'UPDATE_TOAST', + DISMISS_TOAST: 'DISMISS_TOAST', + REMOVE_TOAST: 'REMOVE_TOAST', +} as const; + +let count = 0; + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER; + return count.toString(); +} + +type ActionType = typeof actionTypes; + +type Action = + | { + type: ActionType['ADD_TOAST']; + toast: ToasterToast; + } + | { + type: ActionType['UPDATE_TOAST']; + toast: Partial; + } + | { + type: ActionType['DISMISS_TOAST']; + toastId?: ToasterToast['id']; + } + | { + type: ActionType['REMOVE_TOAST']; + toastId?: ToasterToast['id']; + }; + +interface State { + toasts: ToasterToast[]; +} + +const toastTimeouts = new Map>(); + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return; + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId); + dispatch({ + type: 'REMOVE_TOAST', + toastId: toastId, + }); + }, TOAST_REMOVE_DELAY); + + toastTimeouts.set(toastId, timeout); +}; + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case 'ADD_TOAST': + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + }; + + case 'UPDATE_TOAST': + return { + ...state, + toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)), + }; + + case 'DISMISS_TOAST': { + const { toastId } = action; + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId); + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id); + }); + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t, + ), + }; + } + case 'REMOVE_TOAST': + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + }; + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + }; + } +}; + +const listeners: Array<(state: State) => void> = []; + +let memoryState: State = { toasts: [] }; + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action); + listeners.forEach((listener) => { + listener(memoryState); + }); +} + +type Toast = Omit; + +function toast({ ...props }: Toast) { + const id = genId(); + + const update = (props: ToasterToast) => + dispatch({ + type: 'UPDATE_TOAST', + toast: { ...props, id }, + }); + const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id }); + + dispatch({ + type: 'ADD_TOAST', + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss(); + }, + }, + }); + + return { + id: id, + dismiss, + update, + }; +} + +function useToast() { + const [state, setState] = React.useState(memoryState); + + React.useEffect(() => { + listeners.push(setState); + return () => { + const index = listeners.indexOf(setState); + if (index > -1) { + listeners.splice(index, 1); + } + }; + }, [state]); + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }), + }; +} + +export { useToast, toast }; diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 184b7848..11c684b5 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -1,4 +1,7 @@ +import type { MarkdownHeading } from 'astro'; + export const getHref = () => { + if (typeof window === 'undefined') return ''; return window.location.href.replace('http://dev.', 'https://').replace(/\/$/, ''); }; @@ -12,3 +15,33 @@ export function throttle(func: any, timeFrame: number) { } }; } + +export function copyTextToClipboard(text: string): Promise { + return new Promise((resolve) => { + if (!navigator.clipboard) { + resolve(fallbackCopyTextToClipboard(text)); + } + resolve(navigator.clipboard.writeText(text)); + }); +} + +function fallbackCopyTextToClipboard(text) { + const textArea = document.createElement('textarea'); + textArea.value = text; + + textArea.style.top = '0'; + textArea.style.left = '0'; + textArea.style.position = 'fixed'; + + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand('copy'); + } catch (err) { + console.error('unable to copy', err); + } + + document.body.removeChild(textArea); +} diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro index 80b9c2e9..576b9ff2 100644 --- a/src/pages/blog/[...slug].astro +++ b/src/pages/blog/[...slug].astro @@ -1,6 +1,7 @@ --- import { type CollectionEntry, getCollection } from "astro:content"; import FloatBtn from "@/components/fab/FloatActionBtn.astro"; +import { PostActions } from "@/components/post/PostActions"; import { PostSummary } from "@/components/post/PostSummary"; import BaseLayout from "@/layouts/BaseLayout.astro"; @@ -42,6 +43,8 @@ const { title, tldr, date, tags = [], cover } = post.data; 国际进行许可。 + + - - diff --git a/src/store/postDetail.ts b/src/store/postDetail.ts index b1e3d172..247889d6 100644 --- a/src/store/postDetail.ts +++ b/src/store/postDetail.ts @@ -6,7 +6,7 @@ export interface PostDetail { viewCount: number; likeCount: number; loading: boolean; - like: (url: string) => void; + like: (url: string) => Promise; fetch: (param: Post) => Promise; } diff --git a/tailwind.config.js b/tailwind.config.js index a8bd7bda..e766882d 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -31,5 +31,5 @@ module.exports = { }, }, plugins: [require('tailwindcss-animate'), addDynamicIconSelectors(), require('@tailwindcss/typography')], - safelist: ['icon-[tabler--mail]', 'icon-[tabler--home]'], + safelist: ['icon-[tabler--mail]', 'icon-[tabler--home]', 'icon-[tabler--external-link]'], };