From 4cb2bd200dfa5e23e3085bc8050d936028e73119 Mon Sep 17 00:00:00 2001 From: nabeliwo Date: Tue, 17 Dec 2024 20:52:47 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=E3=83=87=E3=82=B9=E3=82=AF?= =?UTF-8?q?=E3=83=88=E3=83=83=E3=83=97=E3=82=B5=E3=82=A4=E3=82=BA=E3=81=AE?= =?UTF-8?q?=20AppHeader=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 3490654c0f..561609c182 100644 --- a/packages/smarthr-ui/src/index.ts +++ b/packages/smarthr-ui/src/index.ts @@ -92,6 +92,7 @@ export * from './components/Stepper' export * from './components/Picker' export * from './components/Browser' export * from './components/WarekiPicker' +export { AppHeader } from './components/AppHeader' // layout components export { Center, Cluster, Reel, Stack, Sidebar } from './components/Layout' From 3de03bf856387c7a9937e70b630abf7b8de1c3a5 Mon Sep 17 00:00:00 2001 From: nabeliwo Date: Wed, 18 Dec 2024 15:05:02 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=E3=83=A2=E3=83=90=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=B5=E3=82=A4=E3=82=BA=E3=81=AE=20AppHeader=20?= =?UTF-8?q?=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 | 1 + .../AppHeader/components/mobile/Help.tsx | 59 +++++++ .../components/mobile/LanguageSelector.tsx | 68 +++++++ .../AppHeader/components/mobile/Menu.tsx | 121 +++++++++++++ .../components/mobile/MenuAccordion.tsx | 55 ++++++ .../components/mobile/MenuButton.tsx | 23 +++ .../components/mobile/MenuDialog.tsx | 167 ++++++++++++++++++ .../components/mobile/MenuSubHeader.tsx | 29 +++ .../components/mobile/MobileHeader.tsx | 85 ++++++++- .../components/mobile/Navigation.tsx | 54 ++++++ .../components/mobile/NavigationContext.tsx | 9 + .../mobile/NavigationGroupHeader.tsx | 19 ++ .../components/mobile/NavigationItem.tsx | 93 ++++++++++ .../components/mobile/ReleaseNote.tsx | 71 ++++++++ .../components/mobile/ReleaseNoteContext.tsx | 13 ++ .../components/mobile/TenantSelector.tsx | 70 ++++++++ .../AppHeader/components/mobile/UserInfo.tsx | 115 ++++++++++++ .../src/components/AppHeader/types.ts | 3 +- 18 files changed, 1049 insertions(+), 6 deletions(-) create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/Help.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/LanguageSelector.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/Menu.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuAccordion.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuButton.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuDialog.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuSubHeader.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/Navigation.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/NavigationContext.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/NavigationGroupHeader.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/NavigationItem.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/ReleaseNote.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/ReleaseNoteContext.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/TenantSelector.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/UserInfo.tsx diff --git a/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx b/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx index 9c8c1cfa30..90667bc1c0 100644 --- a/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx @@ -131,6 +131,7 @@ const meta = { ], indexUrl: 'https://exmaple.com', }, + mobileAdditionalContent: mobileAdditionalContent, }, } satisfies Meta diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/Help.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/Help.tsx new file mode 100644 index 0000000000..c885edb820 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/Help.tsx @@ -0,0 +1,59 @@ +import React, { FC } from 'react' + +import { Button } from '../../../Button' +import { Dropdown, DropdownContent, DropdownTrigger } from '../../../Dropdown' +import { FaCircleQuestionIcon, FaGraduationCapIcon } from '../../../Icon' +import { useTranslate } from '../../hooks/useTranslate' +import { CommonButton } from '../common/CommonButton' +import { Translate } from '../common/Translate' + +type Props = { + helpPageUrl?: string | null + schoolUrl?: string | null +} + +export const Help: FC = ({ helpPageUrl, schoolUrl }) => { + const translate = useTranslate() + + if (!helpPageUrl && !schoolUrl) { + return null + } + + return ( + + + + + + +
+ {helpPageUrl && ( + } + > + {translate('common/help')} + + )} + + {schoolUrl && ( + } + > + {translate('common/school')} + + )} +
+
+
+ ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/LanguageSelector.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/LanguageSelector.tsx new file mode 100644 index 0000000000..ef2fb99748 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/LanguageSelector.tsx @@ -0,0 +1,68 @@ +import React, { FC } from 'react' +import { tv } from 'tailwind-variants' + +import { Button } from '../../../Button' +import { Heading } from '../../../Heading' +import { FaCheckIcon, FaXmarkIcon } from '../../../Icon' +import { Section } from '../../../SectioningContent' +import { Locale, localeMap } from '../../multilingualization' +import { LocaleProps } from '../../types' +import { CommonButton } from '../common/CommonButton' + +const languageSelector = tv({ + slots: { + header: [ + 'shr-flex shr-justify-between shr-gap-1 shr-items-center shr-px-1 shr-py-0.75 shr-border-b-shorthand', + ], + headerTitle: ['[&&]:shr-text-base shr-font-normal'], + buttonWrapper: ['shr-p-0.5'], + button: ['[&&:not(:has(svg))]:shr-ps-2.5'], + }, +}) + +type Props = { + locale: LocaleProps + onClickClose: (isOpen: boolean) => void +} + +export const LanguageSelector: FC = ({ locale, onClickClose }) => { + const { header, headerTitle, buttonWrapper, button } = languageSelector() + + const onClickButton = (selectedLocale: Locale) => { + locale.onSelectLocale(selectedLocale) + } + + return ( +
+
+ Language + + +
+ +
+ {Object.keys(localeMap).map((localeKey) => ( + onClickButton(localeKey as Locale)} + prefix={localeKey === locale.selectedLocale && } + > + {localeMap[localeKey as Locale]} + + ))} +
+
+ ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/Menu.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/Menu.tsx new file mode 100644 index 0000000000..0ebd70a367 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/Menu.tsx @@ -0,0 +1,121 @@ +import React, { FC, ReactNode, useContext, useEffect, useState } from 'react' +import { tv } from 'tailwind-variants' + +import { useHandleEscape } from '../../../../hooks/useHandleEscape' +import { usePortal } from '../../../../hooks/usePortal' +import { Button } from '../../../Button' +import { FaBarsIcon } from '../../../Icon' +import { useTranslate } from '../../hooks/useTranslate' +import { Translate } from '../common/Translate' + +import { MenuAccordion } from './MenuAccordion' +import { MenuButton } from './MenuButton' +import { MenuDialog } from './MenuDialog' +import { Navigation } from './Navigation' +import { NavigationContext } from './NavigationContext' +import { ReleaseNoteContext } from './ReleaseNoteContext' + +const menuItemBlock = tv({ + base: ['shr-border-t-shorthand shr-py-1', 'first:shr-border-t-0 first:shr-pt-0'], +}) + +let scrollPosition = 0 + +type Props = { + appName: ReactNode + tenantSelector: ReactNode + additionalContent: ReactNode +} + +export const Menu: FC = ({ appName, tenantSelector, additionalContent }) => { + const [isOpen, setIsOpen] = useState(false) + const [isNavigationOpen, setIsNavigationOpen] = useState(true) + const [isAdditionalContentOpen, setIsAdditionalContentOpen] = useState(true) + + const { navigations } = useContext(NavigationContext) + const { releaseNote, setIsReleaseNoteSelected } = useContext(ReleaseNoteContext) + // const { appLauncher, setIsAppLauncherSelected } = useContext(AppLauncherContext) + + const translate = useTranslate() + const { createPortal } = usePortal() + + useEffect(() => { + if (isOpen) { + scrollPosition = window.scrollY + document.body.style.overflow = 'hidden' + } else { + document.body.style.overflow = 'auto' + setTimeout(() => window.scrollTo(0, scrollPosition), 0) + } + }, [isOpen]) + + useHandleEscape(() => setIsOpen(false)) + + const menuItemBlockStyle = menuItemBlock() + + return ( + <> + + + {createPortal( + + {/* {appLauncher && ( +
+ +
+ )} */} + + {navigations.length > 0 && appName ? ( +
+ + setIsOpen(false)} /> + +
+ ) : ( +
+ setIsOpen(false)} /> +
+ )} + + {additionalContent && ( +
+ + {additionalContent} + +
+ )} + + {releaseNote && ( +
+ setIsReleaseNoteSelected(true)}> + {translate('common/releaseNote')} + +
+ )} +
, + )} + + ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuAccordion.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuAccordion.tsx new file mode 100644 index 0000000000..a30e8798c7 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuAccordion.tsx @@ -0,0 +1,55 @@ +import React, { Dispatch, FC, PropsWithChildren, ReactNode, useId } from 'react' + +import { Button } from '../../../Button' +import { Heading } from '../../../Heading' +import { FaCaretDownIcon, FaCaretUpIcon } from '../../../Icon' +import { Cluster } from '../../../Layout' +import { Section } from '../../../SectioningContent' +import { useTranslate } from '../../hooks/useTranslate' +import { Translate } from '../common/Translate' + +type Props = { + isOpen: boolean + setIsOpen: Dispatch<(isOpen: boolean) => boolean> + title: ReactNode +} + +export const MenuAccordion: FC> = ({ + isOpen, + setIsOpen, + title, + children, +}) => { + const translate = useTranslate() + const id = useId() + + if (!title) { + return
{children}
+ } + + return ( +
+ + + {title} + + + + + +
{isOpen &&
{children}
}
+
+ ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuButton.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuButton.tsx new file mode 100644 index 0000000000..d50495c3d1 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuButton.tsx @@ -0,0 +1,23 @@ +import React, { FC, PropsWithChildren } from 'react' + +import { FaAngleRightIcon } from '../../../Icon' +import { CommonButton } from '../common/CommonButton' + +type Props = { + onClick: () => void + isCurrent?: boolean +} + +export const MenuButton: FC> = ({ children, onClick, isCurrent }) => ( + + {children} + + +) diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuDialog.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuDialog.tsx new file mode 100644 index 0000000000..cc27f3198d --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuDialog.tsx @@ -0,0 +1,167 @@ +import React, { + Dispatch, + FC, + PropsWithChildren, + ReactNode, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react' +import { CSSTransition } from 'react-transition-group' +import { tv } from 'tailwind-variants' + +import { Button } from '../../../Button' +import { FocusTrap } from '../../../Dialog/FocusTrap' +import { FaXmarkIcon } from '../../../Icon' +import { Cluster } from '../../../Layout' +import { Section } from '../../../SectioningContent' +import { useTranslate } from '../../hooks/useTranslate' + +import { MenuSubHeader } from './MenuSubHeader' +import { Navigation } from './Navigation' +import { NavigationContext } from './NavigationContext' +import { NavigationGroupHeader } from './NavigationGroupHeader' +import { ReleaseNote } from './ReleaseNote' +import { ReleaseNoteContext } from './ReleaseNoteContext' + +const menu = tv({ + slots: { + wrapper: [ + 'shr-fixed shr-top-0 shr-left-0 shr-w-full shr-h-full shr-flex shr-flex-col shr-bg-white', + 'shr-translate-opacity shr-opacity-0 shr-duration-150', + '[&&.shr-sp-menu-enter-active]:shr-opacity-100', + '[&&.shr-sp-menu-enter-done]:shr-opacity-100', + '[&&.shr-sp-menu-exit-active]:shr-opacity-0', + '[&&.shr-sp-menu-exit-done]:shr-opacity-0', + ], + header: 'shr-px-0.75 shr-py-0.5 shr-border-b-shorthand shr-sticky shr-top-0', + content: 'shr-overflow-auto shr-p-1', + }, +}) + +export const MenuDialog: FC< + PropsWithChildren<{ + isOpen: boolean + setIsOpen: Dispatch + tenantSelector: ReactNode + }> +> = ({ children, isOpen, setIsOpen, tenantSelector }) => { + const { selectedNavigationGroup, setSelectedNavigationGroup } = useContext(NavigationContext) + const { isReleaseNoteSelected, setIsReleaseNoteSelected } = useContext(ReleaseNoteContext) + // const { isAppLauncherSelected, setIsAppLauncherSelected } = useContext(AppLauncherContext) + + const [contentBuffer, setContentBuffer] = useState(null) + const translate = useTranslate() + const domRef = useRef(null) + + const renderedContent = useMemo(() => { + const { wrapper, header, content } = menu() + + return ( + // eslint-disable-next-line smarthr/a11y-heading-in-sectioning-content +
+
+ + {isReleaseNoteSelected ? ( + setIsReleaseNoteSelected(false)} + /> + ) : selectedNavigationGroup ? ( + + ) : ( +
{tenantSelector}
+ )} + {/* {isAppLauncherSelected ? ( + setIsAppLauncherSelected(false)} + /> + ) : isReleaseNoteSelected ? ( + setIsReleaseNoteSelected(false)} + /> + ) : selectedNavigationGroup ? ( + + ) : ( +
{tenantSelector}
+ )} */} + + +
+
+ +
+ {isReleaseNoteSelected ? ( + + ) : selectedNavigationGroup ? ( + setIsOpen(false)} + /> + ) : ( + children + )} + + {/* {isAppLauncherSelected ? ( + + ) : isReleaseNoteSelected ? ( + + ) : selectedNavigationGroup ? ( + setIsOpen(false)} + /> + ) : ( + children + )} */} +
+
+ ) + }, [ + translate, + children, + // isAppLauncherSelected, + isReleaseNoteSelected, + selectedNavigationGroup, + // setIsAppLauncherSelected, + setIsOpen, + setIsReleaseNoteSelected, + tenantSelector, + ]) + + useEffect(() => { + if (isOpen) { + setContentBuffer(renderedContent) + } else { + setIsReleaseNoteSelected(false) + // setIsAppLauncherSelected(false) + setSelectedNavigationGroup(null) + } + }, [ + isOpen, + renderedContent, + // setIsAppLauncherSelected, + setIsReleaseNoteSelected, + setSelectedNavigationGroup, + ]) + + return ( + +
+ {isOpen ? renderedContent : contentBuffer} +
+
+ ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuSubHeader.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuSubHeader.tsx new file mode 100644 index 0000000000..7c54db48b8 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuSubHeader.tsx @@ -0,0 +1,29 @@ +import React, { FC, ReactNode } from 'react' + +import { Button } from '../../../Button' +import { Heading } from '../../../Heading' +import { FaArrowLeftIcon } from '../../../Icon' +import { useTranslate } from '../../hooks/useTranslate' +import { Translate } from '../common/Translate' + +type Props = { + title: ReactNode + onClickBack: () => void +} + +export const MenuSubHeader: FC = ({ title, onClickBack }) => { + const translate = useTranslate() + + return ( + <> + + + {/* eslint-disable-next-line smarthr/a11y-heading-in-sectioning-content */} + + {title} + + + ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/MobileHeader.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MobileHeader.tsx index 27821c31e2..4841fae1fe 100644 --- a/packages/smarthr-ui/src/components/AppHeader/components/mobile/MobileHeader.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MobileHeader.tsx @@ -1,7 +1,82 @@ -import React, { FC } from 'react' +import React, { FC, useState } from 'react' -import { HeaderProps } from '../../types' +import { Header } from '../../../Header' +import { useLocale } from '../../hooks/useLocale' +import { HeaderProps, NavigationGroup } from '../../types' -export const MobileHeader: FC = ({ children, className = '', ...props }) => ( -
MobileHeader
-) +import { Help } from './Help' +import { Menu } from './Menu' +import { NavigationContext } from './NavigationContext' +import { ReleaseNoteContext } from './ReleaseNoteContext' +import { TenantSelector } from './TenantSelector' +import { UserInfo } from './UserInfo' + +export const MobileHeader: FC = ({ + navigations, + releaseNote, + className = '', + tenants, + children, + helpPageUrl, + schoolUrl, + userInfo, + appName, + currentTenantId, + onTenantSelect, + mobileAdditionalContent, + ...props +}) => { + const [isReleaseNoteSelected, setIsReleaseNoteSelected] = useState(false) + const [selectedNavigationGroup, setSelectedNavigationGroup] = useState( + null, + ) + + const { locale } = useLocale() + + // navigations の設定をメニューの解放条件とする + const isMenuAvailable = navigations && navigations.length > 0 + + return ( + + +
+ {children} + + + + + + {isMenuAvailable && ( + + } + additionalContent={mobileAdditionalContent} + /> + )} +
+
+
+ ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/Navigation.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/Navigation.tsx new file mode 100644 index 0000000000..726b4b645e --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/Navigation.tsx @@ -0,0 +1,54 @@ +import React, { FC, Fragment } from 'react' +import { tv } from 'tailwind-variants' + +import { Text } from '../../../Text' +import { NavigationGroup, Navigation as NavigationType } from '../../types' +import { isChildNavigationGroup } from '../../utils' + +import { NavigationItem } from './NavigationItem' + +type Props = { + navigations: NavigationType[] | NavigationGroup['childNavigations'] + onClickNavigation: () => void +} + +const separator = tv({ + base: ['[&&]:shr-mx-0 [&&]:shr-my-0.5 [&&]:shr-border-b-shorthand'], +}) + +export const Navigation: FC = ({ navigations, onClickNavigation }) => ( +
+ {navigations.map((navigation, i) => { + if (isChildNavigationGroup(navigation)) { + const { childNavigations } = navigation + + return ( + + + {navigation.title} + + + {childNavigations.map((childNavigation) => ( + + ))} + + {i + 1 !== navigations.length &&
} +
+ ) + } + + const nextNavigation = navigations[i + 1] + + return ( + + + {isChildNavigationGroup(nextNavigation) &&
} +
+ ) + })} +
+) diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/NavigationContext.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/NavigationContext.tsx new file mode 100644 index 0000000000..c4911d7a54 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/NavigationContext.tsx @@ -0,0 +1,9 @@ +import React, { Dispatch, createContext } from 'react' + +import { Navigation, NavigationGroup } from '../../types' + +export const NavigationContext = createContext({ + navigations: [] as Navigation[], + selectedNavigationGroup: null as NavigationGroup | null, + setSelectedNavigationGroup: (() => {}) as Dispatch>, +}) diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/NavigationGroupHeader.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/NavigationGroupHeader.tsx new file mode 100644 index 0000000000..eb9480c0f0 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/NavigationGroupHeader.tsx @@ -0,0 +1,19 @@ +import React, { FC, useContext } from 'react' + +import { NavigationGroup } from '../../types' + +import { MenuSubHeader } from './MenuSubHeader' +import { NavigationContext } from './NavigationContext' + +export const NavigationGroupHeader: FC<{ + currentNavigationGroup: NavigationGroup +}> = ({ currentNavigationGroup }) => { + const { setSelectedNavigationGroup } = useContext(NavigationContext) + + return ( + setSelectedNavigationGroup(null)} + /> + ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/NavigationItem.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/NavigationItem.tsx new file mode 100644 index 0000000000..d682ecfb00 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/NavigationItem.tsx @@ -0,0 +1,93 @@ +import React, { FC, useContext } from 'react' +import { tv } from 'tailwind-variants' + +import { Navigation } from '../../types' +import { isChildNavigation } from '../../utils' +import { CommonButton, commonButton } from '../common/CommonButton' +import { Translate } from '../common/Translate' + +import { MenuButton } from './MenuButton' +import { NavigationContext } from './NavigationContext' + +const navigationItem = tv({ + base: ['[&&]:shr-px-0.5'], +}) + +export const NavigationItem: FC<{ navigation: Navigation; onClickNavigation: () => void }> = ({ + navigation, + onClickNavigation, +}) => { + const { setSelectedNavigationGroup } = useContext(NavigationContext) + const navigationItemStyle = navigationItem() + + if ('elementAs' in navigation) { + const { children, elementAs: Tag, current, ...rest } = navigation + + return ( + <> + {/* eslint-disable-next-line smarthr/a11y-delegate-element-has-role-presentation */} + + {children} + + + ) + } + + if ('href' in navigation) { + return ( + + {navigation.children} + + ) + } + + if ('onClick' in navigation) { + return ( + { + navigation.onClick(e) + onClickNavigation() + }} + current={navigation.current} + boldWhenCurrent + className={navigationItemStyle} + > + {navigation.children} + + ) + } + + // 子要素に current を持っているものがあるかどうか + const childrenHasCurrent = navigation.childNavigations.some((child) => { + if (isChildNavigation(child)) { + return child.current + } + + return child.childNavigations.some((c) => c.current) + }) + + return ( + setSelectedNavigationGroup(navigation)} + isCurrent={navigation.current || childrenHasCurrent} + > + {navigation.children} + + ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/ReleaseNote.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/ReleaseNote.tsx new file mode 100644 index 0000000000..4de7c0f6f6 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/ReleaseNote.tsx @@ -0,0 +1,71 @@ +import React, { FC, useContext } from 'react' +import { tv } from 'tailwind-variants' + +import { FaUpRightFromSquareIcon } from '../../../Icon' +import { Center, Stack } from '../../../Layout' +import { Loader } from '../../../Loader' +import { Text } from '../../../Text' +import { useTranslate } from '../../hooks/useTranslate' +import { Translate } from '../common/Translate' + +import { ReleaseNoteContext } from './ReleaseNoteContext' + +const releaseNoteStyle = tv({ + slots: { + anchor: ['shr-text-base shr-text-link [&&]:shr-underline', '[&&]:hover:shr-no-underline'], + icon: ['shr-ms-0.5'], + indexLinkWrapper: ['shr-text-end shr-mt-2'], + indexLinkAnchor: [ + 'shr-text-base shr-text-link [&&]:shr-no-underline', + '[&&]:hover:shr-underline', + ], + }, +}) + +export const ReleaseNote: FC = () => { + const translate = useTranslate() + const { releaseNote } = useContext(ReleaseNoteContext) + + if (!releaseNote) { + return null + } + + const { anchor, icon, indexLinkWrapper, indexLinkAnchor } = releaseNoteStyle() + + return ( +
+ {releaseNote.loading ? ( +
+ +
+ ) : releaseNote.error ? ( + + {translate('common/releaseNotesLoadError')} + + ) : ( + + {releaseNote.links.slice(0, 5).map((link) => ( + + ))} + + )} + + +
+ ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/ReleaseNoteContext.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/ReleaseNoteContext.tsx new file mode 100644 index 0000000000..cbc195c583 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/ReleaseNoteContext.tsx @@ -0,0 +1,13 @@ +import React, { Dispatch, createContext } from 'react' + +import { HeaderProps } from '../../types' + +export const ReleaseNoteContext = createContext<{ + releaseNote: HeaderProps['releaseNote'] + isReleaseNoteSelected: boolean + setIsReleaseNoteSelected: Dispatch> +}>({ + releaseNote: null, + isReleaseNoteSelected: false, + setIsReleaseNoteSelected: () => {}, +}) diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/TenantSelector.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/TenantSelector.tsx new file mode 100644 index 0000000000..44868c13e7 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/TenantSelector.tsx @@ -0,0 +1,70 @@ +import React, { ComponentProps, FC } from 'react' +import { tv } from 'tailwind-variants' + +import { Dropdown, DropdownContent, DropdownTrigger } from '../../../Dropdown' +import { Header } from '../../../Header' +import { FaCaretDownIcon } from '../../../Icon' +import { Text } from '../../../Text' +import { CommonButton } from '../common/CommonButton' + +const tenantDropdownTriggerButton = tv({ + base: [ + 'shr-border-none shr-bg-white shr-text-start shr-text-sm shr-rounded-s shr-px-0.5 shr-py-0.25 shr-cursor-pointer', + 'hover:shr-bg-white-darken', + '[&[aria-expanded="true"]>.smarthr-ui-Icon:last-child]:shr-rotate-180', + ], +}) + +type Props = { + tenants?: ComponentProps['tenants'] + currentTenantId?: ComponentProps['currentTenantId'] + onTenantSelect?: ComponentProps['onTenantSelect'] +} + +export const TenantSelector: FC = ({ tenants, currentTenantId, onTenantSelect }) => { + if (!tenants || tenants.length === 0 || !currentTenantId) { + return null + } + + const tenantName = tenants.find((tenant) => tenant.id === currentTenantId)?.name + + if (!tenantName) { + return null + } + + if (tenants.length === 1 || !onTenantSelect) { + return {tenantName} + } + + return ( + + {/* eslint-disable-next-line smarthr/a11y-trigger-has-button */} + + + + + +
+ {tenants.map((tenant) => { + const isCurrent = tenant.id === currentTenantId + + return ( + !isCurrent && onTenantSelect(tenant.id)} + > + {tenant.name} + + ) + })} +
+
+
+ ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/UserInfo.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/UserInfo.tsx new file mode 100644 index 0000000000..0a591a9a3a --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/UserInfo.tsx @@ -0,0 +1,115 @@ +import React, { FC, useState } from 'react' +import { tv } from 'tailwind-variants' + +import { Button } from '../../../Button' +import { Dialog } from '../../../Dialog' +import { Dropdown, DropdownContent, DropdownTrigger } from '../../../Dropdown' +import { FaGearIcon, FaGlobeIcon, FaUserLargeIcon } from '../../../Icon' +import { useLocale } from '../../hooks/useLocale' +import { useTranslate } from '../../hooks/useTranslate' +import { HeaderProps, UserInfoProps } from '../../types' +import { buildDisplayName } from '../../utils' +import { CommonButton } from '../common/CommonButton' +import { Translate } from '../common/Translate' + +import { LanguageSelector } from './LanguageSelector' + +const userInfo = tv({ + slots: { + iconButton: ['[&&&]:shr-border-transparent [&&]:shr-p-0.25'], + iconButtonInner: [ + 'shr-block shr-flex shr-items-center shr-justify-center shr-p-0.25 shr-bg-white shr-border-shorthand shr-rounded-full', + ], + dropdownUserName: ['shr-box-border shr-text-sm shr-px-1 shr-py-0.75 shr-min-w-[246px]'], + dropdownButtonArea: ['shr-border-t-shorthand shr-p-0.5'], + }, +}) + +export const UserInfo: FC> = ({ + arbitraryDisplayName, + email, + empCode, + firstName, + lastName, + accountUrl, +}) => { + const [languageDialogOpen, setLanguageDialogOpen] = useState(false) + const { locale } = useLocale() + const translate = useTranslate() + + const displayName = + arbitraryDisplayName ?? + buildDisplayName({ + email, + empCode, + firstName, + lastName, + }) + + if (!displayName) { + return null + } + + const { iconButton, iconButtonInner, dropdownUserName, dropdownButtonArea } = userInfo() + + return ( + <> + + + + + + +
+

{displayName}

+
+ + {(locale || accountUrl) && ( +
+ {locale && ( + setLanguageDialogOpen(true)} + prefix={} + > + Language + + )} + + {accountUrl && ( + } + > + {translate('common/userSetting')} + + )} +
+ )} +
+
+ + {locale && ( + setLanguageDialogOpen(false)} + width={246} + > + + + )} + + ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/types.ts b/packages/smarthr-ui/src/components/AppHeader/types.ts index 4eab348f35..e95ecec577 100644 --- a/packages/smarthr-ui/src/components/AppHeader/types.ts +++ b/packages/smarthr-ui/src/components/AppHeader/types.ts @@ -32,6 +32,7 @@ export type HeaderProps = ComponentProps & { navigations?: Navigation[] | null desktopNavigationAdditionalContent?: ReactNode releaseNote?: ReleaseNoteProps | null + mobileAdditionalContent?: ReactNode } export type Navigation = NavigationLink | NavigationCustomTag | NavigationButton | NavigationGroup @@ -54,7 +55,7 @@ type NavigationButton = { current?: boolean } -type NavigationGroup = { +export type NavigationGroup = { children: ReactElement | string childNavigations: Array current?: boolean From 7314ae35c6ed8808f1a960b653e9904f782864f3 Mon Sep 17 00:00:00 2001 From: nabeliwo Date: Wed, 18 Dec 2024 18:10:38 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=E3=83=87=E3=82=B9=E3=82=AF?= =?UTF-8?q?=E3=83=88=E3=83=83=E3=83=97=E7=89=88=E3=81=AE=E3=82=A2=E3=83=97?= =?UTF-8?q?=E3=83=AA=E3=83=A9=E3=83=B3=E3=83=81=E3=83=A3=E3=83=BC=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppHeader/AppHeader.stories.tsx | 22 ++ .../src/components/AppHeader/AppHeader.tsx | 2 +- .../components/desktop/AppLauncher.tsx | 296 ++++++++++++++++++ .../desktop/AppLauncherSortDropdown.tsx | 97 ++++++ .../components/desktop/DesktopHeader.tsx | 24 +- .../AppHeader/hooks/useAppLauncher.ts | 112 +++++++ .../AppHeader/multilingualization/messages.ts | 119 ++++++- .../src/components/AppHeader/types.ts | 20 ++ 8 files changed, 673 insertions(+), 19 deletions(-) create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncher.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncherSortDropdown.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/hooks/useAppLauncher.ts diff --git a/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx b/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx index 90667bc1c0..0bd6c69fc2 100644 --- a/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx @@ -14,6 +14,14 @@ const AdditionalContent: FC = ({ children }) => (
{children}
) +const buildFeature = (index: number, name: string, favorite: boolean, position?: number) => ({ + id: `feature-${index}`, + url: 'https://example.com', + name, + favorite, + position: position ?? null, +}) + const meta = { title: 'Navigation(ナビゲーション)/AppHeader', component: AppHeader, @@ -131,6 +139,20 @@ const meta = { ], indexUrl: 'https://exmaple.com', }, + features: [ + buildFeature(1, '従業員リスト', false), + buildFeature(2, '共通設定', true, 4), + buildFeature(3, 'お知らせ管理', true, 3), + buildFeature(4, '給与明細', true, 1), + buildFeature(5, '申請', false), + buildFeature(6, '給与明細管理', false), + buildFeature(7, 'マイナンバー管理', false), + buildFeature(8, '源泉徴収票管理', false), + buildFeature(9, '手続き', false), + buildFeature(10, '手続きToDo', false), + buildFeature(11, '文書配付', false), + buildFeature(12, 'IdP', true, 2), + ], mobileAdditionalContent: mobileAdditionalContent, }, } satisfies Meta diff --git a/packages/smarthr-ui/src/components/AppHeader/AppHeader.tsx b/packages/smarthr-ui/src/components/AppHeader/AppHeader.tsx index 48ea182e2e..332c5f17c0 100644 --- a/packages/smarthr-ui/src/components/AppHeader/AppHeader.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/AppHeader.tsx @@ -1,9 +1,9 @@ import React, { FC } from 'react' import { DesktopHeader } from './components/desktop/DesktopHeader' +import { MobileHeader } from './components/mobile/MobileHeader' 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 }) => { diff --git a/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncher.tsx b/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncher.tsx new file mode 100644 index 0000000000..45c9a6ef10 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncher.tsx @@ -0,0 +1,296 @@ +import React, { FC, ReactNode } from 'react' +import { tv } from 'tailwind-variants' + +import { textColor } from '../../../../themes' +import { AnchorButton, Button, UnstyledButton } from '../../../Button' +import { Dropdown, DropdownContent, DropdownTrigger } from '../../../Dropdown' +import { Heading } from '../../../Heading' +import { + FaArrowRightIcon, + FaCaretDownIcon, + FaCircleXmarkIcon, + FaStarIcon, + FaToolboxIcon, +} from '../../../Icon' +import { SearchInput } from '../../../Input' +import { Cluster } from '../../../Layout' +import { LineClamp } from '../../../LineClamp' +import { Section } from '../../../SectioningContent' +import { SideNav } from '../../../SideNav' +import { Text } from '../../../Text' +import { TextLink } from '../../../TextLink' +import { useAppLauncher } from '../../hooks/useAppLauncher' +import { useTranslate } from '../../hooks/useTranslate' +import { Launcher } from '../../types' +import { Translate } from '../common/Translate' + +import { AppLauncherSortDropdown } from './AppLauncherSortDropdown' + +type Props = { + /** 機能一覧 */ + features: Array + /** 新しいデザインを適用するかどうか */ + enableNew?: boolean +} + +const appLauncher = tv({ + slots: { + appsButton: [ + 'shr-border-none shr-font-normal shr-text-white shr-bg-transparent shr-px-0.25', + 'hover:shr-border-transparent hover:shr-bg-transparent', + 'focus-visible:shr-border-transparent focus-visible:shr-bg-transparent', + 'forced-colors:shr-border-shorthand', + ], + contentWrapper: [ + 'smarthr-ui-AppLauncher', + 'shr-grid shr-grid-rows-[auto_1fr] shr-w-[38rem] shr-h-[40rem]', + ], + searchArea: [ + 'smarthr-ui-AppLauncher-searchArea', + 'shr-p-1 shr-border-b-shorthand', + '[&_.smarthr-ui-Input]:shr-h-[42px]', + ], + inner: ['smarthr-ui-AppLauncher-inner', 'shr-grid shr-grid-cols-[11rem_1fr] shr-min-h-0'], + side: [ + 'smarthr-ui-AppLauncher-side', + 'shr-flex shr-flex-col shr-pt-0.5 shr-pb-1 shr-border-r-shorthand shr-bg-column', + '[&_hr]:shr-h-[1px] [&_hr]:shr-m-0.5 [&_hr]:shr-border-none [&_hr]:shr-bg-border', + ], + sideNav: [ + '[&_.smarthr-ui-SideNav-item>button]:shr-py-0.75 [&_.smarthr-ui-SideNav-item>button]:shr-px-1', + '[&_.smarthr-ui-SideNav-item>button>span]:shr-flex-nowrap', + '[&_.smarthr-ui-SideNav-item>button_.smarthr-ui-Icon]:shr-shrink-0 [&_.smarthr-ui-SideNav-item>button_.smarthr-ui-Icon]:shr-align-bottom', + ], + sideNavHeading: ['shr-py-0.75 shr-px-1 shr-text-xs shr-text-black'], + help: ['smarthr-ui-AppLauncher-help', 'shr-mt-auto shr-px-1 shr-text-xs'], + main: ['smarthr-ui-AppLauncher-main', 'shr-grid shr-min-h-0'], + mainInner: ['shr-grid shr-grid-rows-[auto_1fr] shr-min-h-0'], + contentHead: [ + 'shr-min-h-[2rem] shr-py-0.75 shr-px-1', + '[&_.smarthr-ui-Heading]:shr-text-black', + ], + scrollArea: ['shr-overflow-y-scroll shr-h-[509px]'], + list: ['shr-list-none', '[&>li]:shr-px-0.5 [&>li]:shr-py-0.25'], + listEmpty: ['shr-p-1 shr-text-center'], + listItem: [ + 'smarthr-ui-AppLauncher-listItem', + 'shr-grid shr-grid-cols-[1rem_1fr_1rem] shr-gap-0.75 shr-min-h-[2.5rem] shr-px-1 shr-py-0 shr-leading-tight shr-text-left shr-whitespace-normal', + ], + }, + variants: { + enableNew: { + true: { + appsButton: [ + 'shr-px-0.5 shr-font-bold shr-text-black', + '[&_>_svg]:aria-expanded:shr-rotate-180', + 'hover:shr-bg-white-darken', + 'focus-visible:shr-bg-white-darken', + ], + }, + }, + noIcon: { + true: { + sideNav: ['[&_.smarthr-ui-SideNav-item>button]:shr-pl-1.5'], + }, + }, + selected: { + false: { + sideNav: ['[&_.smarthr-ui-SideNav-item>button_.smarthr-ui-Icon]:shr-text-grey'], + }, + }, + favorite: { + false: { + listItem: ['shr-grid-cols-[1fr_1rem]'], + }, + }, + }, +}) + +export const AppLauncher: FC = ({ features: baseFeatures, enableNew }) => { + const translate = useTranslate() + const { + features, + page, + mode, + sortType, + searchQuery, + changePage, + setSortType, + changeSearchQuery, + } = useAppLauncher(baseFeatures) + + const { + appsButton, + contentWrapper, + searchArea, + inner, + side, + sideNav, + sideNavHeading, + help, + main, + mainInner, + contentHead, + scrollArea, + list, + listEmpty, + listItem, + } = appLauncher({ + enableNew, + }) + + const pageMap: Record = { + favorite: {translate('Launcher/favoriteModeText')}, + all: {translate('Launcher/allModeText')}, + } + + return ( + + + + + + +
+
+ {translate('Launcher/searchInputTitle')}} + width="100%" + value={searchQuery} + suffix={ + mode === 'search' && ( + { + // 別のキューにしないとドロップダウンが閉じてしまう + setTimeout(() => { + changeSearchQuery('') + }, 0) + }} + > + + + ) + } + onChange={(e) => changeSearchQuery(e.target.value)} + /> +
+ +
+
+ + ), + isSelected: mode !== 'search' && page === 'favorite', + }, + ]} + onClick={(_, id) => { + changePage(id as Launcher['page']) + }} + /> + +
+ +
+ + {translate('Launcher/listText')} + + + { + changePage(id as Launcher['page']) + }} + /> +
+ +
+ + {translate('Launcher/helpText')} + +
+
+ +
+
+ + + {mode === 'search' ? ( + {translate('Launcher/searchResultText')} + ) : ( + pageMap[page] + )} + + + {(mode === 'search' || page === 'all') && ( + setSortType(value)} + /> + )} + + +
+
    + {features.length === 0 ? ( +
    + + {translate('Launcher/emptyText')} + +
    + ) : ( + features.map((feature) => ( +
  • + } + suffix={} + wide + target="_blank" + > + {feature.name} + +
  • + )) + )} +
+
+
+
+
+
+
+
+ ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncherSortDropdown.tsx b/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncherSortDropdown.tsx new file mode 100644 index 0000000000..1a4ba6f611 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncherSortDropdown.tsx @@ -0,0 +1,97 @@ +import React, { FC, ReactNode, useRef } from 'react' +import { tv } from 'tailwind-variants' + +import { textColor } from '../../../../themes' +import { Button } from '../../../Button' +import { Dropdown, DropdownContent, DropdownTrigger } from '../../../Dropdown' +import { FaCaretDownIcon, FaCheckIcon } from '../../../Icon' +import { Stack } from '../../../Layout' +import { useTranslate } from '../../hooks/useTranslate' +import { Launcher } from '../../types' +import { Translate } from '../common/Translate' + +const sortDropdown = tv({ + slots: { + trigger: [ + 'smarthr-ui-AppLauncher-SortDropdown-trigger', + 'shr-gap-0.25 shr-text-grey', + '[&[aria-expanded="true"]>.smarthr-ui-Icon]:shr-rotate-180', + ], + stack: ['shr-px-0.25 shr-py-0.5'], + contentButton: ['shr-border-none shr-justify-start shr-py-0.75 shr-font-normal shr-pl-2.5'], + }, + variants: { + selected: { + true: { + contentButton: ['shr-pl-1'], + }, + }, + }, +}) + +type Props = { + sortType: Launcher['sortType'] + onSelectSortType: (sortType: Launcher['sortType']) => void +} + +export const AppLauncherSortDropdown: FC = ({ sortType, onSelectSortType }) => { + const translate = useTranslate() + const triggerRef = useRef(null) + const { trigger, stack, contentButton } = sortDropdown() + + const sortMap: Record = { + default: {translate('Launcher/sortDropdownOrderDefault')}, + 'name/asc': {translate('Launcher/sortDropdownOrderNameAsc')}, + 'name/desc': {translate('Launcher/sortDropdownOrderNameDesc')}, + } + + return ( + + + + + + + {/* eslint-disable-next-line smarthr/best-practice-for-layouts */} + + {Object.entries(sortMap).map(([key, value], i) => ( + + ))} + + + + ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx b/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx index 7c08c45681..d69a649d7a 100644 --- a/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx @@ -9,6 +9,7 @@ import { localeMap } from '../../multilingualization' import { HeaderProps } from '../../types' import { Translate } from '../common/Translate' +import { AppLauncher, NewAppLauncher } from './AppLauncher' import { Navigation } from './Navigation' import { UserInfo } from './UserInfo.tsx' @@ -26,6 +27,7 @@ export const DesktopHeader: FC = ({ navigations, desktopNavigationAdditionalContent, releaseNote, + features, ...props }) => { const translate = useTranslate() @@ -42,14 +44,20 @@ export const DesktopHeader: FC = ({ currentTenantId={currentTenantId} > - {!enableNew && schoolUrl && ( - } - className="shr-flex shr-items-center shr-py-0.75 shr-leading-none" - > - {translate('common/school')} - + {!enableNew && ( + <> + {features && features.length > 0 && } + + {schoolUrl && ( + } + className="shr-flex shr-items-center shr-py-0.75 shr-leading-none" + > + {translate('common/school')} + + )} + )} {helpPageUrl && ( diff --git a/packages/smarthr-ui/src/components/AppHeader/hooks/useAppLauncher.ts b/packages/smarthr-ui/src/components/AppHeader/hooks/useAppLauncher.ts new file mode 100644 index 0000000000..6c41739cd8 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/hooks/useAppLauncher.ts @@ -0,0 +1,112 @@ +import { useCallback, useEffect, useState } from 'react' + +import { Launcher } from '../types' + +export const useAppLauncher = (baseFeatures: Array) => { + const [page, setPage] = useState('favorite') + const [mode, setMode] = useState('default') + const [sortType, setSortType] = useState('default') + const [searchQuery, setSearchQuery] = useState('') + const [features, setFeatures] = useState>( + sortFeatures(baseFeatures, { + page, + mode, + sortType, + searchQuery, + }), + ) + + useEffect(() => { + setFeatures( + sortFeatures(baseFeatures, { + page, + sortType, + mode, + searchQuery, + }), + ) + }, [baseFeatures, page, mode, sortType, searchQuery]) + + const changePage = useCallback((newPage: Launcher['page']) => { + setPage(newPage) + setMode('default') + setSearchQuery('') + }, []) + + const changeSearchQuery = useCallback( + (q: string) => { + setSearchQuery(q) + + if (mode !== 'search') { + setMode('search') + } else { + if (q === '') { + setMode('default') + } + } + }, + [mode], + ) + + return { features, page, mode, sortType, searchQuery, changePage, setSortType, changeSearchQuery } +} + +const sortFeatures = ( + features: Array, + { + page, + sortType, + mode, + searchQuery, + }: { + page: Launcher['page'] + sortType: Launcher['sortType'] + mode: Launcher['mode'] + searchQuery: string + }, +) => { + if (mode !== 'search' && page === 'favorite') { + const filtered = features.filter((item) => item.favorite) + + // feature の position の数値の順に並び替える。position が null の場合は最後に並べる + return filtered.sort((a, b) => { + if (a.position === null && b.position === null) { + return 0 + } else if (a.position === null) { + return 1 + } else if (b.position === null) { + return -1 + } else { + return a.position - b.position + } + }) + } + + const featuresRes = + mode === 'search' + ? features.filter((feature) => looseInclude(feature.name, searchQuery)) + : [...features] + + if (sortType === 'name/asc') { + featuresRes.sort((a, b) => a.name.localeCompare(b.name)) + } + + if (sortType === 'name/desc') { + featuresRes.sort((a, b) => b.name.localeCompare(a.name)) + } + + return featuresRes +} + +// 文字列 a が文字列 b を含んでいたら true を返す +export const looseInclude = (a: string, b: string) => { + const normalizedA = normalize(a) + const normalizedB = normalize(b) + return normalizedA.includes(normalizedB) +} + +// アルファベットの大文字小文字は同じものとして扱う。カタカナとひらがなも同じものとして扱う。 +const normalize = (s: string) => + s + .toLowerCase() + .replace(/[\u30a1-\u30f6]/g, (match) => String.fromCharCode(match.charCodeAt(0) - 0x60)) diff --git a/packages/smarthr-ui/src/components/AppHeader/multilingualization/messages.ts b/packages/smarthr-ui/src/components/AppHeader/multilingualization/messages.ts index 017babf57c..9230d5edd4 100644 --- a/packages/smarthr-ui/src/components/AppHeader/multilingualization/messages.ts +++ b/packages/smarthr-ui/src/components/AppHeader/multilingualization/messages.ts @@ -13,11 +13,22 @@ export type Messages = { '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 + 'Launcher/searchInputTitle': string + 'Launcher/favoriteModeText': string + 'Launcher/allModeText': string + 'Launcher/listText': string + 'Launcher/helpText': string + 'Launcher/searchResultText': string + 'Launcher/emptyText': string + 'Launcher/sortDropdownLabel': string + 'Launcher/sortDropdownSelected': string + 'Launcher/sortDropdownOrderDefault': string + 'Launcher/sortDropdownOrderNameAsc': string + 'Launcher/sortDropdownOrderNameDesc': string } export const translation = { @@ -35,11 +46,22 @@ export const translation = { 'MobileHeader/Menu/closeMenu': 'メニューを閉じる', 'MobileHeader/Menu/allAppButton': 'すべてのアプリ', 'MobileHeader/Menu/managementMenu': '管理メニュー', - 'MobileHeader/Menu/appList': 'アプリ一覧', 'MobileHeader/Menu/latestReleaseNotes': '最新のリリースノート', 'MobileHeader/MenuSubHeader/back': '戻る', 'MobileHeader/MenuAccordion/open': '開く', 'MobileHeader/MenuAccordion/close': '閉じる', + 'Launcher/searchInputTitle': 'アプリ名を入力してください。', + 'Launcher/favoriteModeText': 'よく使うアプリ', + 'Launcher/allModeText': 'すべてのアプリ', + 'Launcher/listText': 'アプリ一覧', + 'Launcher/helpText': 'よく使うアプリとは', + 'Launcher/searchResultText': '検索結果', + 'Launcher/emptyText': '該当するアプリが見つかりませんでした。', + 'Launcher/sortDropdownLabel': '表示順', + 'Launcher/sortDropdownSelected': '選択中', + 'Launcher/sortDropdownOrderDefault': 'デフォルト', + 'Launcher/sortDropdownOrderNameAsc': 'アプリ名の昇順', + 'Launcher/sortDropdownOrderNameDesc': 'アプリ名の降順', }, 'id-id': { 'common/school': 'Sekolah', @@ -54,11 +76,22 @@ export const translation = { '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', + 'Launcher/searchInputTitle': 'Masukkan nama aplikasi.', + 'Launcher/favoriteModeText': 'Aplikasi yang sering digunakan', + 'Launcher/allModeText': 'Semua aplikasi', + 'Launcher/listText': 'Daftar aplikasi', + 'Launcher/helpText': '', // よく使うアプリとは + 'Launcher/searchResultText': 'Hasil penelusuran', + 'Launcher/emptyText': 'Tidak ditemukan aplikasi yang sesuai.', + 'Launcher/sortDropdownLabel': 'Urutan tampilan', + 'Launcher/sortDropdownSelected': 'Sedang dipilih', + 'Launcher/sortDropdownOrderDefault': 'Default', + 'Launcher/sortDropdownOrderNameAsc': 'Urutkan nama aplikasi dari atas ke bawah', + 'Launcher/sortDropdownOrderNameDesc': 'Urutkan nama aplikasi dari bawah ke atas', }, 'en-us': { 'common/school': 'School', @@ -73,11 +106,22 @@ export const translation = { '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', + 'Launcher/searchInputTitle': 'Input the name of the App', + 'Launcher/favoriteModeText': 'Favorite Apps', + 'Launcher/allModeText': 'All Apps', + 'Launcher/listText': 'App List', + 'Launcher/helpText': '', // よく使うアプリとは + 'Launcher/searchResultText': 'Search results', + 'Launcher/emptyText': 'App not found.', + 'Launcher/sortDropdownLabel': 'Sort by', + 'Launcher/sortDropdownSelected': 'Selected', + 'Launcher/sortDropdownOrderDefault': 'Default order', + 'Launcher/sortDropdownOrderNameAsc': 'App name (A→Z)', + 'Launcher/sortDropdownOrderNameDesc': 'App name (Z→A)', }, pt: { 'common/school': 'Escola', @@ -93,11 +137,22 @@ export const translation = { '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', + 'Launcher/searchInputTitle': 'Insira o nome do app.', + 'Launcher/favoriteModeText': 'Apps Favoritos', + 'Launcher/allModeText': 'Todos os Apps', + 'Launcher/listText': 'Lista de Apps', + 'Launcher/helpText': '', // よく使うアプリとは + 'Launcher/searchResultText': 'Resultados de pesquisa', + 'Launcher/emptyText': 'App não encontrado.', + 'Launcher/sortDropdownLabel': 'Ordenar por', + 'Launcher/sortDropdownSelected': 'Selecionando', + 'Launcher/sortDropdownOrderDefault': 'Ordem padrão', + 'Launcher/sortDropdownOrderNameAsc': 'Nome do App (A→Z)', + 'Launcher/sortDropdownOrderNameDesc': 'Nome do App (Z→A)', }, vi: { 'common/school': 'School', @@ -112,11 +167,22 @@ export const translation = { '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', + 'Launcher/searchInputTitle': 'Nhập tên tính năng.', + 'Launcher/favoriteModeText': 'Tính năng thường dùng', + 'Launcher/allModeText': 'Tất cả Tính năng', + 'Launcher/listText': 'Danh sách các tính năng', + 'Launcher/helpText': '', // よく使うアプリとは + 'Launcher/searchResultText': 'Kết quả tìm kiếm', + 'Launcher/emptyText': 'Không tìm thấy tính năng tương thích.', + 'Launcher/sortDropdownLabel': 'Thứ tự hiển thị', + 'Launcher/sortDropdownSelected': 'Đang lựa chọn', + 'Launcher/sortDropdownOrderDefault': 'Mặc định', + 'Launcher/sortDropdownOrderNameAsc': 'Tên tính năng (A→Z)', + 'Launcher/sortDropdownOrderNameDesc': 'Tên tính năng (Z→A)', }, ko: { 'common/school': '스쿨', @@ -132,11 +198,22 @@ export const translation = { 'MobileHeader/Menu/closeMenu': '메뉴를 닫기', 'MobileHeader/Menu/allAppButton': '모든 앱', 'MobileHeader/Menu/managementMenu': '관리메뉴', - 'MobileHeader/Menu/appList': '앱 리스트', 'MobileHeader/Menu/latestReleaseNotes': '최신 리리스 노트', 'MobileHeader/MenuSubHeader/back': '돌아가기', 'MobileHeader/MenuAccordion/open': '열기', 'MobileHeader/MenuAccordion/close': '닫기', + 'Launcher/searchInputTitle': '앱의 이름을 입력해 주세요.', + 'Launcher/favoriteModeText': '자주 사용하는 앱', + 'Launcher/allModeText': '모든 앱', + 'Launcher/listText': '앱 리스트', + 'Launcher/helpText': '', // よく使うアプリとは + 'Launcher/searchResultText': '검색결과', + 'Launcher/emptyText': '해당하는 앱을 발견할수 없습니다.', + 'Launcher/sortDropdownLabel': '표시순서', + 'Launcher/sortDropdownSelected': '선택중', + 'Launcher/sortDropdownOrderDefault': '디폴트', + 'Launcher/sortDropdownOrderNameAsc': '앱 이름의 오름차순', + 'Launcher/sortDropdownOrderNameDesc': '앱 이름의 내림차순', }, 'zh-cn': { 'common/school': '学校', @@ -151,11 +228,22 @@ export const translation = { 'MobileHeader/Menu/closeMenu': '关闭菜单', 'MobileHeader/Menu/allAppButton': '所有功能', 'MobileHeader/Menu/managementMenu': '管理菜单', - 'MobileHeader/Menu/appList': '功能一览表', 'MobileHeader/Menu/latestReleaseNotes': '最新版本说明', 'MobileHeader/MenuSubHeader/back': '返回', 'MobileHeader/MenuAccordion/open': '展开', 'MobileHeader/MenuAccordion/close': '关闭', + 'Launcher/searchInputTitle': '请输入功能名称。', + 'Launcher/favoriteModeText': '常用功能', + 'Launcher/allModeText': '所有功能', + 'Launcher/listText': '功能一览表', + 'Launcher/helpText': '', // よく使うアプリとは + 'Launcher/searchResultText': '搜索结果', + 'Launcher/emptyText': '未找到匹配的功能。', + 'Launcher/sortDropdownLabel': '显示排序', + 'Launcher/sortDropdownSelected': '已选择', + 'Launcher/sortDropdownOrderDefault': '默认', + 'Launcher/sortDropdownOrderNameAsc': '以功能名称升序', + 'Launcher/sortDropdownOrderNameDesc': '以功能名称降序', }, 'zh-tw': { 'common/school': '學校', @@ -170,10 +258,21 @@ export const translation = { 'MobileHeader/Menu/closeMenu': '關閉選單', 'MobileHeader/Menu/allAppButton': '所有功能', 'MobileHeader/Menu/managementMenu': '管理選單', - 'MobileHeader/Menu/appList': '功能一覽表', 'MobileHeader/Menu/latestReleaseNotes': '最新版本說明', - 'MobileHeader/MenuSubHeader/back': '返回', + 'MobileHeader/MenuSubHeader/back': '返回以功能名稱遞減', 'MobileHeader/MenuAccordion/open': '展開', 'MobileHeader/MenuAccordion/close': '關閉', + 'Launcher/searchInputTitle': '請輸入功能名稱。', + 'Launcher/favoriteModeText': '常用功能', + 'Launcher/allModeText': '所有功能', + 'Launcher/listText': '功能一覽表', + 'Launcher/helpText': '', // よく使うアプリとは + 'Launcher/searchResultText': '搜尋結果', + 'Launcher/emptyText': '未找到符合的功能。', + 'Launcher/sortDropdownLabel': '顯示排序', + 'Launcher/sortDropdownSelected': '選擇中', + 'Launcher/sortDropdownOrderDefault': '預設', + 'Launcher/sortDropdownOrderNameAsc': '以功能名稱遞增', + 'Launcher/sortDropdownOrderNameDesc': '以功能名稱遞減', }, } as const satisfies Record diff --git a/packages/smarthr-ui/src/components/AppHeader/types.ts b/packages/smarthr-ui/src/components/AppHeader/types.ts index e95ecec577..3b096f882f 100644 --- a/packages/smarthr-ui/src/components/AppHeader/types.ts +++ b/packages/smarthr-ui/src/components/AppHeader/types.ts @@ -32,6 +32,7 @@ export type HeaderProps = ComponentProps & { navigations?: Navigation[] | null desktopNavigationAdditionalContent?: ReactNode releaseNote?: ReleaseNoteProps | null + features?: Array mobileAdditionalContent?: ReactNode } @@ -77,3 +78,22 @@ export type ReleaseNoteProps = { loading?: boolean | null error?: boolean | null } + +const launcher = { + pages: ['favorite', 'all'], + modes: ['default', 'search'], + sortTypes: ['default', 'name/asc', 'name/desc'], +} as const + +export type Launcher = { + feature: { + id: string + name: string + url: string + favorite: boolean + position: number | null + } + page: (typeof launcher)['pages'][number] + mode: (typeof launcher)['modes'][number] + sortType: (typeof launcher)['sortTypes'][number] +} From b5275b65e5dd11ff95d78a8d4fa3a971f8fc3333 Mon Sep 17 00:00:00 2001 From: nabeliwo Date: Wed, 18 Dec 2024 20:36:28 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=E3=83=A2=E3=83=90=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E7=89=88=E3=81=AE=E3=82=A2=E3=83=97=E3=83=AA=E3=83=A9?= =?UTF-8?q?=E3=83=B3=E3=83=81=E3=83=A3=E3=83=BC=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/AppLauncherFeatures.tsx | 69 +++++++++++ .../AppLauncherSortDropdown.tsx | 12 +- .../components/desktop/AppLauncher.tsx | 56 +-------- .../components/desktop/DesktopHeader.tsx | 2 +- .../components/mobile/AppLauncher.tsx | 112 ++++++++++++++++++ .../components/mobile/AppLauncherContext.tsx | 13 ++ .../mobile/AppLauncherFilterDropdown.tsx | 83 +++++++++++++ .../AppHeader/components/mobile/Menu.tsx | 11 +- .../components/mobile/MenuDialog.tsx | 71 +++++------ .../components/mobile/MobileHeader.tsx | 73 +++++++----- 10 files changed, 365 insertions(+), 137 deletions(-) create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/common/AppLauncherFeatures.tsx rename packages/smarthr-ui/src/components/AppHeader/components/{desktop => common}/AppLauncherSortDropdown.tsx (87%) create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/AppLauncher.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/AppLauncherContext.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/components/mobile/AppLauncherFilterDropdown.tsx diff --git a/packages/smarthr-ui/src/components/AppHeader/components/common/AppLauncherFeatures.tsx b/packages/smarthr-ui/src/components/AppHeader/components/common/AppLauncherFeatures.tsx new file mode 100644 index 0000000000..5715577abd --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/common/AppLauncherFeatures.tsx @@ -0,0 +1,69 @@ +import React, { FC } from 'react' +import { tv } from 'tailwind-variants' + +import { AnchorButton } from '../../../Button' +import { FaArrowRightIcon, FaStarIcon } from '../../../Icon' +import { LineClamp } from '../../../LineClamp' +import { Text } from '../../../Text' +import { useTranslate } from '../../hooks/useTranslate' +import { Launcher } from '../../types' + +import { Translate } from './Translate' + +const appLauncherFeatures = tv({ + slots: { + empty: ['shr-p-1 shr-text-center'], + list: ['shr-list-none', '[&>li]:shr-px-0.5 [&>li]:shr-py-0.25'], + listItem: [ + 'smarthr-ui-AppLauncher-listItem', + 'shr-grid shr-grid-cols-[1rem_1fr_1rem] shr-gap-0.75 shr-min-h-[2.5rem] shr-px-1 shr-py-0 shr-leading-tight shr-text-left shr-whitespace-normal', + ], + }, + variants: { + favorite: { + false: { + listItem: ['shr-grid-cols-[1fr_1rem]'], + }, + }, + }, +}) + +type Props = { + features: Array + page: Launcher['page'] +} + +export const AppLauncherFeatures: FC = ({ features, page }) => { + const translate = useTranslate() + const { empty, list, listItem } = appLauncherFeatures() + + if (features.length === 0) { + return ( +
+ + {translate('Launcher/emptyText')} + +
+ ) + } + + return ( +
    + {features.map((feature) => ( +
  • + } + suffix={} + wide + target="_blank" + > + {feature.name} + +
  • + ))} +
+ ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncherSortDropdown.tsx b/packages/smarthr-ui/src/components/AppHeader/components/common/AppLauncherSortDropdown.tsx similarity index 87% rename from packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncherSortDropdown.tsx rename to packages/smarthr-ui/src/components/AppHeader/components/common/AppLauncherSortDropdown.tsx index 1a4ba6f611..253da6e356 100644 --- a/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncherSortDropdown.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/components/common/AppLauncherSortDropdown.tsx @@ -1,4 +1,4 @@ -import React, { FC, ReactNode, useRef } from 'react' +import React, { FC, useRef } from 'react' import { tv } from 'tailwind-variants' import { textColor } from '../../../../themes' @@ -39,10 +39,10 @@ export const AppLauncherSortDropdown: FC = ({ sortType, onSelectSortType const triggerRef = useRef(null) const { trigger, stack, contentButton } = sortDropdown() - const sortMap: Record = { - default: {translate('Launcher/sortDropdownOrderDefault')}, - 'name/asc': {translate('Launcher/sortDropdownOrderNameAsc')}, - 'name/desc': {translate('Launcher/sortDropdownOrderNameDesc')}, + const sortMap: Record = { + default: translate('Launcher/sortDropdownOrderDefault'), + 'name/asc': translate('Launcher/sortDropdownOrderNameAsc'), + 'name/desc': translate('Launcher/sortDropdownOrderNameDesc'), } return ( @@ -87,7 +87,7 @@ export const AppLauncherSortDropdown: FC = ({ sortType, onSelectSortType }, 0) }} > - {value} + {value} ))} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncher.tsx b/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncher.tsx index 45c9a6ef10..238931c614 100644 --- a/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncher.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncher.tsx @@ -2,30 +2,22 @@ import React, { FC, ReactNode } from 'react' import { tv } from 'tailwind-variants' import { textColor } from '../../../../themes' -import { AnchorButton, Button, UnstyledButton } from '../../../Button' +import { Button, UnstyledButton } from '../../../Button' import { Dropdown, DropdownContent, DropdownTrigger } from '../../../Dropdown' import { Heading } from '../../../Heading' -import { - FaArrowRightIcon, - FaCaretDownIcon, - FaCircleXmarkIcon, - FaStarIcon, - FaToolboxIcon, -} from '../../../Icon' +import { FaCaretDownIcon, FaCircleXmarkIcon, FaStarIcon, FaToolboxIcon } from '../../../Icon' import { SearchInput } from '../../../Input' import { Cluster } from '../../../Layout' -import { LineClamp } from '../../../LineClamp' import { Section } from '../../../SectioningContent' import { SideNav } from '../../../SideNav' -import { Text } from '../../../Text' import { TextLink } from '../../../TextLink' import { useAppLauncher } from '../../hooks/useAppLauncher' import { useTranslate } from '../../hooks/useTranslate' import { Launcher } from '../../types' +import { AppLauncherFeatures } from '../common/AppLauncherFeatures' +import { AppLauncherSortDropdown } from '../common/AppLauncherSortDropdown' import { Translate } from '../common/Translate' -import { AppLauncherSortDropdown } from './AppLauncherSortDropdown' - type Props = { /** 機能一覧 */ features: Array @@ -70,12 +62,6 @@ const appLauncher = tv({ '[&_.smarthr-ui-Heading]:shr-text-black', ], scrollArea: ['shr-overflow-y-scroll shr-h-[509px]'], - list: ['shr-list-none', '[&>li]:shr-px-0.5 [&>li]:shr-py-0.25'], - listEmpty: ['shr-p-1 shr-text-center'], - listItem: [ - 'smarthr-ui-AppLauncher-listItem', - 'shr-grid shr-grid-cols-[1rem_1fr_1rem] shr-gap-0.75 shr-min-h-[2.5rem] shr-px-1 shr-py-0 shr-leading-tight shr-text-left shr-whitespace-normal', - ], }, variants: { enableNew: { @@ -98,11 +84,6 @@ const appLauncher = tv({ sideNav: ['[&_.smarthr-ui-SideNav-item>button_.smarthr-ui-Icon]:shr-text-grey'], }, }, - favorite: { - false: { - listItem: ['shr-grid-cols-[1fr_1rem]'], - }, - }, }, }) @@ -132,9 +113,6 @@ export const AppLauncher: FC = ({ features: baseFeatures, enableNew }) => mainInner, contentHead, scrollArea, - list, - listEmpty, - listItem, } = appLauncher({ enableNew, }) @@ -260,31 +238,7 @@ export const AppLauncher: FC = ({ features: baseFeatures, enableNew }) =>
-
    - {features.length === 0 ? ( -
    - - {translate('Launcher/emptyText')} - -
    - ) : ( - features.map((feature) => ( -
  • - } - suffix={} - wide - target="_blank" - > - {feature.name} - -
  • - )) - )} -
+
diff --git a/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx b/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx index d69a649d7a..eda9084b34 100644 --- a/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx @@ -9,7 +9,7 @@ import { localeMap } from '../../multilingualization' import { HeaderProps } from '../../types' import { Translate } from '../common/Translate' -import { AppLauncher, NewAppLauncher } from './AppLauncher' +import { AppLauncher } from './AppLauncher' import { Navigation } from './Navigation' import { UserInfo } from './UserInfo.tsx' diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/AppLauncher.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/AppLauncher.tsx new file mode 100644 index 0000000000..f6624a395a --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/AppLauncher.tsx @@ -0,0 +1,112 @@ +import React, { FC } from 'react' +import { tv } from 'tailwind-variants' + +import { UnstyledButton } from '../../../Button' +import { FaCircleXmarkIcon } from '../../../Icon' +import { SearchInput } from '../../../Input' +import { Cluster } from '../../../Layout' +import { Text } from '../../../Text' +import { TextLink } from '../../../TextLink' +import { useAppLauncher } from '../../hooks/useAppLauncher' +import { useTranslate } from '../../hooks/useTranslate' +import { Launcher } from '../../types' +import { AppLauncherFeatures } from '../common/AppLauncherFeatures' +import { AppLauncherSortDropdown } from '../common/AppLauncherSortDropdown' +import { Translate } from '../common/Translate' + +import { AppLauncherFilterDropdown } from './AppLauncherFilterDropdown' + +type Props = { + features: Array +} + +const appLauncher = tv({ + slots: { + wrapper: ['smarthr-ui-AppLauncher', 'shr-flex shr-flex-col shr-h-full'], + searchArea: [ + 'smarthr-ui-AppLauncher-searchArea', + 'shr-py-0.75 shr-px-1 shr-border-b-shorthand', + '[&_.smarthr-ui-Input]:shr-h-[42px]', + ], + headArea: 'shr-py-0.75 shr-px-1', + scrollArea: 'shr-overflow-y-scroll shr-flex-1 shr-basis-0', + bottomArea: 'shr-py-0.75 shr-px-1 shr-border-t-shorthand', + }, +}) + +export const AppLauncher: FC = ({ features: baseFeatures }) => { + const translate = useTranslate() + const { + features, + page, + mode, + sortType, + searchQuery, + changePage, + setSortType, + changeSearchQuery, + } = useAppLauncher(baseFeatures) + + const { wrapper, searchArea, headArea, scrollArea, bottomArea } = appLauncher() + + return ( +
+
+ {translate('Launcher/searchInputTitle')}} + width="100%" + value={searchQuery} + suffix={ + mode === 'search' && ( + { + // 別のキューにしないとドロップダウンが閉じてしまう + setTimeout(() => { + changeSearchQuery('') + }, 0) + }} + > + + + ) + } + onChange={(e) => changeSearchQuery(e.target.value)} + /> +
+ + + {mode === 'search' ? ( + + {translate('Launcher/searchResultText')} + + ) : ( + changePage(p)} /> + )} + + {(mode === 'search' || page === 'all') && ( + setSortType(value)} + /> + )} + + +
+ +
+ +
+ + + {translate('Launcher/helpText')} + + +
+
+ ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/AppLauncherContext.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/AppLauncherContext.tsx new file mode 100644 index 0000000000..9bb4eb50fe --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/AppLauncherContext.tsx @@ -0,0 +1,13 @@ +import React, { Dispatch, createContext } from 'react' + +import { Launcher } from '../../types' + +export const AppLauncherContext = createContext<{ + features: Array | null | undefined + isAppLauncherSelected: boolean + setIsAppLauncherSelected: Dispatch> +}>({ + features: null, + isAppLauncherSelected: false, + setIsAppLauncherSelected: () => {}, +}) diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/AppLauncherFilterDropdown.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/AppLauncherFilterDropdown.tsx new file mode 100644 index 0000000000..22497a3b8f --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/AppLauncherFilterDropdown.tsx @@ -0,0 +1,83 @@ +import React, { FC } from 'react' +import { tv } from 'tailwind-variants' + +import { textColor } from '../../../../themes' +import { Button } from '../../../Button' +import { Dropdown, DropdownContent, DropdownTrigger } from '../../../Dropdown' +import { FaCaretDownIcon, FaCheckIcon } from '../../../Icon' +import { Stack } from '../../../Layout' +import { useTranslate } from '../../hooks/useTranslate' +import { Launcher } from '../../types' +import { Translate } from '../common/Translate' + +type Props = { + page: Launcher['page'] + onSelectPage: (page: Launcher['page']) => void +} + +const filterDropdown = tv({ + slots: { + trigger: [ + 'smarthr-ui-AppLauncher-SortDropdown-trigger', + 'shr-gap-0.25 shr-text-grey', + '[&[aria-expanded="true"]>.smarthr-ui-Icon]:shr-rotate-180', + ], + stack: ['shr-px-0.25 shr-py-0.5'], + contentButton: ['shr-border-none shr-justify-start shr-py-0.75 shr-font-normal shr-pl-2.5'], + }, + variants: { + selected: { + true: { + contentButton: ['shr-pl-1'], + }, + }, + }, +}) + +export const AppLauncherFilterDropdown: FC = ({ page, onSelectPage }) => { + const translate = useTranslate() + const { trigger, stack, contentButton } = filterDropdown() + const filterMap: Record = { + favorite: translate('Launcher/favoriteModeText'), + all: translate('MobileHeader/Menu/allAppButton'), + } + + return ( + + + + + + + {/* eslint-disable-next-line smarthr/best-practice-for-layouts */} + + {Object.entries(filterMap).map(([key, value], i) => { + const isSelected = key === page + + return ( + + ) + })} + + + + ) +} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/Menu.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/Menu.tsx index 0ebd70a367..756a9ffab3 100644 --- a/packages/smarthr-ui/src/components/AppHeader/components/mobile/Menu.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/Menu.tsx @@ -4,10 +4,11 @@ import { tv } from 'tailwind-variants' import { useHandleEscape } from '../../../../hooks/useHandleEscape' import { usePortal } from '../../../../hooks/usePortal' import { Button } from '../../../Button' -import { FaBarsIcon } from '../../../Icon' +import { FaAngleRightIcon, FaBarsIcon, FaToolboxIcon } from '../../../Icon' import { useTranslate } from '../../hooks/useTranslate' import { Translate } from '../common/Translate' +import { AppLauncherContext } from './AppLauncherContext' import { MenuAccordion } from './MenuAccordion' import { MenuButton } from './MenuButton' import { MenuDialog } from './MenuDialog' @@ -34,7 +35,7 @@ export const Menu: FC = ({ appName, tenantSelector, additionalContent }) const { navigations } = useContext(NavigationContext) const { releaseNote, setIsReleaseNoteSelected } = useContext(ReleaseNoteContext) - // const { appLauncher, setIsAppLauncherSelected } = useContext(AppLauncherContext) + const { features, setIsAppLauncherSelected } = useContext(AppLauncherContext) const translate = useTranslate() const { createPortal } = usePortal() @@ -61,7 +62,7 @@ export const Menu: FC = ({ appName, tenantSelector, additionalContent }) {createPortal( - {/* {appLauncher && ( + {features && features.length > 0 && (
- )} */} + )} {navigations.length > 0 && appName ? (
diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuDialog.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuDialog.tsx index cc27f3198d..46e6b28043 100644 --- a/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuDialog.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MenuDialog.tsx @@ -19,6 +19,8 @@ import { Cluster } from '../../../Layout' import { Section } from '../../../SectioningContent' import { useTranslate } from '../../hooks/useTranslate' +import { AppLauncher } from './AppLauncher' +import { AppLauncherContext } from './AppLauncherContext' import { MenuSubHeader } from './MenuSubHeader' import { Navigation } from './Navigation' import { NavigationContext } from './NavigationContext' @@ -50,7 +52,8 @@ export const MenuDialog: FC< > = ({ children, isOpen, setIsOpen, tenantSelector }) => { const { selectedNavigationGroup, setSelectedNavigationGroup } = useContext(NavigationContext) const { isReleaseNoteSelected, setIsReleaseNoteSelected } = useContext(ReleaseNoteContext) - // const { isAppLauncherSelected, setIsAppLauncherSelected } = useContext(AppLauncherContext) + const { features, isAppLauncherSelected, setIsAppLauncherSelected } = + useContext(AppLauncherContext) const [contentBuffer, setContentBuffer] = useState(null) const translate = useTranslate() @@ -64,19 +67,9 @@ export const MenuDialog: FC<
- {isReleaseNoteSelected ? ( - setIsReleaseNoteSelected(false)} - /> - ) : selectedNavigationGroup ? ( - - ) : ( -
{tenantSelector}
- )} - {/* {isAppLauncherSelected ? ( + {isAppLauncherSelected ? ( setIsAppLauncherSelected(false)} /> ) : isReleaseNoteSelected ? ( @@ -88,7 +81,7 @@ export const MenuDialog: FC< ) : (
{tenantSelector}
- )} */} + )}
-
- {isReleaseNoteSelected ? ( - - ) : selectedNavigationGroup ? ( - setIsOpen(false)} - /> - ) : ( - children - )} - - {/* {isAppLauncherSelected ? ( - - ) : isReleaseNoteSelected ? ( - - ) : selectedNavigationGroup ? ( - setIsOpen(false)} - /> - ) : ( - children - )} */} -
+ {isAppLauncherSelected && features && features.length > 0 ? ( + + ) : ( +
+ {isReleaseNoteSelected ? ( + + ) : selectedNavigationGroup ? ( + setIsOpen(false)} + /> + ) : ( + children + )} +
+ )}
) }, [ translate, children, - // isAppLauncherSelected, + features, + isAppLauncherSelected, isReleaseNoteSelected, selectedNavigationGroup, - // setIsAppLauncherSelected, + setIsAppLauncherSelected, setIsOpen, setIsReleaseNoteSelected, tenantSelector, @@ -140,13 +125,13 @@ export const MenuDialog: FC< setContentBuffer(renderedContent) } else { setIsReleaseNoteSelected(false) - // setIsAppLauncherSelected(false) + setIsAppLauncherSelected(false) setSelectedNavigationGroup(null) } }, [ isOpen, renderedContent, - // setIsAppLauncherSelected, + setIsAppLauncherSelected, setIsReleaseNoteSelected, setSelectedNavigationGroup, ]) diff --git a/packages/smarthr-ui/src/components/AppHeader/components/mobile/MobileHeader.tsx b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MobileHeader.tsx index 4841fae1fe..4cb583bc82 100644 --- a/packages/smarthr-ui/src/components/AppHeader/components/mobile/MobileHeader.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/components/mobile/MobileHeader.tsx @@ -4,6 +4,7 @@ import { Header } from '../../../Header' import { useLocale } from '../../hooks/useLocale' import { HeaderProps, NavigationGroup } from '../../types' +import { AppLauncherContext } from './AppLauncherContext' import { Help } from './Help' import { Menu } from './Menu' import { NavigationContext } from './NavigationContext' @@ -13,6 +14,7 @@ import { UserInfo } from './UserInfo' export const MobileHeader: FC = ({ navigations, + features, releaseNote, className = '', tenants, @@ -26,6 +28,7 @@ export const MobileHeader: FC = ({ mobileAdditionalContent, ...props }) => { + const [isAppLauncherSelected, setIsAppLauncherSelected] = useState(false) const [isReleaseNoteSelected, setIsReleaseNoteSelected] = useState(false) const [selectedNavigationGroup, setSelectedNavigationGroup] = useState( null, @@ -37,46 +40,54 @@ export const MobileHeader: FC = ({ const isMenuAvailable = navigations && navigations.length > 0 return ( - - -
- {children} +
+ {children} - + - + - {isMenuAvailable && ( - - } - additionalContent={mobileAdditionalContent} - /> - )} -
- - + {isMenuAvailable && ( + + } + additionalContent={mobileAdditionalContent} + /> + )} +
+
+
+ ) } From 2ac6aa9d180468b696e758187e2f6052864ca40b Mon Sep 17 00:00:00 2001 From: nabeliwo Date: Wed, 18 Dec 2024 21:04:51 +0900 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20Story=20=E3=82=92=E6=95=B4=E5=82=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppHeader/AppHeader.stories.tsx | 201 +++++++++++++- .../components/common/AppLauncherFeatures.tsx | 4 +- .../components/desktop/AppLauncher.tsx | 253 ++++++++---------- .../components/desktop/DesktopHeader.tsx | 59 +++- .../AppHeader/multilingualization/messages.ts | 14 +- 5 files changed, 372 insertions(+), 159 deletions(-) diff --git a/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx b/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx index 0bd6c69fc2..cbefd359de 100644 --- a/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx @@ -1,8 +1,10 @@ import { action } from '@storybook/addon-actions' import { Meta, StoryObj } from '@storybook/react/*' +import { within } from '@storybook/test' import React, { FC, PropsWithChildren } from 'react' import { AppHeader } from './AppHeader' +import { Locale } from './multilingualization' const CustomLink: FC> = (props) => ( @@ -155,6 +157,9 @@ const meta = { ], mobileAdditionalContent: mobileAdditionalContent, }, + parameters: { + useFixedHeight: true, + }, } satisfies Meta export default meta @@ -163,8 +168,200 @@ type Story = StoryObj export const Default: Story = {} -export const EnableNew: Story = { +export const VRTNameOnly: Story = { + args: { + userInfo: { + email: 'smarthr@example.com', + empCode: null, + firstName: '須磨', + lastName: '栄子', + accountUrl: 'https://exmaple.com', + }, + }, +} + +export const VRTEmpCodeOnly: Story = { args: { - enableNew: true, + userInfo: { + email: 'smarthr@example.com', + empCode: '001', + firstName: null, + lastName: null, + accountUrl: 'https://exmaple.com', + }, + }, +} + +export const VRTEmailOnly: Story = { + args: { + userInfo: { + email: 'smarthr@example.com', + empCode: null, + firstName: null, + lastName: null, + accountUrl: 'https://exmaple.com', + }, + }, +} + +export const VRTNoUserInfo: Story = { + args: { + userInfo: { + email: null, + empCode: null, + firstName: null, + lastName: null, + accountUrl: 'https://exmaple.com', + }, + }, +} + +export const VRTSingleTenant: Story = { + args: { + tenants: [ + { + id: 'tenant-1', + name: '株式会社テストテナント壱', + }, + ], + }, +} + +export const VRTNoNavigations: Story = { + args: { + navigations: undefined, + }, +} + +export const VRTTenant: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: '株式会社テストテナント壱 候補を開く' }).click() + }, +} + +export const VRTLauncher: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: 'アプリ' }).click() + }, +} + +export const VRTReleaseNote: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: 'リリースノート' }).click() + }, +} + +export const VRTReleaseNoteLoading: Story = { + args: { + releaseNote: { + loading: true, + links: [], + indexUrl: 'https://exmaple.com', + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: 'リリースノート' }).click() + }, +} + +export const VRTReleaseNoteError: Story = { + args: { + releaseNote: { + error: true, + links: [], + indexUrl: 'https://exmaple.com', + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: 'リリースノート' }).click() + }, +} + +export const VRTSetting: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: '栄子 須磨(001)' }).click() + }, +} + +export const VRTNavigationDropdown: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: 'ドロップダウン' }).click() + }, +} + +export const VRTNavigationDropdownGroup: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: 'グループ' }).click() + }, +} + +export const VRTLocaleEnUs: Story = { + args: { + locale: { + selectedLocale: 'en-us', + onSelectLocale: (locale: Locale) => action(locale), + }, + }, +} + +export const VRTLocaleIdId: Story = { + args: { + locale: { + selectedLocale: 'id-id', + onSelectLocale: (locale: Locale) => action(locale), + }, + }, +} + +export const VRTLocalePt: Story = { + args: { + locale: { + selectedLocale: 'pt', + onSelectLocale: (locale: Locale) => action(locale), + }, + }, +} + +export const VRTLocaleVi: Story = { + args: { + locale: { + selectedLocale: 'vi', + onSelectLocale: (locale: Locale) => action(locale), + }, + }, +} + +export const VRTLocaleKo: Story = { + args: { + locale: { + selectedLocale: 'ko', + onSelectLocale: (locale: Locale) => action(locale), + }, + }, +} + +export const VRTLocaleZhCn: Story = { + args: { + locale: { + selectedLocale: 'zh-cn', + onSelectLocale: (locale: Locale) => action(locale), + }, + }, +} + +export const VRTLocaleZhTw: Story = { + args: { + locale: { + selectedLocale: 'zh-tw', + onSelectLocale: (locale: Locale) => action(locale), + }, }, } diff --git a/packages/smarthr-ui/src/components/AppHeader/components/common/AppLauncherFeatures.tsx b/packages/smarthr-ui/src/components/AppHeader/components/common/AppLauncherFeatures.tsx index 5715577abd..969585c611 100644 --- a/packages/smarthr-ui/src/components/AppHeader/components/common/AppLauncherFeatures.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/components/common/AppLauncherFeatures.tsx @@ -5,6 +5,7 @@ import { AnchorButton } from '../../../Button' import { FaArrowRightIcon, FaStarIcon } from '../../../Icon' import { LineClamp } from '../../../LineClamp' import { Text } from '../../../Text' +import { mediaQuery, useMediaQuery } from '../../hooks/useMediaQuery' import { useTranslate } from '../../hooks/useTranslate' import { Launcher } from '../../types' @@ -34,6 +35,7 @@ type Props = { } export const AppLauncherFeatures: FC = ({ features, page }) => { + const isDesktop = useMediaQuery(mediaQuery.desktop) const translate = useTranslate() const { empty, list, listItem } = appLauncherFeatures() @@ -60,7 +62,7 @@ export const AppLauncherFeatures: FC = ({ features, page }) => { wide target="_blank" > - {feature.name} + {isDesktop ? {feature.name} : feature.name} ))} diff --git a/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncher.tsx b/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncher.tsx index 238931c614..ca5ffe7552 100644 --- a/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncher.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/components/desktop/AppLauncher.tsx @@ -2,10 +2,9 @@ import React, { FC, ReactNode } from 'react' import { tv } from 'tailwind-variants' import { textColor } from '../../../../themes' -import { Button, UnstyledButton } from '../../../Button' -import { Dropdown, DropdownContent, DropdownTrigger } from '../../../Dropdown' +import { UnstyledButton } from '../../../Button' import { Heading } from '../../../Heading' -import { FaCaretDownIcon, FaCircleXmarkIcon, FaStarIcon, FaToolboxIcon } from '../../../Icon' +import { FaCircleXmarkIcon, FaStarIcon } from '../../../Icon' import { SearchInput } from '../../../Input' import { Cluster } from '../../../Layout' import { Section } from '../../../SectioningContent' @@ -19,21 +18,12 @@ import { AppLauncherSortDropdown } from '../common/AppLauncherSortDropdown' import { Translate } from '../common/Translate' type Props = { - /** 機能一覧 */ features: Array - /** 新しいデザインを適用するかどうか */ - enableNew?: boolean } const appLauncher = tv({ slots: { - appsButton: [ - 'shr-border-none shr-font-normal shr-text-white shr-bg-transparent shr-px-0.25', - 'hover:shr-border-transparent hover:shr-bg-transparent', - 'focus-visible:shr-border-transparent focus-visible:shr-bg-transparent', - 'forced-colors:shr-border-shorthand', - ], - contentWrapper: [ + wrapper: [ 'smarthr-ui-AppLauncher', 'shr-grid shr-grid-rows-[auto_1fr] shr-w-[38rem] shr-h-[40rem]', ], @@ -64,16 +54,6 @@ const appLauncher = tv({ scrollArea: ['shr-overflow-y-scroll shr-h-[509px]'], }, variants: { - enableNew: { - true: { - appsButton: [ - 'shr-px-0.5 shr-font-bold shr-text-black', - '[&_>_svg]:aria-expanded:shr-rotate-180', - 'hover:shr-bg-white-darken', - 'focus-visible:shr-bg-white-darken', - ], - }, - }, noIcon: { true: { sideNav: ['[&_.smarthr-ui-SideNav-item>button]:shr-pl-1.5'], @@ -87,7 +67,7 @@ const appLauncher = tv({ }, }) -export const AppLauncher: FC = ({ features: baseFeatures, enableNew }) => { +export const AppLauncher: FC = ({ features: baseFeatures }) => { const translate = useTranslate() const { features, @@ -101,8 +81,7 @@ export const AppLauncher: FC = ({ features: baseFeatures, enableNew }) => } = useAppLauncher(baseFeatures) const { - appsButton, - contentWrapper, + wrapper, searchArea, inner, side, @@ -113,9 +92,7 @@ export const AppLauncher: FC = ({ features: baseFeatures, enableNew }) => mainInner, contentHead, scrollArea, - } = appLauncher({ - enableNew, - }) + } = appLauncher() const pageMap: Record = { favorite: {translate('Launcher/favoriteModeText')}, @@ -123,128 +100,112 @@ export const AppLauncher: FC = ({ features: baseFeatures, enableNew }) => } return ( - - - - - - -
-
- {translate('Launcher/searchInputTitle')}} - width="100%" - value={searchQuery} - suffix={ - mode === 'search' && ( - { - // 別のキューにしないとドロップダウンが閉じてしまう - setTimeout(() => { - changeSearchQuery('') - }, 0) - }} - > - - - ) - } - onChange={(e) => changeSearchQuery(e.target.value)} +
+
+ {translate('Launcher/searchInputTitle')}} + width="100%" + value={searchQuery} + suffix={ + mode === 'search' && ( + { + // 別のキューにしないとドロップダウンが閉じてしまう + setTimeout(() => { + changeSearchQuery('') + }, 0) + }} + > + + + ) + } + onChange={(e) => changeSearchQuery(e.target.value)} + /> +
+ +
+
+ + ), + isSelected: mode !== 'search' && page === 'favorite', + }, + ]} + onClick={(_, id) => { + changePage(id as Launcher['page']) + }} + /> + +
+ +
+ + {translate('Launcher/listText')} + + + { + changePage(id as Launcher['page']) + }} /> +
+ +
+ + {translate('Launcher/helpText')} +
+
-
-
- - ), - isSelected: mode !== 'search' && page === 'favorite', - }, - ]} - onClick={(_, id) => { - changePage(id as Launcher['page']) - }} - /> - -
- -
- - {translate('Launcher/listText')} - - - { - changePage(id as Launcher['page']) - }} +
+
+ + + {mode === 'search' ? ( + {translate('Launcher/searchResultText')} + ) : ( + pageMap[page] + )} + + + {(mode === 'search' || page === 'all') && ( + setSortType(value)} /> -
+ )} + -
- - {translate('Launcher/helpText')} - -
+
+
- -
-
- - - {mode === 'search' ? ( - {translate('Launcher/searchResultText')} - ) : ( - pageMap[page] - )} - - - {(mode === 'search' || page === 'all') && ( - setSortType(value)} - /> - )} - - -
- -
-
-
-
-
- - + + +
+
) } diff --git a/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx b/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx index eda9084b34..0f4da92e84 100644 --- a/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx @@ -1,7 +1,16 @@ import React, { FC } from 'react' +import { tv } from 'tailwind-variants' +import { Button } from '../../../Button' +import { Dropdown, DropdownContent, DropdownTrigger } from '../../../Dropdown' import { Header, HeaderLink, LanguageSwitcher } from '../../../Header' -import { FaCircleQuestionIcon, FaGraduationCapIcon, FaRegCircleQuestionIcon } from '../../../Icon' +import { + FaCaretDownIcon, + FaCircleQuestionIcon, + FaGraduationCapIcon, + FaRegCircleQuestionIcon, + FaToolboxIcon, +} from '../../../Icon' import { Cluster } from '../../../Layout' import { useLocale } from '../../hooks/useLocale' import { useTranslate } from '../../hooks/useTranslate' @@ -13,6 +22,30 @@ import { AppLauncher } from './AppLauncher' import { Navigation } from './Navigation' import { UserInfo } from './UserInfo.tsx' +const desktopHeader = tv({ + slots: { + wrapper: 'max-[751px]:!shr-hidden', + appsButton: [ + 'shr-border-none shr-font-normal shr-text-white shr-bg-transparent shr-px-0.25', + 'hover:shr-border-transparent hover:shr-bg-transparent', + 'focus-visible:shr-border-transparent focus-visible:shr-bg-transparent', + 'forced-colors:shr-border-shorthand', + ], + }, + variants: { + enableNew: { + true: { + appsButton: [ + 'shr-px-0.5 shr-font-bold shr-text-black', + '[&_>_svg]:aria-expanded:shr-rotate-180', + 'hover:shr-bg-white-darken', + 'focus-visible:shr-bg-white-darken', + ], + }, + }, + }, +}) + export const DesktopHeader: FC = ({ enableNew, className = '', @@ -33,12 +66,14 @@ export const DesktopHeader: FC = ({ const translate = useTranslate() const { locale } = useLocale() + const { wrapper, appsButton } = desktopHeader() + return ( <>
= ({ {!enableNew && ( <> - {features && features.length > 0 && } + {features && features.length > 0 && ( + + + + + + + + + + )} {schoolUrl && ( Date: Wed, 18 Dec 2024 21:13:19 +0900 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20README=20=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/AppHeader/README.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 packages/smarthr-ui/src/components/AppHeader/README.md diff --git a/packages/smarthr-ui/src/components/AppHeader/README.md b/packages/smarthr-ui/src/components/AppHeader/README.md new file mode 100644 index 0000000000..e8d106eb0f --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/README.md @@ -0,0 +1,60 @@ +## このコンポーネントについて + +props を埋めていくだけで良い感じに共通のヘッダー/ナビゲーションの UI が組み立てられるコンポーネントです。 + +## どのように使うのか + +`types.ts` にある HeaderProps というのが最終的な props なのであり、これらを穴埋めしていくことになります。 +`xxxAdditionalContent` 以外の props は積極的に埋めてください。 + +```ts +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 + features?: Array + mobileAdditionalContent?: ReactNode +} +``` + +下記に、少し特殊な動きをするものやパッと見分かりづらいであろうと思われる props だけ補足説明を書きます。 + +- `locale` + - 多言語対応に wovn を使っている場合はこの props は不要です。 +- `tenants` + - デスクトップ表示時 + - Header コンポーネントと同じです。 + - モバイル表示時 + - ハンバーガーメニューが表示されている場合はメニューの中に、そうでない場合はデスクトップ表示時と同じ箇所 (ロゴの横) に表示されます。 + - もし既存の独自実装ハンバーガーメニュー内にテナント選択の UI があるなどの理由で「ハンバーガーメニューは表示しないがモバイル表示時にヘッダーにテナント選択の UI を表示したくない」という場合は、ウィンドウサイズが 751px 以下のときに tenants props に undefined を渡すようにしてください。 +- `navigations` + - ヘッダーの下にナビゲーションが表示されるようになります。 + - AppNavi コンポーネントの buttons props とほぼ同じ型のデータを取ります。 + - AppNavi コンポーネントの buttons にはなかった、ドロップダウン内でのナビゲーションのグルーピングができるようになっています。 + - storybook の「VRT Navigation Dropdown Group」を参考にしてください。 + - **navigations props に値が渡されているときのみ、モバイル表示時にハンバーガーメニューが表示されます。独自実装の ハンバーガーメニューが存在する場合は、navigations props を利用するタイミングで移行してください。** +- `desktopAdditionalContent` + - ユーザー名をクリックしたときのドロップダウンの、「個人設定」の下に入れたいものがある場合に使います。 + - 見た目の共通化のため、乱用は避けてください +- `desktopNavigationAdditionalContent` + - ナビゲーション内で右寄せ、かつリリースノートの左側に入れたい物がある場合に使います。 + - 見た目の共通化のため、乱用は避けてください +- `mobileAdditionalContent` + - モバイル表示時に、メニュー内に何か追加で起きたいものがある場合に使います。 + - 見た目の共通化のため、乱用は避けてほしいですが、もし何かしらのパーツを配置する必要がある場合は、デザイナーと相談しながら実装してください。 + +## 多言語対応について + +- wovn を使っているアプリの場合 + - 内部で表示されているテキストに関しては、すべて `woven-enabled="true"` がついています。 + - 外部から渡すテキストは全て `ReactNode` 型で受け取るようになっているので、`ほげ` みたいなものを渡すようにしてください。 +- 辞書を持っているアプリの場合 + - コンポーネント側で辞書を持っているので、`locale` の props を埋めると内部的に持っているテキストは翻訳されます。 + - 外部から渡すテキストはアプリケーション側で翻訳されたものを渡すようにしてください。 From 3dcf4a673e0533c8842324aa0a6de23168d6493a Mon Sep 17 00:00:00 2001 From: nabeliwo Date: Wed, 18 Dec 2024 21:18:48 +0900 Subject: [PATCH 7/9] =?UTF-8?q?fix:=20=E3=83=86=E3=82=B9=E3=83=88=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/smarthr-ui/src/index.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/smarthr-ui/src/index.test.ts b/packages/smarthr-ui/src/index.test.ts index ed8e55550c..5a63bb1f92 100644 --- a/packages/smarthr-ui/src/index.test.ts +++ b/packages/smarthr-ui/src/index.test.ts @@ -9,10 +9,12 @@ const readdir = util.promisify(fs.readdir) const IGNORE_COMPONENTS = ['Experimental'] const IGNORE_INNER_DIRS = [ - 'FlashMessage/FlashMessageList', 'Input/InputWithTooltip', 'Browser/models', 'stories', + 'AppHeader/components', + 'AppHeader/hooks', + 'AppHeader/multilingualization', ] describe('index', () => { From e43a94558d4830ae34508f2729eacd7eb923f96d Mon Sep 17 00:00:00 2001 From: nabeliwo Date: Thu, 19 Dec 2024 15:17:55 +0900 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20AppHeader=20=E3=81=AE=20story=20?= =?UTF-8?q?=E3=81=AE=E4=BD=9C=E3=82=8A=E6=96=B9=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppHeader/AppHeader.stories.tsx | 367 ------------------ .../AppHeader/stories/AppHeader.stories.tsx | 17 + .../stories/VRTAppHeader.stories.tsx | 224 +++++++++++ .../src/components/AppHeader/stories/args.tsx | 153 ++++++++ 4 files changed, 394 insertions(+), 367 deletions(-) delete mode 100644 packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/stories/AppHeader.stories.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/stories/VRTAppHeader.stories.tsx create mode 100644 packages/smarthr-ui/src/components/AppHeader/stories/args.tsx diff --git a/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx b/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx deleted file mode 100644 index cbefd359de..0000000000 --- a/packages/smarthr-ui/src/components/AppHeader/AppHeader.stories.tsx +++ /dev/null @@ -1,367 +0,0 @@ -import { action } from '@storybook/addon-actions' -import { Meta, StoryObj } from '@storybook/react/*' -import { within } from '@storybook/test' -import React, { FC, PropsWithChildren } from 'react' - -import { AppHeader } from './AppHeader' -import { Locale } from './multilingualization' - -const CustomLink: FC> = (props) => ( - - {props.children} - -) - -const AdditionalContent: FC = ({ children }) => ( -
{children}
-) - -const buildFeature = (index: number, name: string, favorite: boolean, position?: number) => ({ - id: `feature-${index}`, - url: 'https://example.com', - name, - favorite, - position: position ?? null, -}) - -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', - }, - features: [ - buildFeature(1, '従業員リスト', false), - buildFeature(2, '共通設定', true, 4), - buildFeature(3, 'お知らせ管理', true, 3), - buildFeature(4, '給与明細', true, 1), - buildFeature(5, '申請', false), - buildFeature(6, '給与明細管理', false), - buildFeature(7, 'マイナンバー管理', false), - buildFeature(8, '源泉徴収票管理', false), - buildFeature(9, '手続き', false), - buildFeature(10, '手続きToDo', false), - buildFeature(11, '文書配付', false), - buildFeature(12, 'IdP', true, 2), - ], - mobileAdditionalContent: mobileAdditionalContent, - }, - parameters: { - useFixedHeight: true, - }, -} satisfies Meta - -export default meta - -type Story = StoryObj - -export const Default: Story = {} - -export const VRTNameOnly: Story = { - args: { - userInfo: { - email: 'smarthr@example.com', - empCode: null, - firstName: '須磨', - lastName: '栄子', - accountUrl: 'https://exmaple.com', - }, - }, -} - -export const VRTEmpCodeOnly: Story = { - args: { - userInfo: { - email: 'smarthr@example.com', - empCode: '001', - firstName: null, - lastName: null, - accountUrl: 'https://exmaple.com', - }, - }, -} - -export const VRTEmailOnly: Story = { - args: { - userInfo: { - email: 'smarthr@example.com', - empCode: null, - firstName: null, - lastName: null, - accountUrl: 'https://exmaple.com', - }, - }, -} - -export const VRTNoUserInfo: Story = { - args: { - userInfo: { - email: null, - empCode: null, - firstName: null, - lastName: null, - accountUrl: 'https://exmaple.com', - }, - }, -} - -export const VRTSingleTenant: Story = { - args: { - tenants: [ - { - id: 'tenant-1', - name: '株式会社テストテナント壱', - }, - ], - }, -} - -export const VRTNoNavigations: Story = { - args: { - navigations: undefined, - }, -} - -export const VRTTenant: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement) - canvas.getByRole('button', { name: '株式会社テストテナント壱 候補を開く' }).click() - }, -} - -export const VRTLauncher: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement) - canvas.getByRole('button', { name: 'アプリ' }).click() - }, -} - -export const VRTReleaseNote: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement) - canvas.getByRole('button', { name: 'リリースノート' }).click() - }, -} - -export const VRTReleaseNoteLoading: Story = { - args: { - releaseNote: { - loading: true, - links: [], - indexUrl: 'https://exmaple.com', - }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement) - canvas.getByRole('button', { name: 'リリースノート' }).click() - }, -} - -export const VRTReleaseNoteError: Story = { - args: { - releaseNote: { - error: true, - links: [], - indexUrl: 'https://exmaple.com', - }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement) - canvas.getByRole('button', { name: 'リリースノート' }).click() - }, -} - -export const VRTSetting: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement) - canvas.getByRole('button', { name: '栄子 須磨(001)' }).click() - }, -} - -export const VRTNavigationDropdown: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement) - canvas.getByRole('button', { name: 'ドロップダウン' }).click() - }, -} - -export const VRTNavigationDropdownGroup: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement) - canvas.getByRole('button', { name: 'グループ' }).click() - }, -} - -export const VRTLocaleEnUs: Story = { - args: { - locale: { - selectedLocale: 'en-us', - onSelectLocale: (locale: Locale) => action(locale), - }, - }, -} - -export const VRTLocaleIdId: Story = { - args: { - locale: { - selectedLocale: 'id-id', - onSelectLocale: (locale: Locale) => action(locale), - }, - }, -} - -export const VRTLocalePt: Story = { - args: { - locale: { - selectedLocale: 'pt', - onSelectLocale: (locale: Locale) => action(locale), - }, - }, -} - -export const VRTLocaleVi: Story = { - args: { - locale: { - selectedLocale: 'vi', - onSelectLocale: (locale: Locale) => action(locale), - }, - }, -} - -export const VRTLocaleKo: Story = { - args: { - locale: { - selectedLocale: 'ko', - onSelectLocale: (locale: Locale) => action(locale), - }, - }, -} - -export const VRTLocaleZhCn: Story = { - args: { - locale: { - selectedLocale: 'zh-cn', - onSelectLocale: (locale: Locale) => action(locale), - }, - }, -} - -export const VRTLocaleZhTw: Story = { - args: { - locale: { - selectedLocale: 'zh-tw', - onSelectLocale: (locale: Locale) => action(locale), - }, - }, -} diff --git a/packages/smarthr-ui/src/components/AppHeader/stories/AppHeader.stories.tsx b/packages/smarthr-ui/src/components/AppHeader/stories/AppHeader.stories.tsx new file mode 100644 index 0000000000..dfe652cfb1 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/stories/AppHeader.stories.tsx @@ -0,0 +1,17 @@ +import { Meta, StoryObj } from '@storybook/react/*' + +import { AppHeader } from '../AppHeader' + +import { args } from './args' + +const meta = { + title: 'Navigation(ナビゲーション)/AppHeader', + component: AppHeader, + args, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Playground: Story = {} diff --git a/packages/smarthr-ui/src/components/AppHeader/stories/VRTAppHeader.stories.tsx b/packages/smarthr-ui/src/components/AppHeader/stories/VRTAppHeader.stories.tsx new file mode 100644 index 0000000000..985ccd40a8 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/stories/VRTAppHeader.stories.tsx @@ -0,0 +1,224 @@ +import { action } from '@storybook/addon-actions' +import { Meta, StoryObj } from '@storybook/react/*' +import { within } from '@storybook/test' +import React from 'react' + +import { AppHeader } from '../AppHeader' +import { Locale } from '../multilingualization' + +import { args } from './args' + +const meta = { + title: 'Navigation(ナビゲーション)/AppHeader/VRT', + component: AppHeader, + args, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const VRTNameOnly: Story = { + args: { + userInfo: { + email: 'smarthr@example.com', + empCode: null, + firstName: '須磨', + lastName: '栄子', + accountUrl: 'https://exmaple.com', + }, + }, +} + +export const VRTEmpCodeOnly: Story = { + args: { + userInfo: { + email: 'smarthr@example.com', + empCode: '001', + firstName: null, + lastName: null, + accountUrl: 'https://exmaple.com', + }, + }, +} + +export const VRTEmailOnly: Story = { + args: { + userInfo: { + email: 'smarthr@example.com', + empCode: null, + firstName: null, + lastName: null, + accountUrl: 'https://exmaple.com', + }, + }, +} + +export const VRTNoUserInfo: Story = { + args: { + userInfo: { + email: null, + empCode: null, + firstName: null, + lastName: null, + accountUrl: 'https://exmaple.com', + }, + }, +} + +export const VRTSingleTenant: Story = { + args: { + tenants: [ + { + id: 'tenant-1', + name: '株式会社テストテナント壱', + }, + ], + }, +} + +export const VRTNoNavigations: Story = { + args: { + navigations: undefined, + }, +} + +export const VRTTenant: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: '株式会社テストテナント壱 候補を開く' }).click() + }, +} + +export const VRTLauncher: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: 'アプリ' }).click() + }, +} + +export const VRTReleaseNote: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: 'リリースノート' }).click() + }, +} + +export const VRTReleaseNoteLoading: Story = { + args: { + releaseNote: { + loading: true, + links: [], + indexUrl: 'https://exmaple.com', + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: 'リリースノート' }).click() + }, +} + +export const VRTReleaseNoteError: Story = { + args: { + releaseNote: { + error: true, + links: [], + indexUrl: 'https://exmaple.com', + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: 'リリースノート' }).click() + }, +} + +export const VRTSetting: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: '栄子 須磨(001)' }).click() + }, +} + +export const VRTNavigationDropdown: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: 'ドロップダウン' }).click() + }, +} + +export const VRTNavigationDropdownGroup: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + canvas.getByRole('button', { name: 'グループ' }).click() + }, +} + +export const VRTLocaleEnUs: Story = { + args: { + locale: { + selectedLocale: 'en-us', + onSelectLocale: (locale: Locale) => action(locale), + }, + }, +} + +export const VRTLocaleIdId: Story = { + args: { + locale: { + selectedLocale: 'id-id', + onSelectLocale: (locale: Locale) => action(locale), + }, + }, +} + +export const VRTLocalePt: Story = { + args: { + locale: { + selectedLocale: 'pt', + onSelectLocale: (locale: Locale) => action(locale), + }, + }, +} + +export const VRTLocaleVi: Story = { + args: { + locale: { + selectedLocale: 'vi', + onSelectLocale: (locale: Locale) => action(locale), + }, + }, +} + +export const VRTLocaleKo: Story = { + args: { + locale: { + selectedLocale: 'ko', + onSelectLocale: (locale: Locale) => action(locale), + }, + }, +} + +export const VRTLocaleZhCn: Story = { + args: { + locale: { + selectedLocale: 'zh-cn', + onSelectLocale: (locale: Locale) => action(locale), + }, + }, +} + +export const VRTLocaleZhTw: Story = { + args: { + locale: { + selectedLocale: 'zh-tw', + onSelectLocale: (locale: Locale) => action(locale), + }, + }, +} diff --git a/packages/smarthr-ui/src/components/AppHeader/stories/args.tsx b/packages/smarthr-ui/src/components/AppHeader/stories/args.tsx new file mode 100644 index 0000000000..e217aa6285 --- /dev/null +++ b/packages/smarthr-ui/src/components/AppHeader/stories/args.tsx @@ -0,0 +1,153 @@ +import { action } from '@storybook/addon-actions' +import React, { ComponentProps, FC, PropsWithChildren } from 'react' + +import { AppHeader } from '../AppHeader' + +const CustomLink: FC> = (props) => ( + + {props.children} + +) + +const AdditionalContent: FC = ({ children }) => ( +
{children}
+) + +const buildFeature = (index: number, name: string, favorite: boolean, position?: number) => ({ + id: `feature-${index}`, + url: 'https://example.com', + name, + favorite, + position: position ?? null, +}) + +export const args: ComponentProps = { + 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', + }, + features: [ + buildFeature(1, '従業員リスト', false), + buildFeature(2, '共通設定', true, 4), + buildFeature(3, 'お知らせ管理', true, 3), + buildFeature(4, '給与明細', true, 1), + buildFeature(5, '申請', false), + buildFeature(6, '給与明細管理', false), + buildFeature(7, 'マイナンバー管理', false), + buildFeature(8, '源泉徴収票管理', false), + buildFeature(9, '手続き', false), + buildFeature(10, '手続きToDo', false), + buildFeature(11, '文書配付', false), + buildFeature(12, 'IdP', true, 2), + ], + mobileAdditionalContent: mobileAdditionalContent, +} From 920cb762bbbb8cf9f1b3d89c022e46535b90efc9 Mon Sep 17 00:00:00 2001 From: nabeliwo Date: Mon, 23 Dec 2024 21:52:48 +0900 Subject: [PATCH 9/9] =?UTF-8?q?fix:=20=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E5=90=8D=E3=81=AB=20.tsx=20=E3=81=8C=E5=85=A5=E3=81=A3?= =?UTF-8?q?=E3=81=A6=E3=81=97=E3=81=BE=E3=81=A3=E3=81=A6=E3=81=84=E3=81=9F?= =?UTF-8?q?=E3=81=AE=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/AppHeader/components/desktop/DesktopHeader.tsx | 2 +- .../components/desktop/{UserInfo.tsx.tsx => UserInfo.tsx} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/smarthr-ui/src/components/AppHeader/components/desktop/{UserInfo.tsx.tsx => UserInfo.tsx} (100%) diff --git a/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx b/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx index 0f4da92e84..e6528159c4 100644 --- a/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx +++ b/packages/smarthr-ui/src/components/AppHeader/components/desktop/DesktopHeader.tsx @@ -20,7 +20,7 @@ import { Translate } from '../common/Translate' import { AppLauncher } from './AppLauncher' import { Navigation } from './Navigation' -import { UserInfo } from './UserInfo.tsx' +import { UserInfo } from './UserInfo' const desktopHeader = tv({ slots: { 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 similarity index 100% rename from packages/smarthr-ui/src/components/AppHeader/components/desktop/UserInfo.tsx.tsx rename to packages/smarthr-ui/src/components/AppHeader/components/desktop/UserInfo.tsx