@intlayer/design-system 9.4.0-canary.0 → 9.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/esm/api/index.mjs +2 -2
- package/dist/esm/api/useAuth/useOAuth2.mjs.map +1 -1
- package/dist/esm/api/useIntlayerAPI.mjs +1 -3
- package/dist/esm/api/useIntlayerAPI.mjs.map +1 -1
- package/dist/esm/api/useUser/index.mjs +1 -1
- package/dist/esm/components/Carousel/index.mjs +48 -30
- package/dist/esm/components/Carousel/index.mjs.map +1 -1
- package/dist/esm/components/Navbar/MobileNavbar.mjs +24 -2
- package/dist/esm/components/Navbar/MobileNavbar.mjs.map +1 -1
- package/dist/esm/components/Pattern/GridDistortionPattern.mjs +1 -1
- package/dist/esm/components/RightDrawer/RightDrawer.mjs +29 -6
- package/dist/esm/components/RightDrawer/RightDrawer.mjs.map +1 -1
- package/dist/esm/components/RightDrawer/index.mjs +2 -2
- package/dist/esm/components/RightDrawer/useRightDrawer.mjs +37 -5
- package/dist/esm/components/RightDrawer/useRightDrawer.mjs.map +1 -1
- package/dist/esm/components/Tab/Tab.mjs +20 -3
- package/dist/esm/components/Tab/Tab.mjs.map +1 -1
- package/dist/esm/components/TechLogo/TechLogo.mjs +1 -0
- package/dist/esm/components/TechLogo/TechLogo.mjs.map +1 -1
- package/dist/esm/components/TechLogo/logos/Elysia.mjs +62 -0
- package/dist/esm/components/TechLogo/logos/Elysia.mjs.map +1 -0
- package/dist/esm/components/TechLogo/techLogos.mjs +3 -1
- package/dist/esm/components/TechLogo/techLogos.mjs.map +1 -1
- package/dist/esm/components/index.mjs +2 -2
- package/dist/esm/hooks/useScrollBlockage/index.mjs +9 -10
- package/dist/esm/hooks/useScrollBlockage/index.mjs.map +1 -1
- package/dist/esm/hooks/useScrollDetection.mjs +15 -2
- package/dist/esm/hooks/useScrollDetection.mjs.map +1 -1
- package/dist/esm/hooks/useScrollY.mjs +1 -1
- package/dist/esm/routes.mjs +3 -1
- package/dist/esm/routes.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +1 -1
- package/dist/types/api/useIntlayerAPI.d.ts +12 -0
- package/dist/types/api/useIntlayerAPI.d.ts.map +1 -1
- package/dist/types/components/Carousel/index.d.ts.map +1 -1
- package/dist/types/components/Navbar/MobileNavbar.d.ts.map +1 -1
- package/dist/types/components/RightDrawer/RightDrawer.d.ts.map +1 -1
- package/dist/types/components/RightDrawer/index.d.ts +2 -2
- package/dist/types/components/RightDrawer/useRightDrawer.d.ts +34 -1
- package/dist/types/components/RightDrawer/useRightDrawer.d.ts.map +1 -1
- package/dist/types/components/Tab/Tab.d.ts.map +1 -1
- package/dist/types/components/TechLogo/TechLogo.d.ts.map +1 -1
- package/dist/types/components/TechLogo/logos/Elysia.d.ts +6 -0
- package/dist/types/components/TechLogo/logos/Elysia.d.ts.map +1 -0
- package/dist/types/components/TechLogo/techLogos.d.ts +3 -2
- package/dist/types/components/TechLogo/types.d.ts +1 -1
- package/dist/types/components/index.d.ts +2 -2
- package/dist/types/hooks/useScrollBlockage/index.d.ts.map +1 -1
- package/dist/types/hooks/useScrollDetection.d.ts.map +1 -1
- package/dist/types/routes.d.ts +3 -1
- package/dist/types/routes.d.ts.map +1 -1
- package/package.json +13 -13
package/README.md
CHANGED
|
@@ -194,8 +194,8 @@ Explore our comprehensive documentation to get started with Intlayer and learn h
|
|
|
194
194
|
<li><a href="https://intlayer.org/doc/environment/vite-and-vue" rel=''>Vite + Vue</a></li>
|
|
195
195
|
<li><a href="https://intlayer.org/doc/environment/nuxt-and-vue" rel=''>Vite + Nuxt</a></li>
|
|
196
196
|
<li><a href="https://intlayer.org/doc/environment/vite-and-solid" rel=''>Vite + Solid</a></li>
|
|
197
|
-
<li><a href="https://intlayer.org/doc/environment/
|
|
198
|
-
<li><a href="https://intlayer.org/doc/environment/express" rel=''>Express</a></li>
|
|
197
|
+
<li><a href="https://intlayer.org/doc/environment/solid-start" rel=''>Solid Start</a></li>
|
|
198
|
+
<li><a href="https://intlayer.org/doc/environment/angular" rel=''>Angular</a></li> <li><a href="https://intlayer.org/doc/environment/express" rel=''>Express</a></li>
|
|
199
199
|
<li><a href="https://intlayer.org/doc/environment/nest" rel=''>NestJS</a></li>
|
|
200
200
|
</ul>
|
|
201
201
|
</details>
|
package/dist/esm/api/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { useAiAPI, useAnalyticsAPI, useAssetAPI, useAuditAPI, useBitbucketAPI, useDictionaryAPI, useEditorAPI, useEnvironmentAPI, useGithubAPI, useGitlabAPI, useIntlayerAuth, useIntlayerOAuth, useIntlayerOAuthOptions, useNewsletterAPI, useOAuthAPI, useOrganizationAPI, useProjectAPI, useReviewerAPI, useSearchAPI, useShowcaseProjectAPI, useStripeAPI, useTagAPI, useTranslateAPI, useUserAPI } from "./useIntlayerAPI.mjs";
|
|
2
|
+
import { useAddPasskey, useAskResetPassword, useChangePassword, useDeletePasskey, useDeleteSSOProvider, useDisableTwoFactor, useEnableTwoFactor, useGetUserByAccount, useGetVerifyEmailStatus, useLinkSocial, useListAccounts, useListPasskeys, useListSSOProviders, useLogin, useLogout, useRegister, useRegisterSSO, useResetPassword, useSignInMagicLink, useSignInPasskey, useSignInSSO, useUnlinkAccount, useVerifyBackupCode, useVerifyEmail, useVerifyTotp } from "./hooks/auth.mjs";
|
|
1
3
|
import { useOAuth2 } from "./useAuth/useOAuth2.mjs";
|
|
2
4
|
import { useSession } from "./useAuth/useSession.mjs";
|
|
3
5
|
import { useAuth } from "./useAuth/useAuth.mjs";
|
|
4
|
-
import { useAiAPI, useAnalyticsAPI, useAssetAPI, useAuditAPI, useBitbucketAPI, useDictionaryAPI, useEditorAPI, useEnvironmentAPI, useGithubAPI, useGitlabAPI, useIntlayerAuth, useIntlayerOAuth, useIntlayerOAuthOptions, useNewsletterAPI, useOAuthAPI, useOrganizationAPI, useProjectAPI, useReviewerAPI, useSearchAPI, useShowcaseProjectAPI, useStripeAPI, useTagAPI, useTranslateAPI, useUserAPI } from "./useIntlayerAPI.mjs";
|
|
5
|
-
import { useAddPasskey, useAskResetPassword, useChangePassword, useDeletePasskey, useDeleteSSOProvider, useDisableTwoFactor, useEnableTwoFactor, useGetUserByAccount, useGetVerifyEmailStatus, useLinkSocial, useListAccounts, useListPasskeys, useListSSOProviders, useLogin, useLogout, useRegister, useRegisterSSO, useResetPassword, useSignInMagicLink, useSignInPasskey, useSignInSSO, useUnlinkAccount, useVerifyBackupCode, useVerifyEmail, useVerifyTotp } from "./hooks/auth.mjs";
|
|
6
6
|
import { useUser } from "./useUser/index.mjs";
|
|
7
7
|
import { useAskDocQuestion, useAuditContentDeclaration, useAuditContentDeclarationField, useAuditContentDeclarationMetadata, useAuditTag, useAutocomplete, useChat, useCustomQuery, useGetAIStats, useTranslateJSONDeclaration } from "./hooks/ai.mjs";
|
|
8
8
|
import { useGetAnalyticsAudience, useGetContentStats, useGetExperimentResults } from "./hooks/analytics.mjs";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useOAuth2.mjs","names":[],"sources":["../../../../src/api/useAuth/useOAuth2.ts"],"sourcesContent":["'use client';\n\nimport { getOAuthAPI } from '@intlayer/api';\nimport type { OAuth2Token } from '@intlayer/backend';\nimport { editor } from '@intlayer/config/built';\nimport { useConfiguration } from '@intlayer/editor-react';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { useQuery } from '@tanstack/react-query';\nimport { defu } from 'defu';\nimport { useIsAuthFetchEnabled } from '../../providers/DeferredAuthProvider';\n\nexport type UseOAuth2Result = {\n /** The OAuth2 token: `undefined` while unfetched, `null` when none was issued. */\n oAuth2AccessToken: OAuth2Token | null | undefined;\n};\n\n/**\n * Fetches the OAuth2 client-credentials token for the configured editor client.\n *\n * The query stays disabled when no client credentials are configured, and — under\n * a `DeferredAuthProvider` — until the page has finished loading.\n */\nexport const useOAuth2 = (\n intlayerConfiguration?: Pick<IntlayerConfig, 'editor'>\n): UseOAuth2Result => {\n const configuration = useConfiguration();\n const config = defu(intlayerConfiguration, configuration, {\n editor,\n }) as IntlayerConfig;\n\n const intlayerAPI = getOAuthAPI(undefined, config);\n const isAuthFetchEnabled = useIsAuthFetchEnabled();\n\n const { data } = useQuery({\n queryKey: ['oAuth2AccessToken'],\n queryFn: intlayerAPI.getOAuth2AccessToken,\n // Held back until after page load when a `DeferredAuthProvider` is mounted\n // (content sites); unconditional everywhere else (the CMS dashboard).\n enabled:\n !!(config.editor.clientId && config.editor.clientSecret) &&\n isAuthFetchEnabled,\n staleTime: 0,\n gcTime: 0,\n refetchOnWindowFocus: false,\n refetchOnMount: false,\n refetchOnReconnect: false,\n refetchInterval: false,\n refetchIntervalInBackground: false,\n });\n\n const oAuth2AccessToken = data?.data;\n\n return {\n oAuth2AccessToken,\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAsBA,MAAa,aACX,0BACoB;CACpB,MAAM,gBAAgB,iBAAiB;CACvC,MAAM,SAAS,KAAK,uBAAuB,eAAe,EACxD,OACF,CAAC;CAED,MAAM,cAAc,YAAY,QAAW,MAAM;CACjD,MAAM,qBAAqB,sBAAsB;CAEjD,MAAM,EAAE,SAAS,SAAS;EACxB,UAAU,CAAC,mBAAmB;EAC9B,SAAS,YAAY;
|
|
1
|
+
{"version":3,"file":"useOAuth2.mjs","names":[],"sources":["../../../../src/api/useAuth/useOAuth2.ts"],"sourcesContent":["'use client';\n\nimport { getOAuthAPI } from '@intlayer/api';\nimport type { OAuth2Token } from '@intlayer/backend';\nimport { editor } from '@intlayer/config/built';\nimport { useConfiguration } from '@intlayer/editor-react';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { useQuery } from '@tanstack/react-query';\nimport { defu } from 'defu';\nimport { useIsAuthFetchEnabled } from '../../providers/DeferredAuthProvider';\n\nexport type UseOAuth2Result = {\n /** The OAuth2 token: `undefined` while unfetched, `null` when none was issued. */\n oAuth2AccessToken: OAuth2Token | null | undefined;\n};\n\n/**\n * Fetches the OAuth2 client-credentials token for the configured editor client.\n *\n * The query stays disabled when no client credentials are configured, and — under\n * a `DeferredAuthProvider` — until the page has finished loading.\n */\nexport const useOAuth2 = (\n intlayerConfiguration?: Pick<IntlayerConfig, 'editor'>\n): UseOAuth2Result => {\n const configuration = useConfiguration();\n const config = defu(intlayerConfiguration, configuration, {\n editor,\n }) as IntlayerConfig;\n\n const intlayerAPI = getOAuthAPI(undefined, config);\n const isAuthFetchEnabled = useIsAuthFetchEnabled();\n\n const { data } = useQuery({\n queryKey: ['oAuth2AccessToken'],\n queryFn: intlayerAPI.getOAuth2AccessToken,\n // `client_credentials` is a confidential-client grant, so this query only\n // ever runs where a `clientSecret` is legitimately available — never in a\n // browser, since the generated build-time configuration omits it and\n // `@intlayer/config/secrets` resolves to an inert stub there.\n //\n // Held back until after page load when a `DeferredAuthProvider` is mounted\n // (content sites); unconditional everywhere else (the CMS dashboard).\n enabled:\n !!(config.editor.clientId && config.editor.clientSecret) &&\n isAuthFetchEnabled,\n staleTime: 0,\n gcTime: 0,\n refetchOnWindowFocus: false,\n refetchOnMount: false,\n refetchOnReconnect: false,\n refetchInterval: false,\n refetchIntervalInBackground: false,\n });\n\n const oAuth2AccessToken = data?.data;\n\n return {\n oAuth2AccessToken,\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAsBA,MAAa,aACX,0BACoB;CACpB,MAAM,gBAAgB,iBAAiB;CACvC,MAAM,SAAS,KAAK,uBAAuB,eAAe,EACxD,OACF,CAAC;CAED,MAAM,cAAc,YAAY,QAAW,MAAM;CACjD,MAAM,qBAAqB,sBAAsB;CAEjD,MAAM,EAAE,SAAS,SAAS;EACxB,UAAU,CAAC,mBAAmB;EAC9B,SAAS,YAAY;EAQrB,SACE,CAAC,EAAE,OAAO,OAAO,YAAY,OAAO,OAAO,iBAC3C;EACF,WAAW;EACX,QAAQ;EACR,sBAAsB;EACtB,gBAAgB;EAChB,oBAAoB;EACpB,iBAAiB;EACjB,6BAA6B;CAC/B,CAAC;CAID,OAAO,EACL,mBAHwB,MAAM,KAIhC;AACF"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { getAuthAPI } from "../libs/auth.mjs";
|
|
4
|
-
import { useAuth } from "./useAuth/useAuth.mjs";
|
|
5
4
|
import { getIntlayerAPI } from "@intlayer/api";
|
|
6
5
|
import { getAiAPI } from "@intlayer/api/ai";
|
|
7
6
|
import { getAnalyticsAPI } from "@intlayer/api/analytics";
|
|
@@ -29,9 +28,8 @@ import { useConfiguration } from "@intlayer/editor-react";
|
|
|
29
28
|
//#region src/api/useIntlayerAPI.ts
|
|
30
29
|
const useIntlayerOAuthOptions = (props) => {
|
|
31
30
|
const configuration = useConfiguration();
|
|
32
|
-
const { oAuth2AccessToken } = useAuth();
|
|
33
31
|
const options = {
|
|
34
|
-
...
|
|
32
|
+
...props?.sessionToken && { headers: { Authorization: `Bearer ${props.sessionToken}` } },
|
|
35
33
|
...props?.options ?? {}
|
|
36
34
|
};
|
|
37
35
|
const rawConfig = props?.intlayerConfiguration ?? configuration;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useIntlayerAPI.mjs","names":["getOAuthAPI"],"sources":["../../../src/api/useIntlayerAPI.ts"],"sourcesContent":["'use client';\n\nimport {\n type FetcherOptions,\n getIntlayerAPI,\n type IntlayerAPI,\n} from '@intlayer/api';\nimport { getAiAPI } from '@intlayer/api/ai';\nimport { getAnalyticsAPI } from '@intlayer/api/analytics';\nimport { getAssetAPI } from '@intlayer/api/asset';\nimport { getAuditAPI } from '@intlayer/api/audit';\nimport { getBitbucketAPI } from '@intlayer/api/bitbucket';\nimport { getDictionaryAPI } from '@intlayer/api/dictionary';\nimport { getEditorAPI } from '@intlayer/api/editor';\nimport { getEnvironmentAPI } from '@intlayer/api/environment';\nimport { getGithubAPI } from '@intlayer/api/github';\nimport { getGitlabAPI } from '@intlayer/api/gitlab';\nimport { getNewsletterAPI } from '@intlayer/api/newsletter';\nimport { getOAuthAPI } from '@intlayer/api/oAuth';\nimport { getOrganizationAPI } from '@intlayer/api/organization';\nimport { getProjectAPI } from '@intlayer/api/project';\nimport { getReviewerAPI } from '@intlayer/api/reviewer';\nimport { getSearchAPI } from '@intlayer/api/search';\nimport { getShowcaseProjectAPI } from '@intlayer/api/showcaseProject';\nimport { getStripeAPI } from '@intlayer/api/stripe';\nimport { getTagAPI } from '@intlayer/api/tag';\nimport { getTranslateAPI } from '@intlayer/api/translate';\nimport { getUserAPI } from '@intlayer/api/user';\nimport { useConfiguration } from '@intlayer/editor-react';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { type AuthAPI, getAuthAPI } from '../libs/auth';\nimport { useAuth } from './useAuth';\n\nexport type UseIntlayerAuthProps = {\n options?: FetcherOptions;\n intlayerConfiguration?: IntlayerConfig;\n};\n\nexport const useIntlayerOAuthOptions = (props?: UseIntlayerAuthProps) => {\n const configuration = useConfiguration();\n const { oAuth2AccessToken } = useAuth();\n\n const options = {\n ...(oAuth2AccessToken && {\n headers: {\n Authorization: `Bearer ${oAuth2AccessToken.accessToken}`,\n },\n }),\n ...(props?.options ?? {}),\n };\n\n const rawConfig = props?.intlayerConfiguration ?? configuration;\n // Only use the config if it's fully populated; an empty object (e.g. while\n // the session is still loading) has no `editor` key and would crash API\n // getters that do `intlayerConfig.editor.backendURL`.\n const resolvedConfig = rawConfig?.editor ? rawConfig : undefined;\n\n return { options, resolvedConfig };\n};\n\nexport const useIntlayerOAuth = (props?: UseIntlayerAuthProps): IntlayerAPI => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getIntlayerAPI(options, resolvedConfig);\n};\n\nexport const useIntlayerAuth = (props?: UseIntlayerAuthProps): AuthAPI => {\n const configuration = useConfiguration();\n\n return getAuthAPI(props?.intlayerConfiguration ?? configuration);\n};\n\nexport const useOrganizationAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getOrganizationAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getOrganizationAPI(options, resolvedConfig);\n};\n\nexport const useProjectAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getProjectAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getProjectAPI(options, resolvedConfig);\n};\n\nexport const useUserAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getUserAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getUserAPI(options, resolvedConfig);\n};\n\nexport const useOAuthAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getOAuthAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getOAuthAPI(options, resolvedConfig);\n};\n\nexport const useDictionaryAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getDictionaryAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getDictionaryAPI(options, resolvedConfig);\n};\n\nexport const useStripeAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getStripeAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getStripeAPI(options, resolvedConfig);\n};\n\nexport const useAiAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getAiAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getAiAPI(options, resolvedConfig);\n};\n\nexport const useAuditAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getAuditAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getAuditAPI(options, resolvedConfig);\n};\n\nexport const useAnalyticsAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getAnalyticsAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getAnalyticsAPI(options, resolvedConfig);\n};\n\nexport const useTagAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getTagAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getTagAPI(options, resolvedConfig);\n};\n\nexport const useSearchAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getSearchAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getSearchAPI(options, resolvedConfig);\n};\n\nexport const useEditorAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getEditorAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getEditorAPI(options, resolvedConfig);\n};\n\nexport const useNewsletterAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getNewsletterAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getNewsletterAPI(options, resolvedConfig);\n};\n\nexport const useGithubAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getGithubAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getGithubAPI(options, resolvedConfig);\n};\n\nexport const useGitlabAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getGitlabAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getGitlabAPI(options, resolvedConfig);\n};\n\nexport const useBitbucketAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getBitbucketAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getBitbucketAPI(options, resolvedConfig);\n};\n\nexport const useShowcaseProjectAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getShowcaseProjectAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getShowcaseProjectAPI(options, resolvedConfig);\n};\n\nexport const useTranslateAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getTranslateAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getTranslateAPI(options, resolvedConfig);\n};\n\nexport const useReviewerAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getReviewerAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getReviewerAPI(options, resolvedConfig);\n};\n\nexport const useEnvironmentAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getEnvironmentAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getEnvironmentAPI(options, resolvedConfig);\n};\n\nexport const useAssetAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getAssetAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getAssetAPI(options, resolvedConfig);\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useIntlayerAPI.mjs","names":["getOAuthAPI"],"sources":["../../../src/api/useIntlayerAPI.ts"],"sourcesContent":["'use client';\n\nimport {\n type FetcherOptions,\n getIntlayerAPI,\n type IntlayerAPI,\n} from '@intlayer/api';\nimport { getAiAPI } from '@intlayer/api/ai';\nimport { getAnalyticsAPI } from '@intlayer/api/analytics';\nimport { getAssetAPI } from '@intlayer/api/asset';\nimport { getAuditAPI } from '@intlayer/api/audit';\nimport { getBitbucketAPI } from '@intlayer/api/bitbucket';\nimport { getDictionaryAPI } from '@intlayer/api/dictionary';\nimport { getEditorAPI } from '@intlayer/api/editor';\nimport { getEnvironmentAPI } from '@intlayer/api/environment';\nimport { getGithubAPI } from '@intlayer/api/github';\nimport { getGitlabAPI } from '@intlayer/api/gitlab';\nimport { getNewsletterAPI } from '@intlayer/api/newsletter';\nimport { getOAuthAPI } from '@intlayer/api/oAuth';\nimport { getOrganizationAPI } from '@intlayer/api/organization';\nimport { getProjectAPI } from '@intlayer/api/project';\nimport { getReviewerAPI } from '@intlayer/api/reviewer';\nimport { getSearchAPI } from '@intlayer/api/search';\nimport { getShowcaseProjectAPI } from '@intlayer/api/showcaseProject';\nimport { getStripeAPI } from '@intlayer/api/stripe';\nimport { getTagAPI } from '@intlayer/api/tag';\nimport { getTranslateAPI } from '@intlayer/api/translate';\nimport { getUserAPI } from '@intlayer/api/user';\nimport { useConfiguration } from '@intlayer/editor-react';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { type AuthAPI, getAuthAPI } from '../libs/auth';\n\nexport type UseIntlayerAuthProps = {\n options?: FetcherOptions;\n intlayerConfiguration?: IntlayerConfig;\n /**\n * A short-lived token identifying the **signed-in user**, sent as a bearer.\n *\n * Only needed when the session cookie cannot be used — the backend allows\n * credentialed requests for first-party origins only, so an app served from\n * another origin has to carry the user's identity explicitly.\n *\n * The project's `clientId` / `clientSecret` are deliberately not an option\n * here: they authenticate as the *project*, grant write access to all of its\n * content, and must never reach a browser.\n */\n sessionToken?: string;\n};\n\nexport const useIntlayerOAuthOptions = (props?: UseIntlayerAuthProps) => {\n const configuration = useConfiguration();\n\n // On a first-party origin the session cookie authenticates every call (the\n // fetcher sends `credentials: 'include'`), so no header is needed. Elsewhere\n // the caller supplies a user session token.\n const options = {\n ...(props?.sessionToken && {\n headers: {\n Authorization: `Bearer ${props.sessionToken}`,\n },\n }),\n ...(props?.options ?? {}),\n };\n\n const rawConfig = props?.intlayerConfiguration ?? configuration;\n // Only use the config if it's fully populated; an empty object (e.g. while\n // the session is still loading) has no `editor` key and would crash API\n // getters that do `intlayerConfig.editor.backendURL`.\n const resolvedConfig = rawConfig?.editor ? rawConfig : undefined;\n\n return { options, resolvedConfig };\n};\n\nexport const useIntlayerOAuth = (props?: UseIntlayerAuthProps): IntlayerAPI => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getIntlayerAPI(options, resolvedConfig);\n};\n\nexport const useIntlayerAuth = (props?: UseIntlayerAuthProps): AuthAPI => {\n const configuration = useConfiguration();\n\n return getAuthAPI(props?.intlayerConfiguration ?? configuration);\n};\n\nexport const useOrganizationAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getOrganizationAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getOrganizationAPI(options, resolvedConfig);\n};\n\nexport const useProjectAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getProjectAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getProjectAPI(options, resolvedConfig);\n};\n\nexport const useUserAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getUserAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getUserAPI(options, resolvedConfig);\n};\n\nexport const useOAuthAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getOAuthAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getOAuthAPI(options, resolvedConfig);\n};\n\nexport const useDictionaryAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getDictionaryAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getDictionaryAPI(options, resolvedConfig);\n};\n\nexport const useStripeAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getStripeAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getStripeAPI(options, resolvedConfig);\n};\n\nexport const useAiAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getAiAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getAiAPI(options, resolvedConfig);\n};\n\nexport const useAuditAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getAuditAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getAuditAPI(options, resolvedConfig);\n};\n\nexport const useAnalyticsAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getAnalyticsAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getAnalyticsAPI(options, resolvedConfig);\n};\n\nexport const useTagAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getTagAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getTagAPI(options, resolvedConfig);\n};\n\nexport const useSearchAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getSearchAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getSearchAPI(options, resolvedConfig);\n};\n\nexport const useEditorAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getEditorAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getEditorAPI(options, resolvedConfig);\n};\n\nexport const useNewsletterAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getNewsletterAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getNewsletterAPI(options, resolvedConfig);\n};\n\nexport const useGithubAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getGithubAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getGithubAPI(options, resolvedConfig);\n};\n\nexport const useGitlabAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getGitlabAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getGitlabAPI(options, resolvedConfig);\n};\n\nexport const useBitbucketAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getBitbucketAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getBitbucketAPI(options, resolvedConfig);\n};\n\nexport const useShowcaseProjectAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getShowcaseProjectAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getShowcaseProjectAPI(options, resolvedConfig);\n};\n\nexport const useTranslateAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getTranslateAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getTranslateAPI(options, resolvedConfig);\n};\n\nexport const useReviewerAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getReviewerAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getReviewerAPI(options, resolvedConfig);\n};\n\nexport const useEnvironmentAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getEnvironmentAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getEnvironmentAPI(options, resolvedConfig);\n};\n\nexport const useAssetAPI = (\n props?: UseIntlayerAuthProps\n): ReturnType<typeof getAssetAPI> => {\n const { options, resolvedConfig } = useIntlayerOAuthOptions(props);\n return getAssetAPI(options, resolvedConfig);\n};\n"],"mappings":"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"}
|
|
@@ -12,6 +12,14 @@ import { useIntlayer } from "react-intlayer";
|
|
|
12
12
|
//#region src/components/Carousel/index.tsx
|
|
13
13
|
const SWIPE_THRESHOLD_DIVISOR = 5;
|
|
14
14
|
const TRANSITION_DELAY_MS = 50;
|
|
15
|
+
const CONTAINER_VERTICAL_PADDING_PX = 40;
|
|
16
|
+
const CONTAINER_MIN_HEIGHT_PX = 400;
|
|
17
|
+
/**
|
|
18
|
+
* Border-box height carried by a `ResizeObserverEntry`, equivalent to
|
|
19
|
+
* `offsetHeight` but already computed by the browser during layout, so reading
|
|
20
|
+
* it never forces a reflow.
|
|
21
|
+
*/
|
|
22
|
+
const getEntryBlockSize = (entry) => entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;
|
|
15
23
|
const CarouselContext = createContext(null);
|
|
16
24
|
const useCarousel = () => {
|
|
17
25
|
const context = useContext(CarouselContext);
|
|
@@ -142,29 +150,11 @@ const CarouselRoot = ({ children, className, initialIndex = 0, onIndexChange, ..
|
|
|
142
150
|
const [displayedIndex, setDisplayedIndex] = useState(initialIndex);
|
|
143
151
|
const [containerHeight, setContainerHeight] = useState(0);
|
|
144
152
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
145
|
-
useLayoutEffect(() => {
|
|
146
|
-
const calculateDimensions = () => {
|
|
147
|
-
if (!containerRef.current) return;
|
|
148
|
-
const width = containerRef.current.clientWidth;
|
|
149
|
-
setContainerWidth(width);
|
|
150
|
-
const heights = itemsRef.current.map((item) => item?.offsetHeight || 0);
|
|
151
|
-
const maxHeight = Math.max(0, ...heights);
|
|
152
|
-
if (maxHeight > 0) setContainerHeight(maxHeight + 40);
|
|
153
|
-
};
|
|
154
|
-
calculateDimensions();
|
|
155
|
-
const observer = new ResizeObserver(() => {
|
|
156
|
-
calculateDimensions();
|
|
157
|
-
});
|
|
158
|
-
if (containerRef.current) observer.observe(containerRef.current);
|
|
159
|
-
itemsRef.current.forEach((item) => {
|
|
160
|
-
if (item) observer.observe(item);
|
|
161
|
-
});
|
|
162
|
-
return () => observer.disconnect();
|
|
163
|
-
}, [totalItems]);
|
|
164
153
|
const [startX, setStartX] = useState(0);
|
|
165
154
|
const [isDragging, setIsDragging] = useState(false);
|
|
166
155
|
const containerRef = useRef(null);
|
|
167
156
|
const itemsRef = useRef([]);
|
|
157
|
+
const itemHeightsRef = useRef(/* @__PURE__ */ new Map());
|
|
168
158
|
const handleSwitchItem = (diff) => {
|
|
169
159
|
if (containerWidth === 0) return;
|
|
170
160
|
const swipeStep = containerWidth / SWIPE_THRESHOLD_DIVISOR;
|
|
@@ -215,20 +205,48 @@ const CarouselRoot = ({ children, className, initialIndex = 0, onIndexChange, ..
|
|
|
215
205
|
}, TRANSITION_DELAY_MS);
|
|
216
206
|
return () => clearInterval(interval);
|
|
217
207
|
}, [selectedIndex, displayedIndex]);
|
|
208
|
+
/**
|
|
209
|
+
* Tracks the container width and the height of the tallest slide.
|
|
210
|
+
*
|
|
211
|
+
* The container height is the *output* of this measurement, so the observer
|
|
212
|
+
* must never read geometry back from the DOM: React commits the new inline
|
|
213
|
+
* height between two observer callbacks, and every read after that commit is
|
|
214
|
+
* a forced reflow. `ResizeObserverEntry` already carries the boxes the
|
|
215
|
+
* browser computed during layout, which are free to read.
|
|
216
|
+
*/
|
|
218
217
|
useLayoutEffect(() => {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
218
|
+
const container = containerRef.current;
|
|
219
|
+
if (!container) return;
|
|
220
|
+
const items = itemsRef.current.filter((item) => item !== null);
|
|
221
|
+
const itemHeights = itemHeightsRef.current;
|
|
222
|
+
const publishMaxHeight = () => {
|
|
223
|
+
const maxHeight = Math.max(0, ...itemHeights.values());
|
|
224
|
+
if (maxHeight > 0) setContainerHeight(maxHeight + CONTAINER_VERTICAL_PADDING_PX);
|
|
223
225
|
};
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
const initialWidth = container.clientWidth;
|
|
227
|
+
items.forEach((item) => {
|
|
228
|
+
itemHeights.set(item, item.offsetHeight);
|
|
227
229
|
});
|
|
228
|
-
|
|
229
|
-
|
|
230
|
+
setContainerWidth(initialWidth);
|
|
231
|
+
publishMaxHeight();
|
|
232
|
+
const observer = new ResizeObserver((entries) => {
|
|
233
|
+
entries.forEach((entry) => {
|
|
234
|
+
if (entry.target === container) {
|
|
235
|
+
setContainerWidth(entry.contentRect.width);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
itemHeights.set(entry.target, getEntryBlockSize(entry));
|
|
239
|
+
});
|
|
240
|
+
publishMaxHeight();
|
|
230
241
|
});
|
|
231
|
-
|
|
242
|
+
observer.observe(container);
|
|
243
|
+
items.forEach((item) => {
|
|
244
|
+
observer.observe(item);
|
|
245
|
+
});
|
|
246
|
+
return () => {
|
|
247
|
+
observer.disconnect();
|
|
248
|
+
itemHeights.clear();
|
|
249
|
+
};
|
|
232
250
|
}, [totalItems]);
|
|
233
251
|
return /* @__PURE__ */ jsx(CarouselContext.Provider, {
|
|
234
252
|
value: {
|
|
@@ -243,7 +261,7 @@ const CarouselRoot = ({ children, className, initialIndex = 0, onIndexChange, ..
|
|
|
243
261
|
className: cn("relative flex w-full cursor-grab select-none items-center overflow-hidden pb-4 outline-none transition-[height] duration-300 ease-in-out focus:outline-none focus:outline-none focus:ring-0 active:cursor-grabbing", "max-w-350", className),
|
|
244
262
|
style: {
|
|
245
263
|
height: containerHeight > 0 ? containerHeight : "auto",
|
|
246
|
-
minHeight:
|
|
264
|
+
minHeight: `${CONTAINER_MIN_HEIGHT_PX}px`
|
|
247
265
|
},
|
|
248
266
|
onMouseDown: handleMouseDown,
|
|
249
267
|
onMouseMove: handleMouseMove,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/components/Carousel/index.tsx"],"sourcesContent":["'use client';\n\nimport { Button } from '@components/Button';\nimport { KeyboardShortcut } from '@components/KeyboardShortcut';\nimport { Popover } from '@components/Popover';\nimport { cn } from '@utils/cn';\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\nimport {\n Children,\n createContext,\n type FC,\n type HTMLAttributes,\n isValidElement,\n type MouseEventHandler,\n type ReactElement,\n type ReactNode,\n type TouchEventHandler,\n useContext,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react';\nimport { useIntlayer } from 'react-intlayer';\n\n// ------------------------------------------------------------------\n// Configuration\n// ------------------------------------------------------------------\nconst SWIPE_THRESHOLD_DIVISOR = 5;\nconst TRANSITION_DELAY_MS = 50;\n\n// ------------------------------------------------------------------\n// Context Definition\n// ------------------------------------------------------------------\ntype CarouselContextValue = {\n selectedIndex: number;\n setSelectedIndex: (index: number) => void;\n totalItems: number;\n handlePrev: () => void;\n handleNext: () => void;\n};\n\nconst CarouselContext = createContext<CarouselContextValue | null>(null);\n\nconst useCarousel = () => {\n const context = useContext(CarouselContext);\n if (!context) {\n throw new Error('useCarousel must be used within a Carousel');\n }\n return context;\n};\n\n// ------------------------------------------------------------------\n// Helper Functions\n// ------------------------------------------------------------------\nconst getCardStyle = (index: number, displayedIndex: number) => {\n const diff = Math.abs(index - displayedIndex);\n switch (diff) {\n case 0:\n return 'opacity-100 z-40';\n case 1:\n return 'opacity-75 z-30 cursor-pointer';\n case 2:\n return 'opacity-50 z-20 pointer-events-none';\n default:\n return 'opacity-0 z-10 pointer-events-none';\n }\n};\n\nconst getCardScale = (index: number, displayedIndex: number) => {\n const diff = Math.abs(index - displayedIndex);\n switch (diff) {\n case 0:\n return 1;\n case 1:\n return 0.9;\n case 2:\n return 0.8;\n default:\n return 0.7;\n }\n};\n\n// This allows the calculation to work on SSR without hydration mismatch.\n// Your original logic: (3 * screenWidth) / 10 === 30% of viewport width\nconst getCardPositionX = (\n index: number,\n displayedIndex: number,\n containerWidth: number\n) => {\n const diff = index - displayedIndex;\n const gapPercentage = containerWidth < 600 ? 0.15 : 0.3; // Dropped to 15% for a tighter cluster\n const step = Math.min(containerWidth * gapPercentage, 300);\n\n // The 'px' here is mandatory\n return `${diff * step}px`;\n};\n\n// ------------------------------------------------------------------\n// Sub-Component: Item\n// ------------------------------------------------------------------\ntype CarouselItemProps = HTMLAttributes<HTMLDivElement> & {\n children: ReactNode;\n};\n\nconst CarouselItem: FC<CarouselItemProps> & { isCarouselItem: true } =\n Object.assign(\n ({ children, className, ...props }: CarouselItemProps) => (\n <div className={cn('size-full', className)} {...props}>\n {children}\n </div>\n ),\n { isCarouselItem: true as const }\n );\n\n// ------------------------------------------------------------------\n// Sub-Component: Indicators (Controller)\n// ------------------------------------------------------------------\ntype CarouselIndicatorsProps = HTMLAttributes<HTMLDivElement> & {\n disableKeyboardShortcuts?: boolean;\n};\n\nconst CarouselIndicators: FC<CarouselIndicatorsProps> = ({\n className,\n disableKeyboardShortcuts = false,\n ...props\n}) => {\n const {\n selectedIndex,\n setSelectedIndex,\n totalItems,\n handlePrev,\n handleNext,\n } = useCarousel();\n const { goToSlide, previousSlide, nextSlide } = useIntlayer('carousel');\n\n if (totalItems <= 1) return null;\n\n return (\n <div\n className={cn(\n 'absolute bottom-0 left-1/2 z-50 flex -translate-x-1/2 flex-row items-center gap-2',\n className\n )}\n {...props}\n >\n <Popover identifier=\"carousel-prev\">\n <Button\n variant=\"hoverable\"\n color=\"neutral\"\n label={previousSlide.value}\n roundedSize=\"full\"\n onClick={(e) => {\n e.stopPropagation();\n handlePrev();\n }}\n Icon={ChevronLeft}\n size=\"icon-md\"\n disabled={selectedIndex === 0}\n />\n\n <Popover.Detail identifier=\"carousel-prev\">\n <div className=\"flex items-center gap-2 p-2\">\n <span className=\"whitespace-nowrap text-neutral text-xs\">\n {previousSlide.value}\n </span>\n <KeyboardShortcut\n shortcut=\"ArrowLeft\"\n disabled={disableKeyboardShortcuts}\n size=\"sm\"\n onTriggered={handlePrev}\n />\n </div>\n </Popover.Detail>\n </Popover>\n\n {Array.from({ length: totalItems }).map((_, index) => {\n const isActive = index === selectedIndex;\n return (\n <button\n key={index}\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n setSelectedIndex(index);\n }}\n aria-label={goToSlide({ index: index + 1 }).value}\n className={cn(\n 'h-2.5 w-2.5 rounded-full transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2',\n isActive ? 'scale-110 bg-text' : 'bg-text/20 hover:bg-text/40'\n )}\n />\n );\n })}\n\n <Popover identifier=\"carousel-next\">\n <Button\n variant=\"hoverable\"\n color=\"neutral\"\n roundedSize=\"full\"\n label={nextSlide.value}\n onClick={(e) => {\n e.stopPropagation();\n handleNext();\n }}\n Icon={ChevronRight}\n size=\"icon-md\"\n disabled={selectedIndex === totalItems - 1}\n />\n\n <Popover.Detail identifier=\"carousel-next\">\n <div className=\"flex items-center gap-2 p-2\">\n <span className=\"whitespace-nowrap text-neutral text-xs\">\n {nextSlide.value}\n </span>\n <KeyboardShortcut\n shortcut=\"ArrowRight\"\n size=\"sm\"\n onTriggered={handleNext}\n disabled={disableKeyboardShortcuts}\n />\n </div>\n </Popover.Detail>\n </Popover>\n </div>\n );\n};\n\n// ------------------------------------------------------------------\n// Main Component: Carousel Root\n// ------------------------------------------------------------------\ntype CarouselProps = HTMLAttributes<HTMLDivElement> & {\n children: ReactNode;\n initialIndex?: number;\n onIndexChange?: (index: number) => void;\n};\n\nconst partitionCarouselChildren = (\n children: ReactNode[]\n): [ReactElement[], ReactNode[]] => {\n const slides: ReactElement[] = [];\n const others: ReactNode[] = [];\n\n children.forEach((child) => {\n if (\n isValidElement(child) &&\n (child.type === CarouselItem ||\n (child.type as { isCarouselItem?: boolean }).isCarouselItem === true)\n ) {\n slides.push(child);\n } else {\n others.push(child);\n }\n });\n\n return [slides, others];\n};\n\nconst CarouselRoot: FC<CarouselProps> = ({\n children,\n className,\n initialIndex = 0,\n onIndexChange,\n ...props\n}) => {\n const allChildren = Children.toArray(children);\n const [slides, others] = partitionCarouselChildren(allChildren);\n const totalItems = slides.length;\n\n // State Management\n const [selectedIndex, setSelectedIndex] = useState<number>(initialIndex);\n const [displayedIndex, setDisplayedIndex] = useState<number>(initialIndex);\n const [containerHeight, setContainerHeight] = useState<number>(0);\n const [containerWidth, setContainerWidth] = useState<number>(0);\n\n useLayoutEffect(() => {\n const calculateDimensions = () => {\n if (!containerRef.current) return;\n\n // Track Width\n const width = containerRef.current.clientWidth;\n setContainerWidth(width);\n\n // Track Height (existing logic)\n const heights = itemsRef.current.map((item) => item?.offsetHeight || 0);\n const maxHeight = Math.max(0, ...heights);\n if (maxHeight > 0) {\n setContainerHeight(maxHeight + 40);\n }\n };\n\n calculateDimensions();\n\n const observer = new ResizeObserver(() => {\n calculateDimensions();\n });\n\n if (containerRef.current) observer.observe(containerRef.current);\n itemsRef.current.forEach((item) => {\n if (item) observer.observe(item);\n });\n\n return () => observer.disconnect();\n }, [totalItems]);\n\n // Drag State\n const [startX, setStartX] = useState(0);\n const [isDragging, setIsDragging] = useState(false);\n\n // Refs\n const containerRef = useRef<HTMLDivElement>(null);\n const itemsRef = useRef<(HTMLDivElement | null)[]>([]);\n\n // Navigation Logic\n const handleSwitchItem = (diff: number) => {\n if (containerWidth === 0) return;\n\n // Use container width for the threshold\n const swipeStep = containerWidth / SWIPE_THRESHOLD_DIVISOR;\n const numSwipe = Math.round(diff / swipeStep);\n\n if (Math.abs(numSwipe) >= 1) {\n const newIndex = displayedIndex - numSwipe;\n const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1));\n\n if (clampedIndex !== selectedIndex) {\n setSelectedIndex(clampedIndex);\n setStartX((prev) => prev + diff);\n }\n }\n };\n\n const handleNext = () => {\n setSelectedIndex((prev) => Math.min(prev + 1, totalItems - 1));\n };\n\n const handlePrev = () => {\n setSelectedIndex((prev) => Math.max(prev - 1, 0));\n };\n\n // Input Handlers\n const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {\n setIsDragging(true);\n setStartX(e.clientX);\n };\n const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {\n if (isDragging) handleSwitchItem(e.clientX - startX);\n };\n const handleDragEnd = () => setIsDragging(false);\n const handleTouchStart: TouchEventHandler<HTMLDivElement> = (e) => {\n setIsDragging(true);\n setStartX(e.touches[0]!.clientX);\n };\n const handleTouchMove: TouchEventHandler<HTMLDivElement> = (e) => {\n if (isDragging) handleSwitchItem(e.touches[0]!.clientX - startX);\n };\n\n // Effects\n useEffect(() => {\n if (selectedIndex) onIndexChange?.(selectedIndex);\n }, [selectedIndex, onIndexChange]);\n\n useEffect(() => {\n let interval: NodeJS.Timeout;\n\n if (selectedIndex !== displayedIndex) {\n interval = setInterval(() => {\n setDisplayedIndex((prev) => {\n if (prev === selectedIndex) {\n clearInterval(interval);\n return prev;\n }\n return prev < selectedIndex ? prev + 1 : prev - 1;\n });\n }, TRANSITION_DELAY_MS);\n }\n return () => clearInterval(interval);\n }, [selectedIndex, displayedIndex]);\n\n // Calculate height based on the MAX height of ALL items\n useLayoutEffect(() => {\n const calculateMaxHeight = () => {\n const heights = itemsRef.current.map((item) => item?.offsetHeight || 0);\n const maxHeight = Math.max(0, ...heights);\n\n if (maxHeight > 0) {\n setContainerHeight(maxHeight + 40);\n }\n };\n\n calculateMaxHeight();\n\n const observer = new ResizeObserver(() => {\n calculateMaxHeight();\n });\n\n itemsRef.current.forEach((item) => {\n if (item) observer.observe(item);\n });\n\n return () => observer.disconnect();\n }, [totalItems]); // Removed isMounted dependency\n\n return (\n <CarouselContext.Provider\n value={{\n selectedIndex,\n setSelectedIndex,\n totalItems,\n handlePrev,\n handleNext,\n }}\n >\n <div\n ref={containerRef}\n className={cn(\n 'relative flex w-full cursor-grab select-none items-center overflow-hidden pb-4 outline-none transition-[height] duration-300 ease-in-out focus:outline-none focus:outline-none focus:ring-0 active:cursor-grabbing',\n 'max-w-350',\n className\n )}\n style={{\n height: containerHeight > 0 ? containerHeight : 'auto',\n minHeight: '400px',\n }}\n onMouseDown={handleMouseDown}\n onMouseMove={handleMouseMove}\n onMouseUp={handleDragEnd}\n onMouseLeave={handleDragEnd}\n onTouchStart={handleTouchStart}\n onTouchMove={handleTouchMove}\n onTouchEnd={handleDragEnd}\n role=\"region\"\n aria-label=\"Carousel\"\n {...props}\n >\n {slides.map((child, index) => {\n return (\n <div\n key={index}\n role=\"button\"\n tabIndex={0}\n ref={(el) => {\n itemsRef.current[index] = el;\n }}\n // FIX 2: Removed isMounted checks and invisible classes.\n // CSS units allow correct SSR rendering.\n className={cn(\n 'absolute left-1/2 -translate-x-1/2 transition-all duration-300 ease-in-out',\n 'outline-none ring-0 focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0',\n getCardStyle(index, displayedIndex)\n )}\n onClick={(e) => {\n e.stopPropagation();\n setSelectedIndex(index);\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') setSelectedIndex(index);\n }}\n style={{\n transform: `\n translateX(${getCardPositionX(\n index,\n displayedIndex,\n containerWidth\n )})\n scale(${getCardScale(index, displayedIndex)})\n `,\n }}\n >\n {child}\n </div>\n );\n })}\n\n {others}\n </div>\n </CarouselContext.Provider>\n );\n};\n\nexport const Carousel = Object.assign(CarouselRoot, {\n Item: CarouselItem,\n Indicators: CarouselIndicators,\n});\n"],"mappings":";;;;;;;;;;;;AA4BA,MAAM,0BAA0B;AAChC,MAAM,sBAAsB;AAa5B,MAAM,kBAAkB,cAA2C,IAAI;AAEvE,MAAM,oBAAoB;CACxB,MAAM,UAAU,WAAW,eAAe;CAC1C,IAAI,CAAC,SACH,MAAM,IAAI,MAAM,4CAA4C;CAE9D,OAAO;AACT;AAKA,MAAM,gBAAgB,OAAe,mBAA2B;CAE9D,QADa,KAAK,IAAI,QAAQ,cACnB,GAAX;EACE,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,SACE,OAAO;CACX;AACF;AAEA,MAAM,gBAAgB,OAAe,mBAA2B;CAE9D,QADa,KAAK,IAAI,QAAQ,cACnB,GAAX;EACE,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,SACE,OAAO;CACX;AACF;AAIA,MAAM,oBACJ,OACA,gBACA,mBACG;CAMH,OAAO,IALM,QAAQ,kBAER,KAAK,IAAI,kBADA,iBAAiB,MAAM,MAAO,KACE,GAGlC,EAAE;AACxB;AASA,MAAM,eACJ,OAAO,QACJ,EAAE,UAAU,WAAW,GAAG,YACzB,oBAAC,OAAD;CAAK,WAAW,GAAG,aAAa,SAAS;CAAG,GAAI;CAC7C;AACE,IAEP,EAAE,gBAAgB,KAAc,CAClC;AASF,MAAM,sBAAmD,EACvD,WACA,2BAA2B,OAC3B,GAAG,YACC;CACJ,MAAM,EACJ,eACA,kBACA,YACA,YACA,eACE,YAAY;CAChB,MAAM,EAAE,WAAW,eAAe,cAAc,YAAY,UAAU;CAEtE,IAAI,cAAc,GAAG,OAAO;CAE5B,OACE,qBAAC,OAAD;EACE,WAAW,GACT,qFACA,SACF;EACA,GAAI;YALN;GAOE,qBAAC,SAAD;IAAS,YAAW;cAApB,CACE,oBAAC,QAAD;KACE,SAAQ;KACR,OAAM;KACN,OAAO,cAAc;KACrB,aAAY;KACZ,UAAU,MAAM;MACd,EAAE,gBAAgB;MAClB,WAAW;KACb;KACA,MAAM;KACN,MAAK;KACL,UAAU,kBAAkB;IAC7B,IAED,oBAAC,QAAQ,QAAT;KAAgB,YAAW;eACzB,qBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,oBAAC,QAAD;OAAM,WAAU;iBACb,cAAc;MACX,IACN,oBAAC,kBAAD;OACE,UAAS;OACT,UAAU;OACV,MAAK;OACL,aAAa;MACd,EACE;;IACS,EACT;;GAER,MAAM,KAAK,EAAE,QAAQ,WAAW,CAAC,CAAC,CAAC,KAAK,GAAG,UAAU;IACpD,MAAM,WAAW,UAAU;IAC3B,OACE,oBAAC,UAAD;KAEE,MAAK;KACL,UAAU,MAAM;MACd,EAAE,gBAAgB;MAClB,iBAAiB,KAAK;KACxB;KACA,cAAY,UAAU,EAAE,OAAO,QAAQ,EAAE,CAAC,CAAC,CAAC;KAC5C,WAAW,GACT,mIACA,WAAW,sBAAsB,6BACnC;IACD,GAXM,KAWN;GAEL,CAAC;GAED,qBAAC,SAAD;IAAS,YAAW;cAApB,CACE,oBAAC,QAAD;KACE,SAAQ;KACR,OAAM;KACN,aAAY;KACZ,OAAO,UAAU;KACjB,UAAU,MAAM;MACd,EAAE,gBAAgB;MAClB,WAAW;KACb;KACA,MAAM;KACN,MAAK;KACL,UAAU,kBAAkB,aAAa;IAC1C,IAED,oBAAC,QAAQ,QAAT;KAAgB,YAAW;eACzB,qBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,oBAAC,QAAD;OAAM,WAAU;iBACb,UAAU;MACP,IACN,oBAAC,kBAAD;OACE,UAAS;OACT,MAAK;OACL,aAAa;OACb,UAAU;MACX,EACE;;IACS,EACT;;EACN;;AAET;AAWA,MAAM,6BACJ,aACkC;CAClC,MAAM,SAAyB,CAAC;CAChC,MAAM,SAAsB,CAAC;CAE7B,SAAS,SAAS,UAAU;EAC1B,IACE,eAAe,KAAK,MACnB,MAAM,SAAS,gBACb,MAAM,KAAsC,mBAAmB,OAElE,OAAO,KAAK,KAAK;OAEjB,OAAO,KAAK,KAAK;CAErB,CAAC;CAED,OAAO,CAAC,QAAQ,MAAM;AACxB;AAEA,MAAM,gBAAmC,EACvC,UACA,WACA,eAAe,GACf,eACA,GAAG,YACC;CACJ,MAAM,cAAc,SAAS,QAAQ,QAAQ;CAC7C,MAAM,CAAC,QAAQ,UAAU,0BAA0B,WAAW;CAC9D,MAAM,aAAa,OAAO;CAG1B,MAAM,CAAC,eAAe,oBAAoB,SAAiB,YAAY;CACvE,MAAM,CAAC,gBAAgB,qBAAqB,SAAiB,YAAY;CACzE,MAAM,CAAC,iBAAiB,sBAAsB,SAAiB,CAAC;CAChE,MAAM,CAAC,gBAAgB,qBAAqB,SAAiB,CAAC;CAE9D,sBAAsB;EACpB,MAAM,4BAA4B;GAChC,IAAI,CAAC,aAAa,SAAS;GAG3B,MAAM,QAAQ,aAAa,QAAQ;GACnC,kBAAkB,KAAK;GAGvB,MAAM,UAAU,SAAS,QAAQ,KAAK,SAAS,MAAM,gBAAgB,CAAC;GACtE,MAAM,YAAY,KAAK,IAAI,GAAG,GAAG,OAAO;GACxC,IAAI,YAAY,GACd,mBAAmB,YAAY,EAAE;EAErC;EAEA,oBAAoB;EAEpB,MAAM,WAAW,IAAI,qBAAqB;GACxC,oBAAoB;EACtB,CAAC;EAED,IAAI,aAAa,SAAS,SAAS,QAAQ,aAAa,OAAO;EAC/D,SAAS,QAAQ,SAAS,SAAS;GACjC,IAAI,MAAM,SAAS,QAAQ,IAAI;EACjC,CAAC;EAED,aAAa,SAAS,WAAW;CACnC,GAAG,CAAC,UAAU,CAAC;CAGf,MAAM,CAAC,QAAQ,aAAa,SAAS,CAAC;CACtC,MAAM,CAAC,YAAY,iBAAiB,SAAS,KAAK;CAGlD,MAAM,eAAe,OAAuB,IAAI;CAChD,MAAM,WAAW,OAAkC,CAAC,CAAC;CAGrD,MAAM,oBAAoB,SAAiB;EACzC,IAAI,mBAAmB,GAAG;EAG1B,MAAM,YAAY,iBAAiB;EACnC,MAAM,WAAW,KAAK,MAAM,OAAO,SAAS;EAE5C,IAAI,KAAK,IAAI,QAAQ,KAAK,GAAG;GAC3B,MAAM,WAAW,iBAAiB;GAClC,MAAM,eAAe,KAAK,IAAI,GAAG,KAAK,IAAI,UAAU,aAAa,CAAC,CAAC;GAEnE,IAAI,iBAAiB,eAAe;IAClC,iBAAiB,YAAY;IAC7B,WAAW,SAAS,OAAO,IAAI;GACjC;EACF;CACF;CAEA,MAAM,mBAAmB;EACvB,kBAAkB,SAAS,KAAK,IAAI,OAAO,GAAG,aAAa,CAAC,CAAC;CAC/D;CAEA,MAAM,mBAAmB;EACvB,kBAAkB,SAAS,KAAK,IAAI,OAAO,GAAG,CAAC,CAAC;CAClD;CAGA,MAAM,mBAAsD,MAAM;EAChE,cAAc,IAAI;EAClB,UAAU,EAAE,OAAO;CACrB;CACA,MAAM,mBAAsD,MAAM;EAChE,IAAI,YAAY,iBAAiB,EAAE,UAAU,MAAM;CACrD;CACA,MAAM,sBAAsB,cAAc,KAAK;CAC/C,MAAM,oBAAuD,MAAM;EACjE,cAAc,IAAI;EAClB,UAAU,EAAE,QAAQ,EAAE,CAAE,OAAO;CACjC;CACA,MAAM,mBAAsD,MAAM;EAChE,IAAI,YAAY,iBAAiB,EAAE,QAAQ,EAAE,CAAE,UAAU,MAAM;CACjE;CAGA,gBAAgB;EACd,IAAI,eAAe,gBAAgB,aAAa;CAClD,GAAG,CAAC,eAAe,aAAa,CAAC;CAEjC,gBAAgB;EACd,IAAI;EAEJ,IAAI,kBAAkB,gBACpB,WAAW,kBAAkB;GAC3B,mBAAmB,SAAS;IAC1B,IAAI,SAAS,eAAe;KAC1B,cAAc,QAAQ;KACtB,OAAO;IACT;IACA,OAAO,OAAO,gBAAgB,OAAO,IAAI,OAAO;GAClD,CAAC;EACH,GAAG,mBAAmB;EAExB,aAAa,cAAc,QAAQ;CACrC,GAAG,CAAC,eAAe,cAAc,CAAC;CAGlC,sBAAsB;EACpB,MAAM,2BAA2B;GAC/B,MAAM,UAAU,SAAS,QAAQ,KAAK,SAAS,MAAM,gBAAgB,CAAC;GACtE,MAAM,YAAY,KAAK,IAAI,GAAG,GAAG,OAAO;GAExC,IAAI,YAAY,GACd,mBAAmB,YAAY,EAAE;EAErC;EAEA,mBAAmB;EAEnB,MAAM,WAAW,IAAI,qBAAqB;GACxC,mBAAmB;EACrB,CAAC;EAED,SAAS,QAAQ,SAAS,SAAS;GACjC,IAAI,MAAM,SAAS,QAAQ,IAAI;EACjC,CAAC;EAED,aAAa,SAAS,WAAW;CACnC,GAAG,CAAC,UAAU,CAAC;CAEf,OACE,oBAAC,gBAAgB,UAAjB;EACE,OAAO;GACL;GACA;GACA;GACA;GACA;EACF;YAEA,qBAAC,OAAD;GACE,KAAK;GACL,WAAW,GACT,sNACA,aACA,SACF;GACA,OAAO;IACL,QAAQ,kBAAkB,IAAI,kBAAkB;IAChD,WAAW;GACb;GACA,aAAa;GACb,aAAa;GACb,WAAW;GACX,cAAc;GACd,cAAc;GACd,aAAa;GACb,YAAY;GACZ,MAAK;GACL,cAAW;GACX,GAAI;aApBN,CAsBG,OAAO,KAAK,OAAO,UAAU;IAC5B,OACE,oBAAC,OAAD;KAEE,MAAK;KACL,UAAU;KACV,MAAM,OAAO;MACX,SAAS,QAAQ,SAAS;KAC5B;KAGA,WAAW,GACT,8EACA,uJACA,aAAa,OAAO,cAAc,CACpC;KACA,UAAU,MAAM;MACd,EAAE,gBAAgB;MAClB,iBAAiB,KAAK;KACxB;KACA,YAAY,MAAM;MAChB,IAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,KAAK,iBAAiB,KAAK;KAChE;KACA,OAAO,EACL,WAAW;+BACI,iBACX,OACA,gBACA,cACF,EAAE;0BACM,aAAa,OAAO,cAAc,EAAE;kBAEhD;eAEC;IACE,GAhCE,KAgCF;GAET,CAAC,GAEA,MACE;;CACmB;AAE9B;AAEA,MAAa,WAAW,OAAO,OAAO,cAAc;CAClD,MAAM;CACN,YAAY;AACd,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/components/Carousel/index.tsx"],"sourcesContent":["'use client';\n\nimport { Button } from '@components/Button';\nimport { KeyboardShortcut } from '@components/KeyboardShortcut';\nimport { Popover } from '@components/Popover';\nimport { cn } from '@utils/cn';\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\nimport {\n Children,\n createContext,\n type FC,\n type HTMLAttributes,\n isValidElement,\n type MouseEventHandler,\n type ReactElement,\n type ReactNode,\n type TouchEventHandler,\n useContext,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react';\nimport { useIntlayer } from 'react-intlayer';\n\n// ------------------------------------------------------------------\n// Configuration\n// ------------------------------------------------------------------\nconst SWIPE_THRESHOLD_DIVISOR = 5;\nconst TRANSITION_DELAY_MS = 50;\nconst CONTAINER_VERTICAL_PADDING_PX = 40;\nconst CONTAINER_MIN_HEIGHT_PX = 400;\n\n/**\n * Border-box height carried by a `ResizeObserverEntry`, equivalent to\n * `offsetHeight` but already computed by the browser during layout, so reading\n * it never forces a reflow.\n */\nconst getEntryBlockSize = (entry: ResizeObserverEntry): number =>\n entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;\n\n// ------------------------------------------------------------------\n// Context Definition\n// ------------------------------------------------------------------\ntype CarouselContextValue = {\n selectedIndex: number;\n setSelectedIndex: (index: number) => void;\n totalItems: number;\n handlePrev: () => void;\n handleNext: () => void;\n};\n\nconst CarouselContext = createContext<CarouselContextValue | null>(null);\n\nconst useCarousel = () => {\n const context = useContext(CarouselContext);\n if (!context) {\n throw new Error('useCarousel must be used within a Carousel');\n }\n return context;\n};\n\n// ------------------------------------------------------------------\n// Helper Functions\n// ------------------------------------------------------------------\nconst getCardStyle = (index: number, displayedIndex: number) => {\n const diff = Math.abs(index - displayedIndex);\n switch (diff) {\n case 0:\n return 'opacity-100 z-40';\n case 1:\n return 'opacity-75 z-30 cursor-pointer';\n case 2:\n return 'opacity-50 z-20 pointer-events-none';\n default:\n return 'opacity-0 z-10 pointer-events-none';\n }\n};\n\nconst getCardScale = (index: number, displayedIndex: number) => {\n const diff = Math.abs(index - displayedIndex);\n switch (diff) {\n case 0:\n return 1;\n case 1:\n return 0.9;\n case 2:\n return 0.8;\n default:\n return 0.7;\n }\n};\n\n// This allows the calculation to work on SSR without hydration mismatch.\n// Your original logic: (3 * screenWidth) / 10 === 30% of viewport width\nconst getCardPositionX = (\n index: number,\n displayedIndex: number,\n containerWidth: number\n) => {\n const diff = index - displayedIndex;\n const gapPercentage = containerWidth < 600 ? 0.15 : 0.3; // Dropped to 15% for a tighter cluster\n const step = Math.min(containerWidth * gapPercentage, 300);\n\n // The 'px' here is mandatory\n return `${diff * step}px`;\n};\n\n// ------------------------------------------------------------------\n// Sub-Component: Item\n// ------------------------------------------------------------------\ntype CarouselItemProps = HTMLAttributes<HTMLDivElement> & {\n children: ReactNode;\n};\n\nconst CarouselItem: FC<CarouselItemProps> & { isCarouselItem: true } =\n Object.assign(\n ({ children, className, ...props }: CarouselItemProps) => (\n <div className={cn('size-full', className)} {...props}>\n {children}\n </div>\n ),\n { isCarouselItem: true as const }\n );\n\n// ------------------------------------------------------------------\n// Sub-Component: Indicators (Controller)\n// ------------------------------------------------------------------\ntype CarouselIndicatorsProps = HTMLAttributes<HTMLDivElement> & {\n disableKeyboardShortcuts?: boolean;\n};\n\nconst CarouselIndicators: FC<CarouselIndicatorsProps> = ({\n className,\n disableKeyboardShortcuts = false,\n ...props\n}) => {\n const {\n selectedIndex,\n setSelectedIndex,\n totalItems,\n handlePrev,\n handleNext,\n } = useCarousel();\n const { goToSlide, previousSlide, nextSlide } = useIntlayer('carousel');\n\n if (totalItems <= 1) return null;\n\n return (\n <div\n className={cn(\n 'absolute bottom-0 left-1/2 z-50 flex -translate-x-1/2 flex-row items-center gap-2',\n className\n )}\n {...props}\n >\n <Popover identifier=\"carousel-prev\">\n <Button\n variant=\"hoverable\"\n color=\"neutral\"\n label={previousSlide.value}\n roundedSize=\"full\"\n onClick={(e) => {\n e.stopPropagation();\n handlePrev();\n }}\n Icon={ChevronLeft}\n size=\"icon-md\"\n disabled={selectedIndex === 0}\n />\n\n <Popover.Detail identifier=\"carousel-prev\">\n <div className=\"flex items-center gap-2 p-2\">\n <span className=\"whitespace-nowrap text-neutral text-xs\">\n {previousSlide.value}\n </span>\n <KeyboardShortcut\n shortcut=\"ArrowLeft\"\n disabled={disableKeyboardShortcuts}\n size=\"sm\"\n onTriggered={handlePrev}\n />\n </div>\n </Popover.Detail>\n </Popover>\n\n {Array.from({ length: totalItems }).map((_, index) => {\n const isActive = index === selectedIndex;\n return (\n <button\n key={index}\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n setSelectedIndex(index);\n }}\n aria-label={goToSlide({ index: index + 1 }).value}\n className={cn(\n 'h-2.5 w-2.5 rounded-full transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2',\n isActive ? 'scale-110 bg-text' : 'bg-text/20 hover:bg-text/40'\n )}\n />\n );\n })}\n\n <Popover identifier=\"carousel-next\">\n <Button\n variant=\"hoverable\"\n color=\"neutral\"\n roundedSize=\"full\"\n label={nextSlide.value}\n onClick={(e) => {\n e.stopPropagation();\n handleNext();\n }}\n Icon={ChevronRight}\n size=\"icon-md\"\n disabled={selectedIndex === totalItems - 1}\n />\n\n <Popover.Detail identifier=\"carousel-next\">\n <div className=\"flex items-center gap-2 p-2\">\n <span className=\"whitespace-nowrap text-neutral text-xs\">\n {nextSlide.value}\n </span>\n <KeyboardShortcut\n shortcut=\"ArrowRight\"\n size=\"sm\"\n onTriggered={handleNext}\n disabled={disableKeyboardShortcuts}\n />\n </div>\n </Popover.Detail>\n </Popover>\n </div>\n );\n};\n\n// ------------------------------------------------------------------\n// Main Component: Carousel Root\n// ------------------------------------------------------------------\ntype CarouselProps = HTMLAttributes<HTMLDivElement> & {\n children: ReactNode;\n initialIndex?: number;\n onIndexChange?: (index: number) => void;\n};\n\nconst partitionCarouselChildren = (\n children: ReactNode[]\n): [ReactElement[], ReactNode[]] => {\n const slides: ReactElement[] = [];\n const others: ReactNode[] = [];\n\n children.forEach((child) => {\n if (\n isValidElement(child) &&\n (child.type === CarouselItem ||\n (child.type as { isCarouselItem?: boolean }).isCarouselItem === true)\n ) {\n slides.push(child);\n } else {\n others.push(child);\n }\n });\n\n return [slides, others];\n};\n\nconst CarouselRoot: FC<CarouselProps> = ({\n children,\n className,\n initialIndex = 0,\n onIndexChange,\n ...props\n}) => {\n const allChildren = Children.toArray(children);\n const [slides, others] = partitionCarouselChildren(allChildren);\n const totalItems = slides.length;\n\n // State Management\n const [selectedIndex, setSelectedIndex] = useState<number>(initialIndex);\n const [displayedIndex, setDisplayedIndex] = useState<number>(initialIndex);\n const [containerHeight, setContainerHeight] = useState<number>(0);\n const [containerWidth, setContainerWidth] = useState<number>(0);\n\n // Drag State\n const [startX, setStartX] = useState(0);\n const [isDragging, setIsDragging] = useState(false);\n\n // Refs\n const containerRef = useRef<HTMLDivElement>(null);\n const itemsRef = useRef<(HTMLDivElement | null)[]>([]);\n const itemHeightsRef = useRef<Map<Element, number>>(new Map());\n\n // Navigation Logic\n const handleSwitchItem = (diff: number) => {\n if (containerWidth === 0) return;\n\n // Use container width for the threshold\n const swipeStep = containerWidth / SWIPE_THRESHOLD_DIVISOR;\n const numSwipe = Math.round(diff / swipeStep);\n\n if (Math.abs(numSwipe) >= 1) {\n const newIndex = displayedIndex - numSwipe;\n const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1));\n\n if (clampedIndex !== selectedIndex) {\n setSelectedIndex(clampedIndex);\n setStartX((prev) => prev + diff);\n }\n }\n };\n\n const handleNext = () => {\n setSelectedIndex((prev) => Math.min(prev + 1, totalItems - 1));\n };\n\n const handlePrev = () => {\n setSelectedIndex((prev) => Math.max(prev - 1, 0));\n };\n\n // Input Handlers\n const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {\n setIsDragging(true);\n setStartX(e.clientX);\n };\n const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {\n if (isDragging) handleSwitchItem(e.clientX - startX);\n };\n const handleDragEnd = () => setIsDragging(false);\n const handleTouchStart: TouchEventHandler<HTMLDivElement> = (e) => {\n setIsDragging(true);\n setStartX(e.touches[0]!.clientX);\n };\n const handleTouchMove: TouchEventHandler<HTMLDivElement> = (e) => {\n if (isDragging) handleSwitchItem(e.touches[0]!.clientX - startX);\n };\n\n // Effects\n useEffect(() => {\n if (selectedIndex) onIndexChange?.(selectedIndex);\n }, [selectedIndex, onIndexChange]);\n\n useEffect(() => {\n let interval: NodeJS.Timeout;\n\n if (selectedIndex !== displayedIndex) {\n interval = setInterval(() => {\n setDisplayedIndex((prev) => {\n if (prev === selectedIndex) {\n clearInterval(interval);\n return prev;\n }\n return prev < selectedIndex ? prev + 1 : prev - 1;\n });\n }, TRANSITION_DELAY_MS);\n }\n return () => clearInterval(interval);\n }, [selectedIndex, displayedIndex]);\n\n /**\n * Tracks the container width and the height of the tallest slide.\n *\n * The container height is the *output* of this measurement, so the observer\n * must never read geometry back from the DOM: React commits the new inline\n * height between two observer callbacks, and every read after that commit is\n * a forced reflow. `ResizeObserverEntry` already carries the boxes the\n * browser computed during layout, which are free to read.\n */\n useLayoutEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const items = itemsRef.current.filter((item) => item !== null);\n const itemHeights = itemHeightsRef.current;\n\n const publishMaxHeight = () => {\n const maxHeight = Math.max(0, ...itemHeights.values());\n\n if (maxHeight > 0) {\n setContainerHeight(maxHeight + CONTAINER_VERTICAL_PADDING_PX);\n }\n };\n\n // First pass: every read happens before any state write, so the initial\n // measurement costs a single layout.\n const initialWidth = container.clientWidth;\n items.forEach((item) => {\n itemHeights.set(item, item.offsetHeight);\n });\n\n setContainerWidth(initialWidth);\n publishMaxHeight();\n\n const observer = new ResizeObserver((entries) => {\n entries.forEach((entry) => {\n if (entry.target === container) {\n setContainerWidth(entry.contentRect.width);\n return;\n }\n\n itemHeights.set(entry.target, getEntryBlockSize(entry));\n });\n\n publishMaxHeight();\n });\n\n observer.observe(container);\n items.forEach((item) => {\n observer.observe(item);\n });\n\n return () => {\n observer.disconnect();\n itemHeights.clear();\n };\n }, [totalItems]);\n\n return (\n <CarouselContext.Provider\n value={{\n selectedIndex,\n setSelectedIndex,\n totalItems,\n handlePrev,\n handleNext,\n }}\n >\n <div\n ref={containerRef}\n className={cn(\n 'relative flex w-full cursor-grab select-none items-center overflow-hidden pb-4 outline-none transition-[height] duration-300 ease-in-out focus:outline-none focus:outline-none focus:ring-0 active:cursor-grabbing',\n 'max-w-350',\n className\n )}\n style={{\n height: containerHeight > 0 ? containerHeight : 'auto',\n minHeight: `${CONTAINER_MIN_HEIGHT_PX}px`,\n }}\n onMouseDown={handleMouseDown}\n onMouseMove={handleMouseMove}\n onMouseUp={handleDragEnd}\n onMouseLeave={handleDragEnd}\n onTouchStart={handleTouchStart}\n onTouchMove={handleTouchMove}\n onTouchEnd={handleDragEnd}\n role=\"region\"\n aria-label=\"Carousel\"\n {...props}\n >\n {slides.map((child, index) => {\n return (\n <div\n key={index}\n role=\"button\"\n tabIndex={0}\n ref={(el) => {\n itemsRef.current[index] = el;\n }}\n // FIX 2: Removed isMounted checks and invisible classes.\n // CSS units allow correct SSR rendering.\n className={cn(\n 'absolute left-1/2 -translate-x-1/2 transition-all duration-300 ease-in-out',\n 'outline-none ring-0 focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0',\n getCardStyle(index, displayedIndex)\n )}\n onClick={(e) => {\n e.stopPropagation();\n setSelectedIndex(index);\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') setSelectedIndex(index);\n }}\n style={{\n transform: `\n translateX(${getCardPositionX(\n index,\n displayedIndex,\n containerWidth\n )})\n scale(${getCardScale(index, displayedIndex)})\n `,\n }}\n >\n {child}\n </div>\n );\n })}\n\n {others}\n </div>\n </CarouselContext.Provider>\n );\n};\n\nexport const Carousel = Object.assign(CarouselRoot, {\n Item: CarouselItem,\n Indicators: CarouselIndicators,\n});\n"],"mappings":"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"}
|
|
@@ -5,7 +5,7 @@ import { MaxHeightSmoother } from "../MaxHeightSmoother/index.mjs";
|
|
|
5
5
|
import { useScrollBlockage } from "../../hooks/useScrollBlockage/index.mjs";
|
|
6
6
|
import { useScrollDetection } from "../../hooks/useScrollDetection.mjs";
|
|
7
7
|
import { Burger } from "./Burger.mjs";
|
|
8
|
-
import { useRef, useState } from "react";
|
|
8
|
+
import { useEffect, useRef, useState } from "react";
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
import { m } from "framer-motion";
|
|
11
11
|
|
|
@@ -110,6 +110,7 @@ const bgStyle = "bg-card/95 shadow-[0_0_10px_-15px_rgba(0,0,0,0.3)] backdrop-blu
|
|
|
110
110
|
const MobileNavbar = ({ logo, topChildren, topSections = [], bottomChildren, bottomSections = [], rightItems, rollable = true }) => {
|
|
111
111
|
const [isHidden, setIsHidden] = useState(false);
|
|
112
112
|
const [isUnrolled, setIsUnrolled] = useState(false);
|
|
113
|
+
const [headerHeight, setHeaderHeight] = useState(0);
|
|
113
114
|
const navRef = useRef(null);
|
|
114
115
|
useScrollBlockage({
|
|
115
116
|
disableScroll: rollable,
|
|
@@ -120,7 +121,28 @@ const MobileNavbar = ({ logo, topChildren, topSections = [], bottomChildren, bot
|
|
|
120
121
|
onScrollDown: () => setIsHidden(true),
|
|
121
122
|
isEnabled: !isUnrolled && rollable
|
|
122
123
|
});
|
|
123
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Measures the collapsed header so the rolled-down panel can size itself to
|
|
126
|
+
* the rest of the viewport.
|
|
127
|
+
*
|
|
128
|
+
* Reading `clientHeight` from the render body forced a layout on every
|
|
129
|
+
* render, and `useScrollDetection` re-renders this component on each wheel
|
|
130
|
+
* event — so the measurement ran once per scroll frame. A `ResizeObserver`
|
|
131
|
+
* callback runs *after* layout, which makes the same read free, and the
|
|
132
|
+
* height is published only when it actually changed.
|
|
133
|
+
*/
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
const headerElement = navRef.current;
|
|
136
|
+
if (!headerElement) return;
|
|
137
|
+
const resizeObserver = new ResizeObserver(([entry]) => {
|
|
138
|
+
if (!entry) return;
|
|
139
|
+
const measuredHeight = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;
|
|
140
|
+
setHeaderHeight((previousHeight) => previousHeight === measuredHeight ? previousHeight : measuredHeight);
|
|
141
|
+
});
|
|
142
|
+
resizeObserver.observe(headerElement);
|
|
143
|
+
return () => resizeObserver.disconnect();
|
|
144
|
+
}, []);
|
|
145
|
+
const backDivHeight = isHidden ? 0 : headerHeight;
|
|
124
146
|
const isBurgerShowed = topSections.length + bottomSections.length > 0;
|
|
125
147
|
return /* @__PURE__ */ jsxs("nav", {
|
|
126
148
|
className: cn(bgStyle, "sticky top-0 z-50 flex w-screen flex-col transition", isHidden ? "-translate-y-full" : "translate-y-0"),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MobileNavbar.mjs","names":[],"sources":["../../../../src/components/Navbar/MobileNavbar.tsx"],"sourcesContent":["'use client';\n\nimport { useScrollBlockage } from '@hooks/useScrollBlockage';\nimport { useScrollDetection } from '@hooks/useScrollDetection';\nimport { cn } from '@utils/cn';\nimport { m, type Variants } from 'framer-motion';\nimport { type ReactElement, type ReactNode, useRef, useState } from 'react';\nimport { MaxHeightSmoother } from '../MaxHeightSmoother';\nimport type { TabSelectorItemProps } from '../TabSelector';\nimport { Burger } from './Burger';\n\n/**\n * Props for the MobileNavbar component\n * @template T - The tab props type extending TabProps\n */\ntype MobileNavbarProps<T extends TabSelectorItemProps> = {\n /** Logo component or element displayed in the header */\n logo: ReactNode;\n /** Additional content displayed at the top of expanded mobile menu */\n topChildren?: ReactNode;\n /** Navigation sections displayed in the top area of expanded menu */\n topSections?: ReactElement<T>[];\n /** Additional content displayed at the bottom of expanded mobile menu */\n bottomChildren?: ReactNode;\n /** Navigation sections displayed in the bottom area of expanded menu */\n bottomSections?: ReactElement<T>[];\n /** Right-aligned items in the collapsed header (e.g., search, notifications) */\n rightItems?: ReactNode;\n /** Whether the navbar should be rollable (default: true) */\n rollable?: boolean;\n};\n\n/**\n * Framer Motion animation variants for staggered menu item reveals\n * Creates a smooth cascading effect when menu opens/closes\n */\nconst navVariants: Variants = {\n open: {\n transition: { staggerChildren: 0.07, delayChildren: 0.2 },\n },\n closed: {\n transition: { staggerChildren: 0.05, staggerDirection: -1 },\n },\n};\n\n/**\n * Shared background styling for mobile navbar components\n * Provides glass-morphism effect with blur and transparency\n */\nconst bgStyle =\n 'bg-card/95 shadow-[0_0_10px_-15px_rgba(0,0,0,0.3)] backdrop-blur';\n\n/**\n * Mobile Navigation Bar Component\n *\n * A sophisticated mobile-first navigation component with rollable full-screen menu,\n * scroll-aware behavior, and smooth animations. Optimized for touch interactions and\n * mobile user experience patterns.\n *\n * Features:\n * - rollable hamburger menu with full-screen overlay\n * - Auto-hide on scroll down, show on scroll up for screen space optimization\n * - Background scroll prevention when menu is open\n * - Staggered animations for smooth menu item reveals\n * - Flexible content areas (top/bottom children and sections)\n * - Responsive layout with viewport-aware sizing\n * - Backdrop blur effects for modern glass-morphism design\n *\n * Layout Structure:\n * ```\n * [Logo] ----------- [Right Items] [Burger]\n * (when expanded)\n * ┌─────────────────────────────────────────┐\n * │ [Top Children] │\n * │ [Top Sections - Navigation Items] │\n * │ [Bottom Sections - Navigation Items] │\n * │ [Bottom Children] │\n * └─────────────────────────────────────────┘\n * ```\n *\n * Behavioral Features:\n * - Sticky positioning with dynamic hide/show based on scroll direction\n * - Background scroll locking when menu is expanded\n * - Click outside to close expanded menu\n * - Smooth height animations with MaxHeightSmoother\n * - Intelligent burger button visibility (only shown if sections exist)\n *\n * Animation Details:\n * - Menu items animate in with staggered timing (70ms delay between items)\n * - Exit animations are reversed with 50ms stagger\n * - Initial delay of 200ms before items start animating in\n * - Full viewport height menu with dynamic height calculation\n *\n * @example\n * Basic mobile navbar:\n * ```tsx\n * <MobileNavbar\n * logo={<MobileLogo />}\n * topSections={primaryNavItems}\n * rightItems={<SearchIcon />}\n * />\n * ```\n *\n * @example\n * Full-featured mobile navbar:\n * ```tsx\n * <MobileNavbar\n * logo={<Logo />}\n * topChildren={<WelcomeMessage />}\n * topSections={mainNavItems}\n * bottomSections={utilityNavItems}\n * bottomChildren={<UserProfile />}\n * rightItems={\n * <>\n * <NotificationIcon />\n * <SearchIcon />\n * </>\n * }\n * />\n * ```\n *\n * Accessibility Features:\n * - Menu expanded state communicated via aria-expanded\n * - Focus management and keyboard navigation support\n * - Screen reader friendly with semantic nav structure\n *\n * @template T - Tab properties type extending TabProps for type safety\n * @param props - MobileNavbar component props\n * @returns Mobile navigation with rollable full-screen menu\n */\nexport const MobileNavbar = <T extends TabSelectorItemProps>({\n logo,\n topChildren,\n topSections = [],\n bottomChildren,\n bottomSections = [],\n rightItems,\n rollable = true,\n}: MobileNavbarProps<T>) => {\n const [isHidden, setIsHidden] = useState<boolean>(false);\n const [isUnrolled, setIsUnrolled] = useState<boolean>(false);\n\n const navRef = useRef<HTMLDivElement>(null);\n\n useScrollBlockage({\n disableScroll: rollable,\n key: 'mobile_nav',\n });\n\n useScrollDetection({\n onScrollUp: () => setIsHidden(false),\n onScrollDown: () => setIsHidden(true),\n isEnabled: !isUnrolled && rollable,\n });\n\n const backDivHeight = !isHidden ? (navRef.current?.clientHeight ?? 0) : 0;\n\n const isBurgerShowed = topSections.length + bottomSections.length > 0;\n\n return (\n <nav\n className={cn(\n bgStyle,\n 'sticky top-0 z-50 flex w-screen flex-col transition',\n isHidden ? '-translate-y-full' : 'translate-y-0'\n )}\n id=\"mobile-menu\"\n >\n <div\n className=\"flex w-full items-center justify-between gap-1 px-4 py-3 md:gap-[10vw]\"\n ref={navRef}\n >\n {logo}\n\n <div className=\"flex w-full flex-1 items-center justify-end gap-6\">\n <div className=\"flex w-full items-center justify-end gap-1\">\n {rightItems}\n </div>\n\n {isBurgerShowed && (\n <Burger\n isActive={isUnrolled}\n onClick={() => setIsUnrolled((isUnrolled) => !isUnrolled)}\n />\n )}\n </div>\n </div>\n\n <div\n className={cn(\n bgStyle,\n 'absolute bottom-0 left-0 w-full translate-y-full'\n )}\n >\n <MaxHeightSmoother isHidden={!isUnrolled}>\n <m.div\n className=\"flex w-full flex-col pt-10 pb-[20%] text-lg text-text tracking-wide\"\n onClick={() => setIsUnrolled(false)}\n animate={isUnrolled ? 'open' : 'closed'}\n variants={navVariants}\n style={{\n height: `calc(100vh - ${backDivHeight}px)`,\n }}\n >\n {topChildren}\n <div className=\"flex h-full flex-col justify-center\">\n {topSections}\n {bottomSections}\n </div>\n\n <div className=\"m-auto flex w-full max-w-100 items-center justify-center gap-1 px-5 py-3\">\n {bottomChildren}\n </div>\n </m.div>\n </MaxHeightSmoother>\n </div>\n </nav>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAoCA,MAAM,cAAwB;CAC5B,MAAM,EACJ,YAAY;EAAE,iBAAiB;EAAM,eAAe;CAAI,EAC1D;CACA,QAAQ,EACN,YAAY;EAAE,iBAAiB;EAAM,kBAAkB;CAAG,EAC5D;AACF;;;;;AAMA,MAAM,UACJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgFF,MAAa,gBAAgD,EAC3D,MACA,aACA,cAAc,CAAC,GACf,gBACA,iBAAiB,CAAC,GAClB,YACA,WAAW,WACe;CAC1B,MAAM,CAAC,UAAU,eAAe,SAAkB,KAAK;CACvD,MAAM,CAAC,YAAY,iBAAiB,SAAkB,KAAK;CAE3D,MAAM,SAAS,OAAuB,IAAI;CAE1C,kBAAkB;EAChB,eAAe;EACf,KAAK;CACP,CAAC;CAED,mBAAmB;EACjB,kBAAkB,YAAY,KAAK;EACnC,oBAAoB,YAAY,IAAI;EACpC,WAAW,CAAC,cAAc;CAC5B,CAAC;CAED,MAAM,gBAAgB,CAAC,WAAY,OAAO,SAAS,gBAAgB,IAAK;CAExE,MAAM,iBAAiB,YAAY,SAAS,eAAe,SAAS;CAEpE,OACE,qBAAC,OAAD;EACE,WAAW,GACT,SACA,uDACA,WAAW,sBAAsB,eACnC;EACA,IAAG;YANL,CAQE,qBAAC,OAAD;GACE,WAAU;GACV,KAAK;aAFP,CAIG,MAED,qBAAC,OAAD;IAAK,WAAU;cAAf,CACE,oBAAC,OAAD;KAAK,WAAU;eACZ;IACE,IAEJ,kBACC,oBAAC,QAAD;KACE,UAAU;KACV,eAAe,eAAe,eAAe,CAAC,UAAU;IACzD,EAEA;KACF;MAEL,oBAAC,OAAD;GACE,WAAW,GACT,SACA,kDACF;aAEA,oBAAC,mBAAD;IAAmB,UAAU,CAAC;cAC5B,qBAAC,EAAE,KAAH;KACE,WAAU;KACV,eAAe,cAAc,KAAK;KAClC,SAAS,aAAa,SAAS;KAC/B,UAAU;KACV,OAAO,EACL,QAAQ,gBAAgB,cAAc,KACxC;eAPF;MASG;MACD,qBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,aACA,cACE;;MAEL,oBAAC,OAAD;OAAK,WAAU;iBACZ;MACE;KACA;;GACU;EAChB,EACF;;AAET"}
|
|
1
|
+
{"version":3,"file":"MobileNavbar.mjs","names":[],"sources":["../../../../src/components/Navbar/MobileNavbar.tsx"],"sourcesContent":["'use client';\n\nimport { useScrollBlockage } from '@hooks/useScrollBlockage';\nimport { useScrollDetection } from '@hooks/useScrollDetection';\nimport { cn } from '@utils/cn';\nimport { m, type Variants } from 'framer-motion';\nimport {\n type ReactElement,\n type ReactNode,\n useEffect,\n useRef,\n useState,\n} from 'react';\nimport { MaxHeightSmoother } from '../MaxHeightSmoother';\nimport type { TabSelectorItemProps } from '../TabSelector';\nimport { Burger } from './Burger';\n\n/**\n * Props for the MobileNavbar component\n * @template T - The tab props type extending TabProps\n */\ntype MobileNavbarProps<T extends TabSelectorItemProps> = {\n /** Logo component or element displayed in the header */\n logo: ReactNode;\n /** Additional content displayed at the top of expanded mobile menu */\n topChildren?: ReactNode;\n /** Navigation sections displayed in the top area of expanded menu */\n topSections?: ReactElement<T>[];\n /** Additional content displayed at the bottom of expanded mobile menu */\n bottomChildren?: ReactNode;\n /** Navigation sections displayed in the bottom area of expanded menu */\n bottomSections?: ReactElement<T>[];\n /** Right-aligned items in the collapsed header (e.g., search, notifications) */\n rightItems?: ReactNode;\n /** Whether the navbar should be rollable (default: true) */\n rollable?: boolean;\n};\n\n/**\n * Framer Motion animation variants for staggered menu item reveals\n * Creates a smooth cascading effect when menu opens/closes\n */\nconst navVariants: Variants = {\n open: {\n transition: { staggerChildren: 0.07, delayChildren: 0.2 },\n },\n closed: {\n transition: { staggerChildren: 0.05, staggerDirection: -1 },\n },\n};\n\n/**\n * Shared background styling for mobile navbar components\n * Provides glass-morphism effect with blur and transparency\n */\nconst bgStyle =\n 'bg-card/95 shadow-[0_0_10px_-15px_rgba(0,0,0,0.3)] backdrop-blur';\n\n/**\n * Mobile Navigation Bar Component\n *\n * A sophisticated mobile-first navigation component with rollable full-screen menu,\n * scroll-aware behavior, and smooth animations. Optimized for touch interactions and\n * mobile user experience patterns.\n *\n * Features:\n * - rollable hamburger menu with full-screen overlay\n * - Auto-hide on scroll down, show on scroll up for screen space optimization\n * - Background scroll prevention when menu is open\n * - Staggered animations for smooth menu item reveals\n * - Flexible content areas (top/bottom children and sections)\n * - Responsive layout with viewport-aware sizing\n * - Backdrop blur effects for modern glass-morphism design\n *\n * Layout Structure:\n * ```\n * [Logo] ----------- [Right Items] [Burger]\n * (when expanded)\n * ┌─────────────────────────────────────────┐\n * │ [Top Children] │\n * │ [Top Sections - Navigation Items] │\n * │ [Bottom Sections - Navigation Items] │\n * │ [Bottom Children] │\n * └─────────────────────────────────────────┘\n * ```\n *\n * Behavioral Features:\n * - Sticky positioning with dynamic hide/show based on scroll direction\n * - Background scroll locking when menu is expanded\n * - Click outside to close expanded menu\n * - Smooth height animations with MaxHeightSmoother\n * - Intelligent burger button visibility (only shown if sections exist)\n *\n * Animation Details:\n * - Menu items animate in with staggered timing (70ms delay between items)\n * - Exit animations are reversed with 50ms stagger\n * - Initial delay of 200ms before items start animating in\n * - Full viewport height menu with dynamic height calculation\n *\n * @example\n * Basic mobile navbar:\n * ```tsx\n * <MobileNavbar\n * logo={<MobileLogo />}\n * topSections={primaryNavItems}\n * rightItems={<SearchIcon />}\n * />\n * ```\n *\n * @example\n * Full-featured mobile navbar:\n * ```tsx\n * <MobileNavbar\n * logo={<Logo />}\n * topChildren={<WelcomeMessage />}\n * topSections={mainNavItems}\n * bottomSections={utilityNavItems}\n * bottomChildren={<UserProfile />}\n * rightItems={\n * <>\n * <NotificationIcon />\n * <SearchIcon />\n * </>\n * }\n * />\n * ```\n *\n * Accessibility Features:\n * - Menu expanded state communicated via aria-expanded\n * - Focus management and keyboard navigation support\n * - Screen reader friendly with semantic nav structure\n *\n * @template T - Tab properties type extending TabProps for type safety\n * @param props - MobileNavbar component props\n * @returns Mobile navigation with rollable full-screen menu\n */\nexport const MobileNavbar = <T extends TabSelectorItemProps>({\n logo,\n topChildren,\n topSections = [],\n bottomChildren,\n bottomSections = [],\n rightItems,\n rollable = true,\n}: MobileNavbarProps<T>) => {\n const [isHidden, setIsHidden] = useState<boolean>(false);\n const [isUnrolled, setIsUnrolled] = useState<boolean>(false);\n const [headerHeight, setHeaderHeight] = useState<number>(0);\n\n const navRef = useRef<HTMLDivElement>(null);\n\n useScrollBlockage({\n disableScroll: rollable,\n key: 'mobile_nav',\n });\n\n useScrollDetection({\n onScrollUp: () => setIsHidden(false),\n onScrollDown: () => setIsHidden(true),\n isEnabled: !isUnrolled && rollable,\n });\n\n /**\n * Measures the collapsed header so the rolled-down panel can size itself to\n * the rest of the viewport.\n *\n * Reading `clientHeight` from the render body forced a layout on every\n * render, and `useScrollDetection` re-renders this component on each wheel\n * event — so the measurement ran once per scroll frame. A `ResizeObserver`\n * callback runs *after* layout, which makes the same read free, and the\n * height is published only when it actually changed.\n */\n useEffect(() => {\n const headerElement = navRef.current;\n\n if (!headerElement) return;\n\n const resizeObserver = new ResizeObserver(([entry]) => {\n if (!entry) return;\n\n // Border box, to stay equivalent to the `clientHeight` this replaces:\n // `contentRect` would drop the header's vertical padding.\n const measuredHeight =\n entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;\n\n setHeaderHeight((previousHeight) =>\n previousHeight === measuredHeight ? previousHeight : measuredHeight\n );\n });\n\n resizeObserver.observe(headerElement);\n\n return () => resizeObserver.disconnect();\n }, []);\n\n const backDivHeight = isHidden ? 0 : headerHeight;\n\n const isBurgerShowed = topSections.length + bottomSections.length > 0;\n\n return (\n <nav\n className={cn(\n bgStyle,\n 'sticky top-0 z-50 flex w-screen flex-col transition',\n isHidden ? '-translate-y-full' : 'translate-y-0'\n )}\n id=\"mobile-menu\"\n >\n <div\n className=\"flex w-full items-center justify-between gap-1 px-4 py-3 md:gap-[10vw]\"\n ref={navRef}\n >\n {logo}\n\n <div className=\"flex w-full flex-1 items-center justify-end gap-6\">\n <div className=\"flex w-full items-center justify-end gap-1\">\n {rightItems}\n </div>\n\n {isBurgerShowed && (\n <Burger\n isActive={isUnrolled}\n onClick={() => setIsUnrolled((isUnrolled) => !isUnrolled)}\n />\n )}\n </div>\n </div>\n\n <div\n className={cn(\n bgStyle,\n 'absolute bottom-0 left-0 w-full translate-y-full'\n )}\n >\n <MaxHeightSmoother isHidden={!isUnrolled}>\n <m.div\n className=\"flex w-full flex-col pt-10 pb-[20%] text-lg text-text tracking-wide\"\n onClick={() => setIsUnrolled(false)}\n animate={isUnrolled ? 'open' : 'closed'}\n variants={navVariants}\n style={{\n height: `calc(100vh - ${backDivHeight}px)`,\n }}\n >\n {topChildren}\n <div className=\"flex h-full flex-col justify-center\">\n {topSections}\n {bottomSections}\n </div>\n\n <div className=\"m-auto flex w-full max-w-100 items-center justify-center gap-1 px-5 py-3\">\n {bottomChildren}\n </div>\n </m.div>\n </MaxHeightSmoother>\n </div>\n </nav>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;AA0CA,MAAM,cAAwB;CAC5B,MAAM,EACJ,YAAY;EAAE,iBAAiB;EAAM,eAAe;CAAI,EAC1D;CACA,QAAQ,EACN,YAAY;EAAE,iBAAiB;EAAM,kBAAkB;CAAG,EAC5D;AACF;;;;;AAMA,MAAM,UACJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgFF,MAAa,gBAAgD,EAC3D,MACA,aACA,cAAc,CAAC,GACf,gBACA,iBAAiB,CAAC,GAClB,YACA,WAAW,WACe;CAC1B,MAAM,CAAC,UAAU,eAAe,SAAkB,KAAK;CACvD,MAAM,CAAC,YAAY,iBAAiB,SAAkB,KAAK;CAC3D,MAAM,CAAC,cAAc,mBAAmB,SAAiB,CAAC;CAE1D,MAAM,SAAS,OAAuB,IAAI;CAE1C,kBAAkB;EAChB,eAAe;EACf,KAAK;CACP,CAAC;CAED,mBAAmB;EACjB,kBAAkB,YAAY,KAAK;EACnC,oBAAoB,YAAY,IAAI;EACpC,WAAW,CAAC,cAAc;CAC5B,CAAC;;;;;;;;;;;CAYD,gBAAgB;EACd,MAAM,gBAAgB,OAAO;EAE7B,IAAI,CAAC,eAAe;EAEpB,MAAM,iBAAiB,IAAI,gBAAgB,CAAC,WAAW;GACrD,IAAI,CAAC,OAAO;GAIZ,MAAM,iBACJ,MAAM,gBAAgB,EAAE,EAAE,aAAa,MAAM,YAAY;GAE3D,iBAAiB,mBACf,mBAAmB,iBAAiB,iBAAiB,cACvD;EACF,CAAC;EAED,eAAe,QAAQ,aAAa;EAEpC,aAAa,eAAe,WAAW;CACzC,GAAG,CAAC,CAAC;CAEL,MAAM,gBAAgB,WAAW,IAAI;CAErC,MAAM,iBAAiB,YAAY,SAAS,eAAe,SAAS;CAEpE,OACE,qBAAC,OAAD;EACE,WAAW,GACT,SACA,uDACA,WAAW,sBAAsB,eACnC;EACA,IAAG;YANL,CAQE,qBAAC,OAAD;GACE,WAAU;GACV,KAAK;aAFP,CAIG,MAED,qBAAC,OAAD;IAAK,WAAU;cAAf,CACE,oBAAC,OAAD;KAAK,WAAU;eACZ;IACE,IAEJ,kBACC,oBAAC,QAAD;KACE,UAAU;KACV,eAAe,eAAe,eAAe,CAAC,UAAU;IACzD,EAEA;KACF;MAEL,oBAAC,OAAD;GACE,WAAW,GACT,SACA,kDACF;aAEA,oBAAC,mBAAD;IAAmB,UAAU,CAAC;cAC5B,qBAAC,EAAE,KAAH;KACE,WAAU;KACV,eAAe,cAAc,KAAK;KAClC,SAAS,aAAa,SAAS;KAC/B,UAAU;KACV,OAAO,EACL,QAAQ,gBAAgB,cAAc,KACxC;eAPF;MASG;MACD,qBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,aACA,cACE;;MAEL,oBAAC,OAAD;OAAK,WAAU;iBACZ;MACE;KACA;;GACU;EAChB,EACF;;AAET"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useTheme } from "../../providers/ThemeProvider.mjs";
|
|
4
3
|
import { cn } from "../../utils/cn.mjs";
|
|
4
|
+
import { useTheme } from "../../providers/ThemeProvider.mjs";
|
|
5
5
|
import { useEffect, useRef } from "react";
|
|
6
6
|
import { jsx } from "react/jsx-runtime";
|
|
7
7
|
|
|
@@ -9,8 +9,8 @@ import { useGetElementOrWindow } from "../../hooks/useGetElementOrWindow.mjs";
|
|
|
9
9
|
import { useScrollBlockage } from "../../hooks/useScrollBlockage/index.mjs";
|
|
10
10
|
import { MaxWidthSmoother } from "../MaxWidthSmoother/index.mjs";
|
|
11
11
|
import { isElementAtTopAndNotCovered } from "./isElementAtTopAndNotCovered.mjs";
|
|
12
|
-
import {
|
|
13
|
-
import { useEffect, useRef } from "react";
|
|
12
|
+
import { useIsRightDrawerOpen, useRightDrawerActions } from "./useRightDrawer.mjs";
|
|
13
|
+
import { useEffect, useRef, useState } from "react";
|
|
14
14
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
15
15
|
import { ChevronLeft, X } from "lucide-react";
|
|
16
16
|
import { useIntlayer } from "react-intlayer";
|
|
@@ -23,18 +23,41 @@ const RightDrawer = ({ title, identifier, children, header, footer, closeOnOutsi
|
|
|
23
23
|
const panelRef = useRef(null);
|
|
24
24
|
const childrenContainerRef = useRef(null);
|
|
25
25
|
const containerElement = useGetElementOrWindow(container);
|
|
26
|
-
const { open: openDrawer, close: closeDrawer
|
|
27
|
-
const storeIsOpen =
|
|
26
|
+
const { open: openDrawer, close: closeDrawer } = useRightDrawerActions();
|
|
27
|
+
const storeIsOpen = useIsRightDrawerOpen(identifier);
|
|
28
28
|
const isOpen = storeIsOpen;
|
|
29
|
+
/**
|
|
30
|
+
* A drawer mounted by its parent in the already-open state is inserted into
|
|
31
|
+
* the DOM at its final width, leaving the transition no previous value to
|
|
32
|
+
* start from: it pops instead of sliding. Painting the closed state for a
|
|
33
|
+
* frame first gives the transition its starting point.
|
|
34
|
+
*/
|
|
35
|
+
const [hasEnteredOpen, setHasEnteredOpen] = useState(false);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!isOpen) {
|
|
38
|
+
setHasEnteredOpen(false);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let secondFrame = 0;
|
|
42
|
+
const firstFrame = requestAnimationFrame(() => {
|
|
43
|
+
secondFrame = requestAnimationFrame(() => setHasEnteredOpen(true));
|
|
44
|
+
});
|
|
45
|
+
return () => {
|
|
46
|
+
cancelAnimationFrame(firstFrame);
|
|
47
|
+
cancelAnimationFrame(secondFrame);
|
|
48
|
+
};
|
|
49
|
+
}, [isOpen]);
|
|
50
|
+
const isVisuallyOpen = isOpen && hasEnteredOpen;
|
|
29
51
|
useScrollBlockage({
|
|
30
52
|
disableScroll: isOpen,
|
|
31
53
|
key: identifier ? `right_drawer_${identifier}` : "right_drawer"
|
|
32
54
|
});
|
|
33
55
|
useEffect(() => {
|
|
56
|
+
if (!isOpen) return;
|
|
34
57
|
const handleClickOutside = (event) => {
|
|
35
58
|
try {
|
|
36
59
|
if (!panelRef.current) return;
|
|
37
|
-
const isClickAble =
|
|
60
|
+
const isClickAble = closeOnOutsideClick;
|
|
38
61
|
const isClickOutside = event.target && !panelRef.current.contains(event.target);
|
|
39
62
|
const isAtTopAndVisible = isElementAtTopAndNotCovered(panelRef.current);
|
|
40
63
|
if (isClickAble && isClickOutside && isAtTopAndVisible || !event.target) {
|
|
@@ -95,7 +118,7 @@ const RightDrawer = ({ title, identifier, children, header, footer, closeOnOutsi
|
|
|
95
118
|
return createPortal(/* @__PURE__ */ jsx("div", {
|
|
96
119
|
className: "fixed top-0 right-0 z-50 flex h-full justify-end",
|
|
97
120
|
children: /* @__PURE__ */ jsx(MaxWidthSmoother, {
|
|
98
|
-
isHidden: !
|
|
121
|
+
isHidden: !isVisuallyOpen,
|
|
99
122
|
align: "right",
|
|
100
123
|
children: /* @__PURE__ */ jsxs(Container, {
|
|
101
124
|
className: "relative flex h-screen w-screen flex-col border-l text-text md:w-100",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RightDrawer.mjs","names":[],"sources":["../../../../src/components/RightDrawer/RightDrawer.tsx"],"sourcesContent":["'use client';\n\nimport { useGetElementOrWindow } from '@hooks/index';\nimport { useDevice } from '@hooks/useDevice';\nimport { useScrollBlockage } from '@hooks/useScrollBlockage';\nimport { ChevronLeft, X } from 'lucide-react';\nimport {\n type FC,\n type KeyboardEventHandler,\n type MouseEventHandler,\n type ReactNode,\n useEffect,\n useRef,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { useIntlayer } from 'react-intlayer';\nimport { Button } from '../Button';\nimport { Container } from '../Container';\nimport { KeyboardShortcut } from '../KeyboardShortcut';\nimport { MaxWidthSmoother } from '../MaxWidthSmoother/index';\nimport { Popover } from '../Popover';\nimport { isElementAtTopAndNotCovered } from './isElementAtTopAndNotCovered';\nimport { useRightDrawer } from './useRightDrawer';\n\n/**\n * Configuration for the back button functionality in the RightDrawer\n *\n * @interface BackButtonProps\n */\ntype BackButtonProps = {\n /** Callback function triggered when the back button is clicked */\n onBack: () => void;\n /** Optional custom text for the back buttoDefaults to \"Go back\" if not provided */\n text?: string;\n};\n\n/**\n * Props configuration for the RightDrawer component\n *\n * @interface RightDrawerProps\n */\ntype RightDrawerProps = {\n /**\n * Title displayed in the drawer header\n */\n title?: ReactNode;\n\n /**\n * Unique identifier for the drawer instancRequired for store management\n */\n identifier: string;\n\n /** The content to be displayed inside the drawer */\n children?: ReactNode;\n\n /**\n * Optional header content displayed below the title\n */\n header?: ReactNode;\n\n /**\n * Optional footer content pinned to the bottom of the drawer\n */\n footer?: ReactNode;\n\n /**\n * Whether the drawer should close when clicking outside of it\n * @default true\n */\n closeOnOutsideClick?: boolean;\n\n /**\n * Configuration for an optional back button in the drawer header\n */\n backButton?: BackButtonProps;\n\n /**\n * External control for the open statWhen provided, overrides internal store state\n */\n isOpen?: boolean;\n\n /**\n * Callback function triggered when the drawer is closed\n */\n onClose?: () => void;\n\n /**\n * Optional container to render the drawer into.\n * If not provided, it will be rendered into the body.\n */\n container?: HTMLElement;\n};\n\nexport const RightDrawer: FC<RightDrawerProps> = ({\n title,\n identifier,\n children,\n header,\n footer,\n closeOnOutsideClick = true,\n backButton,\n isOpen: isOpenProp,\n onClose,\n container,\n}) => {\n const content = useIntlayer('right-drawer');\n const { isMobile } = useDevice('md');\n const panelRef = useRef<HTMLDivElement>(null);\n const childrenContainerRef = useRef<HTMLDivElement>(null);\n const containerElement = useGetElementOrWindow(container);\n\n const {\n open: openDrawer,\n close: closeDrawer,\n isOpen: checkIsOpen,\n } = useRightDrawer();\n const storeIsOpen = checkIsOpen(identifier);\n const isOpen = storeIsOpen;\n\n useScrollBlockage({\n disableScroll: isOpen,\n key: identifier ? `right_drawer_${identifier}` : 'right_drawer',\n });\n\n // Handle Click Outside\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent) => {\n try {\n if (!panelRef.current) return;\n\n const isClickAble = isOpen && closeOnOutsideClick;\n const isClickOutside =\n event.target && !panelRef.current.contains(event.target as Node);\n const isAtTopAndVisible = isElementAtTopAndNotCovered(panelRef.current);\n\n if (\n (isClickAble && isClickOutside && isAtTopAndVisible) ||\n !event.target\n ) {\n closeDrawer(identifier);\n onClose?.();\n }\n } catch (_e) {\n closeDrawer(identifier);\n onClose?.();\n }\n };\n\n window.addEventListener('mousedown', handleClickOutside);\n return () => window.removeEventListener('mousedown', handleClickOutside);\n }, [isOpen, closeDrawer, onClose, closeOnOutsideClick, identifier]);\n\n const onCloseRef = useRef(onClose);\n useEffect(() => {\n onCloseRef.current = onClose;\n }, [onClose]);\n\n useEffect(() => {\n if (isOpenProp === undefined) return;\n if (isOpenProp === storeIsOpen) return;\n\n if (isOpenProp) {\n openDrawer(identifier);\n } else {\n closeDrawer(identifier);\n onCloseRef.current?.();\n }\n }, [isOpenProp, storeIsOpen, identifier, openDrawer, closeDrawer]);\n\n const handleSpareSpaceClick: MouseEventHandler<HTMLDivElement> = (e) => {\n if (e.target !== e.currentTarget) {\n return;\n }\n if (isMobile) {\n closeDrawer(identifier);\n onClose?.();\n }\n };\n\n // Handle Keyboard on Spare Space (Linter Fix)\n const handleSpareSpaceKeyDown: KeyboardEventHandler<HTMLDivElement> = (e) => {\n if (e.target !== e.currentTarget) return;\n\n // Allow closing via Enter or Space if focused on the spare area\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n if (isMobile) {\n closeDrawer(identifier);\n onClose?.();\n }\n }\n };\n\n if (!containerElement) return <></>;\n\n return createPortal(\n <div className=\"fixed top-0 right-0 z-50 flex h-full justify-end\">\n <MaxWidthSmoother isHidden={!isOpen} align=\"right\">\n <Container\n className=\"relative flex h-screen w-screen flex-col border-l text-text md:w-100\"\n ref={panelRef}\n roundedSize=\"none\"\n borderColor=\"neutral\"\n >\n {/* Header */}\n <div className=\"flex shrink-0 flex-col gap-3 px-6 pt-6\">\n <div className=\"flex justify-between gap-3\">\n <div>\n {backButton && (\n <Button\n variant=\"hoverable\"\n color=\"text\"\n label={backButton.text ?? content.goBack.value}\n onClick={backButton.onBack}\n Icon={ChevronLeft}\n >\n {backButton?.text}\n </Button>\n )}\n </div>\n <div>\n <Popover identifier=\"close-drawer\">\n <Button\n variant=\"hoverable\"\n color=\"text\"\n label=\"Close\"\n className=\"ml-auto\"\n onClick={() => {\n closeDrawer(identifier);\n onClose?.();\n }}\n Icon={X}\n size=\"icon-md\"\n />\n\n <Popover.Detail identifier=\"close-drawer\">\n <div className=\"flex items-center gap-2 p-2\">\n <span className=\"whitespace-nowrap text-neutral text-xs\">\n {content.closeDrawer}\n </span>\n <KeyboardShortcut\n shortcut=\"Escape\"\n size=\"sm\"\n onTriggered={() => {\n closeDrawer(identifier);\n onClose?.();\n }}\n />\n </div>\n </Popover.Detail>\n </Popover>\n </div>\n </div>\n {title && (\n <h2 className=\"flex items-center justify-center font-bold text-lg\">\n {title}\n </h2>\n )}\n {header}\n </div>\n\n {/* Body */}\n <div className=\"flex min-h-0 flex-1 flex-col overflow-y-auto p-2\">\n {/** biome-ignore lint/a11y/useSemanticElements: This div is used to handle the spare space click and keydown events */}\n <div\n className=\"flex flex-1 flex-col outline-none\"\n onClick={handleSpareSpaceClick}\n onKeyDown={handleSpareSpaceKeyDown}\n ref={childrenContainerRef}\n role=\"button\" // Semantically acts as a button area\n tabIndex={0} // Makes it focusable to receive key events\n >\n {children}\n </div>\n </div>\n\n {/* Footer */}\n {footer && <div className=\"shrink-0\">{footer}</div>}\n </Container>\n </MaxWidthSmoother>\n </div>,\n containerElement\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RightDrawer.mjs","names":[],"sources":["../../../../src/components/RightDrawer/RightDrawer.tsx"],"sourcesContent":["'use client';\n\nimport { useGetElementOrWindow } from '@hooks/index';\nimport { useDevice } from '@hooks/useDevice';\nimport { useScrollBlockage } from '@hooks/useScrollBlockage';\nimport { ChevronLeft, X } from 'lucide-react';\nimport {\n type FC,\n type KeyboardEventHandler,\n type MouseEventHandler,\n type ReactNode,\n useEffect,\n useRef,\n useState,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { useIntlayer } from 'react-intlayer';\nimport { Button } from '../Button';\nimport { Container } from '../Container';\nimport { KeyboardShortcut } from '../KeyboardShortcut';\nimport { MaxWidthSmoother } from '../MaxWidthSmoother/index';\nimport { Popover } from '../Popover';\nimport { isElementAtTopAndNotCovered } from './isElementAtTopAndNotCovered';\nimport { useIsRightDrawerOpen, useRightDrawerActions } from './useRightDrawer';\n\n/**\n * Configuration for the back button functionality in the RightDrawer\n *\n * @interface BackButtonProps\n */\ntype BackButtonProps = {\n /** Callback function triggered when the back button is clicked */\n onBack: () => void;\n /** Optional custom text for the back buttoDefaults to \"Go back\" if not provided */\n text?: string;\n};\n\n/**\n * Props configuration for the RightDrawer component\n *\n * @interface RightDrawerProps\n */\ntype RightDrawerProps = {\n /**\n * Title displayed in the drawer header\n */\n title?: ReactNode;\n\n /**\n * Unique identifier for the drawer instancRequired for store management\n */\n identifier: string;\n\n /** The content to be displayed inside the drawer */\n children?: ReactNode;\n\n /**\n * Optional header content displayed below the title\n */\n header?: ReactNode;\n\n /**\n * Optional footer content pinned to the bottom of the drawer\n */\n footer?: ReactNode;\n\n /**\n * Whether the drawer should close when clicking outside of it\n * @default true\n */\n closeOnOutsideClick?: boolean;\n\n /**\n * Configuration for an optional back button in the drawer header\n */\n backButton?: BackButtonProps;\n\n /**\n * External control for the open statWhen provided, overrides internal store state\n */\n isOpen?: boolean;\n\n /**\n * Callback function triggered when the drawer is closed\n */\n onClose?: () => void;\n\n /**\n * Optional container to render the drawer into.\n * If not provided, it will be rendered into the body.\n */\n container?: HTMLElement;\n};\n\nexport const RightDrawer: FC<RightDrawerProps> = ({\n title,\n identifier,\n children,\n header,\n footer,\n closeOnOutsideClick = true,\n backButton,\n isOpen: isOpenProp,\n onClose,\n container,\n}) => {\n const content = useIntlayer('right-drawer');\n const { isMobile } = useDevice('md');\n const panelRef = useRef<HTMLDivElement>(null);\n const childrenContainerRef = useRef<HTMLDivElement>(null);\n const containerElement = useGetElementOrWindow(container);\n\n const { open: openDrawer, close: closeDrawer } = useRightDrawerActions();\n const storeIsOpen = useIsRightDrawerOpen(identifier);\n const isOpen = storeIsOpen;\n\n /**\n * A drawer mounted by its parent in the already-open state is inserted into\n * the DOM at its final width, leaving the transition no previous value to\n * start from: it pops instead of sliding. Painting the closed state for a\n * frame first gives the transition its starting point.\n */\n const [hasEnteredOpen, setHasEnteredOpen] = useState(false);\n\n useEffect(() => {\n if (!isOpen) {\n setHasEnteredOpen(false);\n return;\n }\n\n let secondFrame = 0;\n const firstFrame = requestAnimationFrame(() => {\n secondFrame = requestAnimationFrame(() => setHasEnteredOpen(true));\n });\n\n return () => {\n cancelAnimationFrame(firstFrame);\n cancelAnimationFrame(secondFrame);\n };\n }, [isOpen]);\n\n const isVisuallyOpen = isOpen && hasEnteredOpen;\n\n useScrollBlockage({\n disableScroll: isOpen,\n key: identifier ? `right_drawer_${identifier}` : 'right_drawer',\n });\n\n // Handle Click Outside\n useEffect(() => {\n if (!isOpen) return;\n\n const handleClickOutside = (event: MouseEvent) => {\n try {\n if (!panelRef.current) return;\n\n const isClickAble = closeOnOutsideClick;\n const isClickOutside =\n event.target && !panelRef.current.contains(event.target as Node);\n const isAtTopAndVisible = isElementAtTopAndNotCovered(panelRef.current);\n\n if (\n (isClickAble && isClickOutside && isAtTopAndVisible) ||\n !event.target\n ) {\n closeDrawer(identifier);\n onClose?.();\n }\n } catch (_e) {\n closeDrawer(identifier);\n onClose?.();\n }\n };\n\n window.addEventListener('mousedown', handleClickOutside);\n return () => window.removeEventListener('mousedown', handleClickOutside);\n }, [isOpen, closeDrawer, onClose, closeOnOutsideClick, identifier]);\n\n const onCloseRef = useRef(onClose);\n useEffect(() => {\n onCloseRef.current = onClose;\n }, [onClose]);\n\n useEffect(() => {\n if (isOpenProp === undefined) return;\n if (isOpenProp === storeIsOpen) return;\n\n if (isOpenProp) {\n openDrawer(identifier);\n } else {\n closeDrawer(identifier);\n onCloseRef.current?.();\n }\n }, [isOpenProp, storeIsOpen, identifier, openDrawer, closeDrawer]);\n\n const handleSpareSpaceClick: MouseEventHandler<HTMLDivElement> = (e) => {\n if (e.target !== e.currentTarget) {\n return;\n }\n if (isMobile) {\n closeDrawer(identifier);\n onClose?.();\n }\n };\n\n // Handle Keyboard on Spare Space (Linter Fix)\n const handleSpareSpaceKeyDown: KeyboardEventHandler<HTMLDivElement> = (e) => {\n if (e.target !== e.currentTarget) return;\n\n // Allow closing via Enter or Space if focused on the spare area\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n if (isMobile) {\n closeDrawer(identifier);\n onClose?.();\n }\n }\n };\n\n if (!containerElement) return <></>;\n\n return createPortal(\n <div className=\"fixed top-0 right-0 z-50 flex h-full justify-end\">\n <MaxWidthSmoother isHidden={!isVisuallyOpen} align=\"right\">\n <Container\n className=\"relative flex h-screen w-screen flex-col border-l text-text md:w-100\"\n ref={panelRef}\n roundedSize=\"none\"\n borderColor=\"neutral\"\n >\n {/* Header */}\n <div className=\"flex shrink-0 flex-col gap-3 px-6 pt-6\">\n <div className=\"flex justify-between gap-3\">\n <div>\n {backButton && (\n <Button\n variant=\"hoverable\"\n color=\"text\"\n label={backButton.text ?? content.goBack.value}\n onClick={backButton.onBack}\n Icon={ChevronLeft}\n >\n {backButton?.text}\n </Button>\n )}\n </div>\n <div>\n <Popover identifier=\"close-drawer\">\n <Button\n variant=\"hoverable\"\n color=\"text\"\n label=\"Close\"\n className=\"ml-auto\"\n onClick={() => {\n closeDrawer(identifier);\n onClose?.();\n }}\n Icon={X}\n size=\"icon-md\"\n />\n\n <Popover.Detail identifier=\"close-drawer\">\n <div className=\"flex items-center gap-2 p-2\">\n <span className=\"whitespace-nowrap text-neutral text-xs\">\n {content.closeDrawer}\n </span>\n <KeyboardShortcut\n shortcut=\"Escape\"\n size=\"sm\"\n onTriggered={() => {\n closeDrawer(identifier);\n onClose?.();\n }}\n />\n </div>\n </Popover.Detail>\n </Popover>\n </div>\n </div>\n {title && (\n <h2 className=\"flex items-center justify-center font-bold text-lg\">\n {title}\n </h2>\n )}\n {header}\n </div>\n\n {/* Body */}\n <div className=\"flex min-h-0 flex-1 flex-col overflow-y-auto p-2\">\n {/** biome-ignore lint/a11y/useSemanticElements: This div is used to handle the spare space click and keydown events */}\n <div\n className=\"flex flex-1 flex-col outline-none\"\n onClick={handleSpareSpaceClick}\n onKeyDown={handleSpareSpaceKeyDown}\n ref={childrenContainerRef}\n role=\"button\" // Semantically acts as a button area\n tabIndex={0} // Makes it focusable to receive key events\n >\n {children}\n </div>\n </div>\n\n {/* Footer */}\n {footer && <div className=\"shrink-0\">{footer}</div>}\n </Container>\n </MaxWidthSmoother>\n </div>,\n containerElement\n );\n};\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { drawerManager, useRightDrawer } from "./useRightDrawer.mjs";
|
|
1
|
+
import { drawerManager, useIsRightDrawerOpen, useRightDrawer, useRightDrawerActions } from "./useRightDrawer.mjs";
|
|
2
2
|
import { RightDrawer } from "./RightDrawer.mjs";
|
|
3
3
|
|
|
4
|
-
export { RightDrawer, drawerManager, useRightDrawer };
|
|
4
|
+
export { RightDrawer, drawerManager, useIsRightDrawerOpen, useRightDrawer, useRightDrawerActions };
|