From d04f30efc2e6a1754b3604e9287a600470f9589e Mon Sep 17 00:00:00 2001 From: nabeliwo Date: Tue, 17 Dec 2024 20:52:47 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=83=87=E3=82=B9=E3=82=AF=E3=83=88?= =?UTF-8?q?=E3=83=83=E3=83=97=E3=82=B5=E3=82=A4=E3=82=BA=E3=81=AE=20AppHea?= =?UTF-8?q?der=20=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppHeader/AppHeader.stories.tsx | 147 ++++++++++++ .../src/components/AppHeader/AppHeader.tsx | 22 ++ .../components/common/CommonButton.tsx | 73 ++++++ .../AppHeader/components/common/Translate.tsx | 5 + .../components/desktop/DesktopHeader.tsx | 102 +++++++++ .../components/desktop/Navigation.tsx | 144 ++++++++++++ .../desktop/ReleaseNotesDropdown.tsx | 85 +++++++ .../components/desktop/UserInfo.tsx.tsx | 210 ++++++++++++++++++ .../components/mobile/MobileHeader.tsx | 7 + .../components/AppHeader/hooks/useLocale.tsx | 18 ++ .../AppHeader/hooks/useMediaQuery.ts | 29 +++ .../AppHeader/hooks/useTranslate.ts | 15 ++ .../src/components/AppHeader/index.ts | 1 + .../AppHeader/multilingualization/index.ts | 3 + .../multilingualization/localeMap.ts | 10 + .../AppHeader/multilingualization/messages.ts | 179 +++++++++++++++ .../multilingualization/translate.ts | 7 + .../AppHeader/multilingualization/types.ts | 4 + .../src/components/AppHeader/types.ts | 78 +++++++ .../src/components/AppHeader/utils.ts | 33 +++ packages/smarthr-ui/src/index.ts | 1 + 21 files changed, 1173 insertions(+) create mode 100644 packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/AppHeader.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/common/CommonButton.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/common/Translate.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/desktop/Navigation.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/desktop/ReleaseNotesDropdown.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/desktop/UserInfo.tsx.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/MobileHeader.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/hooks/useLocale.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/hooks/useMediaQuery.ts create mode 100644 packages/smarthr-ui/src/components/AppHeader/hooks/useTranslate.ts create mode 100644 packages/smarthr-ui/src/components/AppHeader/index.ts create mode 100644 packages/smarthr-ui/src/components/AppHeader/multilingualization/index.ts create mode 100644 packages/smarthr-ui/src/components/AppHeader/multilingualization/localeMap.ts create mode 100644 packages/smarthr-ui/src/components/AppHeader/multilingualization/messages.ts create mode 100644 packages/smarthr-ui/src/components/AppHeader/multilingualization/translate.ts create mode 100644 packages/smarthr-ui/src/components/AppHeader/multilingualization/types.ts create mode 100644 packages/smarthr-ui/src/components/AppHeader/types.ts create mode 100644 packages/smarthr-ui/src/components/AppHeader/utils.ts diff --git a/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx b/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx new file mode 100644 index 0000000000..9c8c1cfa30 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx @@ -0,0 +1,147 @@ +import { action } from '@storybook/addon-actions' +import { Meta, StoryObj } from '@storybook/react/*' +import React, { FC, PropsWithChildren } from 'react' + +import { AppHeader } from './AppHeader' + +const CustomLink: FC> = (props) => ( + + {props.children} + +) + +const AdditionalContent: FC = ({ children }) => ( +
{children}
+) + +const meta = { + title: 'Navigation(ナビゲーション)/AppHeader', + component: AppHeader, + args: { + children: children, + appName: '勤怠管理', + tenants: [ + { + id: 'tenant-1', + name: '株式会社テストテナント壱', + }, + { + id: 'tenant-2', + name: '株式会社テストテナント弐', + }, + ], + currentTenantId: 'tenant-1', + onTenantSelect: action('テナント選択'), + schoolUrl: 'https://exmaple.com', + helpPageUrl: 'https://exmaple.com', + locale: { + selectedLocale: 'ja', + onSelectLocale: action('locale'), + }, + userInfo: { + email: 'smarthr@example.com', + empCode: '001', + firstName: '須磨', + lastName: '栄子', + accountUrl: 'https://exmaple.com', + }, + desktopAdditionalContent: desktopAdditionalContent, + navigations: [ + { + children: 'aタグ', + href: 'https://exmaple.com', + }, + { + children: 'カスタムタグ', + elementAs: CustomLink, + to: 'https://exmaple.com', + }, + { + children: 'ボタン', + onClick: action('AppNavボタンクリック'), + }, + { + children: 'ドロップダウン', + childNavigations: [ + { + children: 'aタグ', + href: 'https://exmaple.com', + }, + { + children: 'カスタムタグ', + elementAs: CustomLink, + to: 'https://exmaple.com', + }, + { + children: 'ボタン', + onClick: action('ボタンクリック'), + }, + ], + }, + { + children: 'グループ', + childNavigations: [ + { + title: 'グループ1', + childNavigations: [ + { + children: 'グループ1_アイテム1', + href: 'https://exmaple.com', + current: true, + }, + { + children: 'グループ1_アイテム2', + href: 'https://exmaple.com', + }, + ], + }, + { + title: 'グループ2', + childNavigations: [ + { + children: 'グループ2_アイテム1', + href: 'https://exmaple.com', + }, + { + children: 'グループ2_アイテム2', + href: 'https://exmaple.com', + }, + ], + }, + ], + }, + ], + desktopNavigationAdditionalContent: ( + desktopNavigationAdditionalContent + ), + releaseNote: { + links: [ + { + title: 'リリースノート1', + url: 'https://exmaple.com', + }, + { + title: 'リリースノート2', + url: 'https://exmaple.com', + }, + { + title: 'リリースノート3', + url: 'https://exmaple.com', + }, + ], + indexUrl: 'https://exmaple.com', + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = {} + +export const EnableNew: Story = { + args: { + enableNew: true, + }, +} diff --git a/packages/smarthr-ui/src/components/AppHeader/AppHeader.tsx b/packages/smarthr-ui/src/components/AppHeader/AppHeader.tsx new file mode 100644 index 0000000000..48ea182e2e --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/AppHeader.tsx @@ -0,0 +1,22 @@ +import React, { FC } from 'react' + +import { DesktopHeader } from './components/desktop/DesktopHeader' +import { LocaleContextProvider } from './hooks/useLocale' +import { mediaQuery, useMediaQuery } from './hooks/useMediaQuery' +import { MobileHeader } from './components/mobile/MobileHeader' +import { HeaderProps } from './types' + +export const AppHeader: FC = ({ locale, children, ...props }) => { + // NOTE: ヘッダーの出し分けは CSS によって行われているので、useMediaQuery による children の出し分けは本来不要ですが、 + // wovn の言語切替カスタム UI の挿入対象となる DOM ("wovn-embedded-widget-anchor" クラスを持った div) が複数描画されていると、 + // wovn のスクリプトの仕様上1つ目の DOM にしか UI が挿入されないため、やむを得ず children のみ React のレンダリングレベルでの出し分けをしています。 + const isDesktop = useMediaQuery(mediaQuery.desktop) + const isMobile = useMediaQuery(mediaQuery.mobile) + + return ( + + {isDesktop && children} + {isMobile && children} + + ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/common/CommonButton.tsx b/packages/smarthr-ui/src/components/AppHeader/components/common/CommonButton.tsx new file mode 100644 index 0000000000..587f7885ab --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/common/CommonButton.tsx @@ -0,0 +1,73 @@ +import React, { ComponentPropsWithoutRef, FC, ReactNode } from 'react' +import { tv } from 'tailwind-variants' + +export const commonButton = tv({ + base: [ + '[&&]:shr-flex [&&]:shr-items-center [&&]:shr-w-full [&&]:shr-px-1 [&&]:shr-py-0.5 [&&]:shr-box-border [&&]:shr-bg-transparent [&&]:shr-text-base [&&]:shr-text-black [&&]:shr-leading-normal [&&]:shr-no-underline [&&]:shr-rounded-m [&&]:shr-cursor-pointer [&&]:shr-border-none', + '[&&]:hover:shr-bg-white-darken', + '[&&]:focus-visible:shr-bg-white-darken', + ], + variants: { + prefix: { + true: ['[&&]:shr-gap-0.5'], + }, + current: { + true: ['[&&]:shr-bg-white-darken'], + }, + boldWhenCurrent: { + true: null, + false: ['[&&]:shr-font-normal'], + }, + }, + compoundVariants: [ + { + boldWhenCurrent: true, + current: true, + className: ['[&&]:shr-font-bold'], + }, + ], +}) + +type AnchorProps = Omit, 'prefix'> +type ButtonProps = Omit, 'prefix'> + +type Props = (({ elementAs: 'a' } & AnchorProps) | ({ elementAs: 'button' } & ButtonProps)) & { + prefix?: ReactNode + current?: boolean + boldWhenCurrent?: boolean +} + +export const CommonButton: FC = ({ + elementAs, + prefix, + current, + boldWhenCurrent, + className, + ...props +}) => { + const commonButtonStyle = commonButton({ + prefix: Boolean(prefix), + current, + boldWhenCurrent, + className, + }) + + if (elementAs === 'a') { + return ( + + {prefix} + {props.children} + + ) + } else if (elementAs === 'button') { + return ( + // eslint-disable-next-line smarthr/best-practice-for-button-element + + ) + } else { + throw new Error(elementAs satisfies never) + } +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/common/Translate.tsx b/packages/smarthr-ui/src/components/AppHeader/components/common/Translate.tsx new file mode 100644 index 0000000000..1c8da968e9 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/common/Translate.tsx @@ -0,0 +1,5 @@ +import React, { PropsWithChildren, memo } from 'react' + +export const Translate = memo(({ children }) => ( + {children} +)) diff --git a/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx b/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx new file mode 100644 index 0000000000..7c08c45681 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx @@ -0,0 +1,102 @@ +import React, { FC } from 'react' + +import { Header, HeaderLink, LanguageSwitcher } from '../../../Header' +import { FaCircleQuestionIcon, FaGraduationCapIcon, FaRegCircleQuestionIcon } from '../../../Icon' +import { Cluster } from '../../../Layout' +import { useLocale } from '../../hooks/useLocale' +import { useTranslate } from '../../hooks/useTranslate' +import { localeMap } from '../../multilingualization' +import { HeaderProps } from '../../types' +import { Translate } from '../common/Translate' + +import { Navigation } from './Navigation' +import { UserInfo } from './UserInfo.tsx' + +export const DesktopHeader: FC = ({ + enableNew, + className = '', + appName, + tenants, + currentTenantId, + schoolUrl, + helpPageUrl, + children, + userInfo, + desktopAdditionalContent, + navigations, + desktopNavigationAdditionalContent, + releaseNote, + ...props +}) => { + const translate = useTranslate() + const { locale } = useLocale() + + return ( + <> +
+ + {!enableNew && schoolUrl && ( + } + className="shr-flex shr-items-center shr-py-0.75 shr-leading-none" + > + {translate('common/school')} + + )} + + {helpPageUrl && ( + : } + className={ + enableNew ? undefined : 'shr-flex shr-items-center shr-py-0.75 shr-leading-none' + } + enableNew={enableNew} + > + {translate('common/help')} + + )} + + {locale && ( + void} + enableNew={enableNew} + /> + )} + + {children} + + {userInfo && ( + + )} + +
+ + {navigations && ( + + )} + + ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/desktop/Navigation.tsx b/packages/smarthr-ui/src/components/AppHeader/components/desktop/Navigation.tsx new file mode 100644 index 0000000000..c5c38d6e3d --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/desktop/Navigation.tsx @@ -0,0 +1,144 @@ +import React, { ComponentProps, FC, Fragment, ReactNode } from 'react' +import { tv } from 'tailwind-variants' + +import { AppNavi } from '../../../AppNavi' +import { Cluster } from '../../../Layout' +import { Text } from '../../../Text' +import { ChildNavigation, Navigation as NavigationType, ReleaseNoteProps } from '../../types' +import { isChildNavigation, isChildNavigationGroup } from '../../utils' +import { CommonButton, commonButton } from '../common/CommonButton' + +import { ReleaseNotesDropdown } from './ReleaseNotesDropdown' + +const appNavi = tv({ + base: ['shr-overflow-x-auto shr-min-w-[auto]', 'max-[751px]:!shr-hidden'], + variants: { + withReleaseNote: { + true: ['[&&]:shr-pe-0'], + }, + }, +}) + +type Props = { + appName: ReactNode + navigations: NavigationType[] + additionalContent: ReactNode + releaseNote?: ReleaseNoteProps | null + enableNew?: boolean +} + +export const Navigation: FC = ({ + appName, + navigations, + additionalContent, + releaseNote, + enableNew, +}) => { + const buttons = buildButtonsFromNavigations(navigations) + + return ( + + {additionalContent} + {releaseNote && } + + } + /> + ) +} + +const navigationTitle = tv({ + base: ['shr-px-1 shr-pt-0.5 shr-pb-0.25'], +}) + +const separator = tv({ + base: ['[&&]:shr-mx-0 [&&]:shr-my-0.5 [&&]:shr-border-b-shorthand'], +}) + +// TODO smarthr-ui 側でグループ化された Navigation が対応されたら AppNaviDropdownMenuButton を使った実装に変更する +const buildButtonsFromNavigations = ( + navigations: NavigationType[], +): ComponentProps['buttons'] => + navigations.map((navigation) => { + if (isChildNavigation(navigation)) { + // smarthr-ui の buttons props ではカスタムエレメントは elementAs ではなく tag という名前なので変換する必要がある + if ('elementAs' in navigation) { + const { elementAs, ...rest } = navigation + return { + ...rest, + tag: elementAs, + } + } + return navigation + } + + // 子要素に current を持っているものがあるかどうか + const childrenHasCurrent = navigation.childNavigations.some((child) => { + if (isChildNavigation(child)) return child.current + return child.childNavigations.some((c) => c.current) + }) + + return { + ...navigation, + current: navigation.current || childrenHasCurrent, + dropdownContent: ( +
+ {navigation.childNavigations.map((childNavigation, i) => { + if (isChildNavigationGroup(childNavigation)) { + const { childNavigations } = childNavigation + + return ( + +
+ + {childNavigation.title} + + + {childNavigations.map((child) => ( + + {buildDropdownItemFromNavigation(child)} + + ))} +
+ + {i + 1 !== navigation.childNavigations.length &&
} +
+ ) + } + + const nextChildNavigation = navigation.childNavigations[i + 1] + + return ( + +
{buildDropdownItemFromNavigation(childNavigation)}
+ {isChildNavigationGroup(nextChildNavigation) &&
} +
+ ) + })} +
+ ), + } + }) + +const buildDropdownItemFromNavigation = (navigation: ChildNavigation) => { + if ('elementAs' in navigation) { + const { elementAs: Tag, current: isCurrent, ...rest } = navigation + + return ( + + ) + } + + if ('href' in navigation) { + return + } + + return ( + + ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/desktop/ReleaseNotesDropdown.tsx b/packages/smarthr-ui/src/components/AppHeader/components/desktop/ReleaseNotesDropdown.tsx new file mode 100644 index 0000000000..326e8d4cc7 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/desktop/ReleaseNotesDropdown.tsx @@ -0,0 +1,85 @@ +import React, { FC } from 'react' +import { tv } from 'tailwind-variants' + +import { Button } from '../../../Button' +import { Dropdown, DropdownContent, DropdownTrigger } from '../../../Dropdown' +import { FaCaretDownIcon } from '../../../Icon' +import { Center } from '../../../Layout' +import { Loader } from '../../../Loader' +import { Text } from '../../../Text' +import { TextLink } from '../../../TextLink' +import { useTranslate } from '../../hooks/useTranslate' +import { ReleaseNoteProps } from '../../types' +import { Translate } from '../common/Translate' + +const wrapper = tv({ + base: 'shr-w-[400px]', + variants: { + type: { + content: '', + }, + }, +}) + +export const ReleaseNotesDropdown: FC = ({ indexUrl, links, loading, error }) => { + const translate = useTranslate() + + return ( +
+ + + + + + +
+ {loading ? ( +
+ +
+ ) : error || !links ? ( +
+ + {translate('common/releaseNotesLoadError')} + +
+ ) : ( +
+ {links.slice(0, 5).map(({ title, url }, index) => ( +
+ + {title} + +
+ ))} + +
+ + {translate('common/seeAllReleaseNotes')} + +
+
+ )} +
+
+
+
+ ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/desktop/UserInfo.tsx.tsx b/packages/smarthr-ui/src/components/AppHeader/components/desktop/UserInfo.tsx.tsx new file mode 100644 index 0000000000..5d046d288b --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/desktop/UserInfo.tsx.tsx @@ -0,0 +1,210 @@ +import React, { FC } from 'react' +import { tv } from 'tailwind-variants' + +import { AnchorButton, Button } from '../../../Button' +import { Dropdown, DropdownContent, DropdownMenuButton, DropdownTrigger } from '../../../Dropdown' +import { FaCaretDownIcon, FaGearIcon, FaUserIcon } from '../../../Icon' +import { Cluster, Stack } from '../../../Layout' +import { Text } from '../../../Text' +import { useTranslate } from '../../hooks/useTranslate' +import { HeaderProps, UserInfoProps } from '../../types' +import { buildDisplayName } from '../../utils' +import { CommonButton } from '../common/CommonButton' +import { Translate } from '../common/Translate' + +// HeaderDropdownMenuButton と同じスタイルを適用 +const userInfo = tv({ + slots: { + userSummary: [ + 'shr-relative -shr-mt-0.5 -shr-mx-0.25 shr-p-1', + // FIXME: smarthr-ui で DropdownMenuButton のグルーピングができるようになったら修正しましょう + 'after:shr-absolute after:shr-content-[""] after:shr-block after:shr-inset-x-0.5 after:shr-bottom-0 after:shr-h-px after:shr-bg-border', + ], + dropdownMenuButton: [ + '[&_.smarthr-ui-DropdownMenuButton-trigger]:shr-border-transparent [&_.smarthr-ui-DropdownMenuButton-trigger]:shr-px-0.5 [&_.smarthr-ui-DropdownMenuButton-trigger]:shr-font-normal', + ], + dropdownContentButton: [ + '[&&.smarthr-ui-AnchorButton]:shr-p-0.75 [&&.smarthr-ui-AnchorButton]:shr-py-0.75', + '[&&.smarthr-ui-Button]:shr-p-0.75 [&&.smarthr-ui-Button]:shr-py-0.75', + ], + button: [ + '[&&]:shr-border-transparent [&&]:shr-font-normal [&&]:last-of-type:-shr-me-0.25', + '[&&]:focus-visible:shr-bg-transparent', + "[&[aria-expanded='true']>.smarthr-ui-Icon:last-child]:shr-rotate-180", + ], + dropdownContent: '[&&]:shr-whitespace-pre [&&]:shr-p-0.5', + accountImage: '', + placeholderImage: 'shr-p-0.5', + }, + variants: { + enableNew: { + true: { + button: '[&&]:shr-px-0.5', + }, + false: { + button: + '[&&]:shr-bg-transparent [&&]:hover:shr-bg-transparent [&&]:shr-px-0.25 [&&]:shr-text-white', + }, + }, + }, + compoundSlots: [ + { + slots: ['accountImage', 'placeholderImage'], + className: + 'shr-box-border shr-flex shr-items-center shr-justify-center -shr-my-1 shr-border-shorthand shr-rounded-full shr-bg-white shr-size-2', + }, + ], +}) + +export const UserInfo: FC< + UserInfoProps & Pick +> = ({ + arbitraryDisplayName, + email, + empCode, + firstName, + lastName, + tenants, + currentTenantId, + accountUrl, + accountImageUrl, + desktopAdditionalContent, + enableNew, +}) => { + const translate = useTranslate() + + const displayName = + arbitraryDisplayName ?? + buildDisplayName({ + email, + empCode, + firstName, + lastName, + }) + + if (!displayName) { + return null + } + + if (!accountUrl && !desktopAdditionalContent) { + return {displayName} + } + + const currentTenantName = tenants?.find((tenant) => tenant.id === currentTenantId)?.name + const { + userSummary, + dropdownMenuButton, + dropdownContentButton, + button, + dropdownContent, + accountImage, + placeholderImage, + } = userInfo({ + enableNew, + }) + + if (enableNew) { + return ( + + {accountImageUrl ? ( + // eslint-disable-next-line smarthr/a11y-image-has-alt-attribute, jsx-a11y/alt-text + + ) : ( + + + + )} + + + {/* eslint-disable-next-line smarthr/best-practice-for-layouts */} + + {currentTenantName && ( + + {currentTenantName} + + )} + + {firstName && lastName ? ( + + {firstName} {lastName} + + ) : ( + email && ( + + {email} + + ) + )} + + + + } + > + + + {currentTenantName} + + + {empCode || (firstName && lastName) ? ( + + {empCode && ( + + {empCode} + + )} + {firstName && lastName && {`${lastName} ${firstName}`}} + + ) : ( + {email} + )} + + + {accountUrl && ( + } + className={dropdownContentButton()} + > + {translate('common/userSetting')} + + )} + + {desktopAdditionalContent} + + ) + } + + return ( + + + + + + + {/* eslint-disable-next-line smarthr/best-practice-for-layouts */} + + {accountUrl && ( + } + > + {translate('common/userSetting')} + + )} + + {desktopAdditionalContent} + + + + ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/MobileHeader.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MobileHeader.tsx new file mode 100644 index 0000000000..27821c31e2 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MobileHeader.tsx @@ -0,0 +1,7 @@ +import React, { FC } from 'react' + +import { HeaderProps } from '../../types' + +export const MobileHeader: FC = ({ children, className = '', ...props }) => ( +
MobileHeader
+) diff --git a/packages/smarthr-ui/src/components/AppHeader/hooks/useLocale.tsx b/packages/smarthr-ui/src/components/AppHeader/hooks/useLocale.tsx new file mode 100644 index 0000000000..15bc2edbe3 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/hooks/useLocale.tsx @@ -0,0 +1,18 @@ +import React, { FC, ReactNode, createContext, useContext } from 'react' + +import { HeaderProps } from '../types' + +const LocaleContext = createContext<{ locale: HeaderProps['locale'] }>({ + locale: null, +}) + +type LocaleContextProviderProps = { + locale: HeaderProps['locale'] + children: ReactNode +} + +export const LocaleContextProvider: FC = ({ locale, children }) => ( + {children} +) + +export const useLocale = () => useContext(LocaleContext) diff --git a/packages/smarthr-ui/src/components/AppHeader/hooks/useMediaQuery.ts b/packages/smarthr-ui/src/components/AppHeader/hooks/useMediaQuery.ts new file mode 100644 index 0000000000..5cf230d739 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/hooks/useMediaQuery.ts @@ -0,0 +1,29 @@ +import { useCallback, useMemo, useSyncExternalStore } from 'react' + +export const mediaQuery = { + desktop: 'min-width: 752px', + mobile: 'max-width: 751px', +} as const + +export const useMediaQuery = (query: string) => { + const mediaQueryList = useMemo( + () => (typeof window === 'undefined' ? null : matchMedia(`(${query})`)), + [query], + ) + + const subscribe = useCallback( + (callback: () => void) => { + mediaQueryList?.addEventListener('change', callback) + return () => { + mediaQueryList?.removeEventListener('change', callback) + } + }, + [mediaQueryList], + ) + + return useSyncExternalStore( + subscribe, + () => mediaQueryList?.matches ?? false, + () => false, + ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/hooks/useTranslate.ts b/packages/smarthr-ui/src/components/AppHeader/hooks/useTranslate.ts new file mode 100644 index 0000000000..fdc2255de2 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/hooks/useTranslate.ts @@ -0,0 +1,15 @@ +import { useCallback } from 'react' + +import { translate } from '../multilingualization' +import { Messages } from '../multilingualization/messages' + +import { useLocale } from './useLocale' + +export const useTranslate = () => { + const { locale } = useLocale() + + return useCallback( + (id: ID) => translate(id, locale?.selectedLocale), + [locale?.selectedLocale], + ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/index.ts b/packages/smarthr-ui/src/components/AppHeader/index.ts new file mode 100644 index 0000000000..8af702b365 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/index.ts @@ -0,0 +1 @@ +export { AppHeader } from './AppHeader' diff --git a/packages/smarthr-ui/src/components/AppHeader/multilingualization/index.ts b/packages/smarthr-ui/src/components/AppHeader/multilingualization/index.ts new file mode 100644 index 0000000000..8e509207c3 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/multilingualization/index.ts @@ -0,0 +1,3 @@ +export { Locale } from './types' +export { translate } from './translate' +export { localeMap } from './localeMap' diff --git a/packages/smarthr-ui/src/components/AppHeader/multilingualization/localeMap.ts b/packages/smarthr-ui/src/components/AppHeader/multilingualization/localeMap.ts new file mode 100644 index 0000000000..f03e3f707e --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/multilingualization/localeMap.ts @@ -0,0 +1,10 @@ +export const localeMap = { + ja: '日本語', + 'id-id': 'Bahasa Indonesia', + 'en-us': 'English', + pt: 'Português', + vi: 'Tiếng Việt', + ko: '한국어', + 'zh-cn': '简体中文', + 'zh-tw': '繁體中文', +} as const diff --git a/packages/smarthr-ui/src/components/AppHeader/multilingualization/messages.ts b/packages/smarthr-ui/src/components/AppHeader/multilingualization/messages.ts new file mode 100644 index 0000000000..017babf57c --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/multilingualization/messages.ts @@ -0,0 +1,179 @@ +import { Locale } from './types' + +export type Messages = { + 'common/school': string + 'common/help': string + 'common/userSetting': string + 'common/releaseNote': string + 'common/releaseNotesLoadError': string + 'common/seeAllReleaseNotes': string + 'DesktopHeader/DesktopHeader/appLauncherLabel': string + 'MobileHeader/UserInfo/account': string + 'MobileHeader/Menu/openMenu': string + 'MobileHeader/Menu/closeMenu': string + 'MobileHeader/Menu/allAppButton': string + 'MobileHeader/Menu/managementMenu': string + 'MobileHeader/Menu/appList': string + 'MobileHeader/Menu/latestReleaseNotes': string + 'MobileHeader/MenuSubHeader/back': string + 'MobileHeader/MenuAccordion/open': string + 'MobileHeader/MenuAccordion/close': string +} + +export const translation = { + ja: { + 'common/school': 'スクール', + 'common/help': 'ヘルプ', + 'common/userSetting': '個人設定', + 'common/releaseNote': 'リリースノート', + 'common/releaseNotesLoadError': + 'リリースノートの読み込みに失敗しました。\n時間をおいて、やり直してください。', + 'common/seeAllReleaseNotes': 'すべてのリリースノートを見る', + 'DesktopHeader/DesktopHeader/appLauncherLabel': 'アプリ', + 'MobileHeader/UserInfo/account': 'アカウント', + 'MobileHeader/Menu/openMenu': 'メニューを開く', + 'MobileHeader/Menu/closeMenu': 'メニューを閉じる', + 'MobileHeader/Menu/allAppButton': 'すべてのアプリ', + 'MobileHeader/Menu/managementMenu': '管理メニュー', + 'MobileHeader/Menu/appList': 'アプリ一覧', + 'MobileHeader/Menu/latestReleaseNotes': '最新のリリースノート', + 'MobileHeader/MenuSubHeader/back': '戻る', + 'MobileHeader/MenuAccordion/open': '開く', + 'MobileHeader/MenuAccordion/close': '閉じる', + }, + 'id-id': { + 'common/school': 'Sekolah', + 'common/help': 'Bantuan', + 'common/userSetting': 'Personalisasi', + 'common/releaseNote': 'Release Note', + 'common/releaseNotesLoadError': 'Gagal memuat Release Note. \nSilakan coba lagi setelah jam.', + 'common/seeAllReleaseNotes': 'Lihat semua Release Note', + 'DesktopHeader/DesktopHeader/appLauncherLabel': 'Aplikasi', + 'MobileHeader/UserInfo/account': 'Akun', + 'MobileHeader/Menu/openMenu': 'Buka menu', + 'MobileHeader/Menu/closeMenu': 'Tutup menu', + 'MobileHeader/Menu/allAppButton': 'Semua aplikasi', + 'MobileHeader/Menu/managementMenu': 'Menu pengelolaan', + 'MobileHeader/Menu/appList': 'Daftar aplikasi', + 'MobileHeader/Menu/latestReleaseNotes': 'Release Note terkini', + 'MobileHeader/MenuSubHeader/back': 'Kembali', + 'MobileHeader/MenuAccordion/open': 'Buka', + 'MobileHeader/MenuAccordion/close': 'Tutup', + }, + 'en-us': { + 'common/school': 'School', + 'common/help': 'Help', + 'common/userSetting': 'Personal Settings', + 'common/releaseNote': 'Release notes', + 'common/releaseNotesLoadError': 'Failed to load release notes.\nTry again later.', + 'common/seeAllReleaseNotes': 'See all release notes', + 'DesktopHeader/DesktopHeader/appLauncherLabel': 'Apps', + 'MobileHeader/UserInfo/account': 'Account', + 'MobileHeader/Menu/openMenu': 'Open menu', + 'MobileHeader/Menu/closeMenu': 'Close menu', + 'MobileHeader/Menu/allAppButton': 'All apps', + 'MobileHeader/Menu/managementMenu': 'Admin Menu', + 'MobileHeader/Menu/appList': 'App list', + 'MobileHeader/Menu/latestReleaseNotes': 'Latest release notes', + 'MobileHeader/MenuSubHeader/back': 'Back', + 'MobileHeader/MenuAccordion/open': 'Expand', + 'MobileHeader/MenuAccordion/close': 'Collapse', + }, + pt: { + 'common/school': 'Escola', + 'common/help': 'Ajuda', + 'common/userSetting': 'Configuração pessoal', + 'common/releaseNote': 'Notas de versão', + 'common/releaseNotesLoadError': + 'Não foi possível carregar as notas de versão. \nPor favor, tente novamente mais tarde.', + 'common/seeAllReleaseNotes': 'Ver todas as notas de versão', + 'DesktopHeader/DesktopHeader/appLauncherLabel': 'Apps', + 'MobileHeader/UserInfo/account': 'Conta', + 'MobileHeader/Menu/openMenu': 'Abrir menu', + 'MobileHeader/Menu/closeMenu': 'Fechar menu', + 'MobileHeader/Menu/allAppButton': 'Todos os Apps', + 'MobileHeader/Menu/managementMenu': 'Menu de administração', + 'MobileHeader/Menu/appList': 'Lista de Apps', + 'MobileHeader/Menu/latestReleaseNotes': 'Notas de versão mais recentes', + 'MobileHeader/MenuSubHeader/back': 'Voltar', + 'MobileHeader/MenuAccordion/open': 'Abrir', + 'MobileHeader/MenuAccordion/close': 'Fechar', + }, + vi: { + 'common/school': 'School', + 'common/help': 'Trợ giúp', + 'common/userSetting': 'Cài đặt cá nhân', + 'common/releaseNote': 'Release Notes', + 'common/releaseNotesLoadError': 'Tải Release Notes thất bại.\nHãy thử lại sau một lúc nữa.', + 'common/seeAllReleaseNotes': 'Xem tất cả Release Notes', + 'DesktopHeader/DesktopHeader/appLauncherLabel': 'Danh mục', + 'MobileHeader/UserInfo/account': 'Tài khoản', + 'MobileHeader/Menu/openMenu': 'Mở menu', + 'MobileHeader/Menu/closeMenu': 'Đóng menu', + 'MobileHeader/Menu/allAppButton': 'Tất cả Tính năng', + 'MobileHeader/Menu/managementMenu': 'Menu Quản lý', + 'MobileHeader/Menu/appList': 'Danh sách các tính năng', + 'MobileHeader/Menu/latestReleaseNotes': 'Ghi chú phát hành mới nhất', + 'MobileHeader/MenuSubHeader/back': 'Quay lại', + 'MobileHeader/MenuAccordion/open': 'Mở', + 'MobileHeader/MenuAccordion/close': 'Đóng', + }, + ko: { + 'common/school': '스쿨', + 'common/help': '도움말', + 'common/userSetting': '개인 설정', + 'common/releaseNote': '리리스 노트', + 'common/releaseNotesLoadError': + '리리스노트의 불러오기를 실패했습니다.\n시간을 두고 다시 시도해 주세요.', + 'common/seeAllReleaseNotes': '모든 리리스 노트를 보기', + 'DesktopHeader/DesktopHeader/appLauncherLabel': '앱', + 'MobileHeader/UserInfo/account': '어카운트', + 'MobileHeader/Menu/openMenu': '메뉴를 열기', + 'MobileHeader/Menu/closeMenu': '메뉴를 닫기', + 'MobileHeader/Menu/allAppButton': '모든 앱', + 'MobileHeader/Menu/managementMenu': '관리메뉴', + 'MobileHeader/Menu/appList': '앱 리스트', + 'MobileHeader/Menu/latestReleaseNotes': '최신 리리스 노트', + 'MobileHeader/MenuSubHeader/back': '돌아가기', + 'MobileHeader/MenuAccordion/open': '열기', + 'MobileHeader/MenuAccordion/close': '닫기', + }, + 'zh-cn': { + 'common/school': '学校', + 'common/help': '帮助', + 'common/userSetting': '个人设置', + 'common/releaseNote': '版本说明', + 'common/releaseNotesLoadError': '无法取得版本说明。\n请稍等片刻后再试。', + 'common/seeAllReleaseNotes': '查看全部版本说明', + 'DesktopHeader/DesktopHeader/appLauncherLabel': '应用程序', + 'MobileHeader/UserInfo/account': '账号', + 'MobileHeader/Menu/openMenu': '展开菜单', + 'MobileHeader/Menu/closeMenu': '关闭菜单', + 'MobileHeader/Menu/allAppButton': '所有功能', + 'MobileHeader/Menu/managementMenu': '管理菜单', + 'MobileHeader/Menu/appList': '功能一览表', + 'MobileHeader/Menu/latestReleaseNotes': '最新版本说明', + 'MobileHeader/MenuSubHeader/back': '返回', + 'MobileHeader/MenuAccordion/open': '展开', + 'MobileHeader/MenuAccordion/close': '关闭', + }, + 'zh-tw': { + 'common/school': '學校', + 'common/help': '幫助', + 'common/userSetting': '個人設定', + 'common/releaseNote': '版本說明', + 'common/releaseNotesLoadError': '載入版本說明失敗。\n請稍等片刻後再試。', + 'common/seeAllReleaseNotes': '查看全部版本說明', + 'DesktopHeader/DesktopHeader/appLauncherLabel': '應用程式', + 'MobileHeader/UserInfo/account': '帳戶', + 'MobileHeader/Menu/openMenu': '展開選單', + 'MobileHeader/Menu/closeMenu': '關閉選單', + 'MobileHeader/Menu/allAppButton': '所有功能', + 'MobileHeader/Menu/managementMenu': '管理選單', + 'MobileHeader/Menu/appList': '功能一覽表', + 'MobileHeader/Menu/latestReleaseNotes': '最新版本說明', + 'MobileHeader/MenuSubHeader/back': '返回', + 'MobileHeader/MenuAccordion/open': '展開', + 'MobileHeader/MenuAccordion/close': '關閉', + }, +} as const satisfies Record diff --git a/packages/smarthr-ui/src/components/AppHeader/multilingualization/translate.ts b/packages/smarthr-ui/src/components/AppHeader/multilingualization/translate.ts new file mode 100644 index 0000000000..c3f47404a0 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/multilingualization/translate.ts @@ -0,0 +1,7 @@ +import { Messages, translation } from './messages' +import { DEFAULT_LOCALE, Locale } from './types' + +export const translate = (id: ID, locale?: Locale | null) => + translation[locale ?? DEFAULT_LOCALE][ + id + ] as (typeof translation)[typeof DEFAULT_LOCALE][typeof id] diff --git a/packages/smarthr-ui/src/components/AppHeader/multilingualization/types.ts b/packages/smarthr-ui/src/components/AppHeader/multilingualization/types.ts new file mode 100644 index 0000000000..adcd1c83f0 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/multilingualization/types.ts @@ -0,0 +1,4 @@ +import { localeMap } from './localeMap' +export const DEFAULT_LOCALE = 'ja' + +export type Locale = keyof typeof localeMap diff --git a/packages/smarthr-ui/src/components/AppHeader/types.ts b/packages/smarthr-ui/src/components/AppHeader/types.ts new file mode 100644 index 0000000000..4eab348f35 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/types.ts @@ -0,0 +1,78 @@ +import { ComponentProps, ComponentType, MouseEvent, ReactElement, ReactNode } from 'react' + +import { Header } from '../Header' + +type Locale = 'ja' | 'en-us' | 'id-id' | 'pt' | 'vi' | 'ko' | 'zh-cn' | 'zh-tw' + +export type LocaleProps = { + selectedLocale: Locale + onSelectLocale: (locale: Locale) => void +} + +export type UserInfoProps = { + /** @deprecated 書式の統一のために、可能な限り使用しないでください */ + arbitraryDisplayName?: string | null + email?: string | null + empCode?: string | null + firstName?: string | null + lastName?: string | null + accountUrl?: string | null + accountImageUrl?: string + enableNew?: boolean +} + +export type HeaderProps = ComponentProps & { + locale?: LocaleProps | null + enableNew?: boolean + appName?: ReactNode + schoolUrl?: string | null + helpPageUrl?: string | null + userInfo?: UserInfoProps | null + desktopAdditionalContent?: ReactNode + navigations?: Navigation[] | null + desktopNavigationAdditionalContent?: ReactNode + releaseNote?: ReleaseNoteProps | null +} + +export type Navigation = NavigationLink | NavigationCustomTag | NavigationButton | NavigationGroup + +type NavigationLink = { + children: ReactElement | string + href: string + current?: boolean +} + +type NavigationCustomTag = { + children: ReactElement | string + elementAs: ComponentType + current?: boolean +} & { [key: string]: any } + +type NavigationButton = { + children: ReactElement | string + onClick: (e: MouseEvent) => void + current?: boolean +} + +type NavigationGroup = { + children: ReactElement | string + childNavigations: Array + current?: boolean +} + +export type ChildNavigationGroup = { + title: ReactElement | string + childNavigations: ChildNavigation[] +} + +export type ChildNavigation = NavigationLink | NavigationCustomTag | NavigationButton + +export type ReleaseNoteProps = { + indexUrl: string + links: Array<{ + title: string + url: string + }> + loading?: boolean | null + error?: boolean | null +} diff --git a/packages/smarthr-ui/src/components/AppHeader/utils.ts b/packages/smarthr-ui/src/components/AppHeader/utils.ts new file mode 100644 index 0000000000..17c2556e16 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/utils.ts @@ -0,0 +1,33 @@ +import { ChildNavigation, ChildNavigationGroup, Navigation } from './types' + +export const buildDisplayName = ({ + email, + empCode, + firstName, + lastName, +}: { + email?: string | null + empCode?: string | null + firstName?: string | null + lastName?: string | null +}) => { + const empCodeStr = empCode ? `(${empCode})` : '' + + return ( + (firstName && lastName ? `${lastName} ${firstName}` + empCodeStr : empCode ? empCode : email) ?? + '' + ) +} + +export const isChildNavigation = ( + navigation: Navigation | ChildNavigationGroup, +): navigation is ChildNavigation => + 'href' in navigation || 'elementAs' in navigation || 'onClick' in navigation + +export const isChildNavigationGroup = ( + navigation: Navigation | ChildNavigationGroup, +): navigation is ChildNavigationGroup => + navigation && + 'childNavigations' in navigation && + 'title' in navigation && + !('elementAs' in navigation) diff --git a/packages/smarthr-ui/src/index.ts b/packages/smarthr-ui/src/index.ts index b3cd1d4834..f26904b8d8 100644 --- a/packages/smarthr-ui/src/index.ts +++ b/packages/smarthr-ui/src/index.ts @@ -91,6 +91,7 @@ export * from './components/Switch' export * from './components/Stepper' export * from './components/Picker' export * from './components/Browser' +export { AppHeader } from './components/AppHeader' // layout components export { Center, Cluster, Reel, Stack, Sidebar } from './components/Layout'