diff --git a/.changeset/quiet-ties-sin.md b/.changeset/quiet-ties-sin.md new file mode 100644 index 000000000..49d580a72 --- /dev/null +++ b/.changeset/quiet-ties-sin.md @@ -0,0 +1,9 @@ +--- +'@o2s/blocks.order-details': minor +'@o2s/blocks.ticket-list': minor +'@o2s/ui': minor +--- + +- add MoreActionsMenu component and refactor ActionList +- migrate OrderDetails and TicketList to unified ActionList API +- improve breadcrumbs visibility and card border styling diff --git a/packages/blocks/order-details/src/frontend/OrderDetails.client.tsx b/packages/blocks/order-details/src/frontend/OrderDetails.client.tsx index 90845fd5e..fde8445e0 100644 --- a/packages/blocks/order-details/src/frontend/OrderDetails.client.tsx +++ b/packages/blocks/order-details/src/frontend/OrderDetails.client.tsx @@ -38,7 +38,7 @@ import { Typography } from '@o2s/ui/elements/typography'; import { Model, Request } from '../api-harmonization/order-details.client'; import { sdk } from '../sdk'; -import { OrderDetailsPureProps } from './OrderDetails.types'; +import { Action, OrderDetailsPureProps } from './OrderDetails.types'; const ProgressBar: React.FC< Readonly<{ @@ -166,22 +166,27 @@ export const OrderDetailsPure: React.FC> = ({ }); }; - const buttons = [ + const actionsDefinition: Action[] = [ { label: data.payOnlineLabel, icon: 'ArrowUpRight', + variant: 'destructive', }, { label: data.reorderLabel, icon: 'IterationCw', + variant: data.order.overdue.isOverdue ? 'secondary' : 'default', + className: data.order.overdue.isOverdue ? 'flex-1' : '', }, { label: data.trackOrderLabel, icon: 'Truck', + variant: data.order.overdue.isOverdue ? 'ghost' : 'secondary', + className: data.order.overdue.isOverdue ? 'w-full justify-start h-8' : 'flex-1', }, ]; - const actions = data.order.overdue.isOverdue ? buttons : buttons.slice(1); + const actions = data.order.overdue.isOverdue ? actionsDefinition : actionsDefinition.slice(1); const t = useTranslations(); @@ -206,49 +211,24 @@ export const OrderDetailsPure: React.FC> = ({
( - - )} - content={

{t('general.comingSoon')}

} - />, - ( - - )} - content={

{t('general.comingSoon')}

} - />, - ]} - dropdownActions={actions.slice(2).map((action) => ( - ( - - )} - content={

{t('general.comingSoon')}

} - /> - ))} + actions={actions + .filter((action) => action.label) + .map((action, index) => ( + ( + + )} + content={

{t('general.comingSoon')}

} + /> + ))} showMoreLabel={data.labels.showMore} />
diff --git a/packages/blocks/order-details/src/frontend/OrderDetails.types.ts b/packages/blocks/order-details/src/frontend/OrderDetails.types.ts index 78b955c88..b23076e6f 100644 --- a/packages/blocks/order-details/src/frontend/OrderDetails.types.ts +++ b/packages/blocks/order-details/src/frontend/OrderDetails.types.ts @@ -1,5 +1,8 @@ +import { VariantProps } from 'class-variance-authority'; import { defineRouting } from 'next-intl/routing'; +import { baseVariant } from '@o2s/ui/lib/utils'; + import type { Model } from '../api-harmonization/order-details.client'; export interface OrderDetailsProps { @@ -16,3 +19,8 @@ export type OrderDetailsPureProps = OrderDetailsProps & Model.OrderDetailsBlock; export interface OrderDetailsRendererProps extends Omit { slug: string[]; } + +export type Action = { + variant: VariantProps['variant']; + className?: string; +} & ({ label: string; icon?: string } | { label?: string; icon: string }); diff --git a/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx b/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx index 3c63cd812..537b70017 100644 --- a/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx +++ b/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx @@ -27,7 +27,7 @@ import { Typography } from '@o2s/ui/elements/typography'; import { Model, Request } from '../api-harmonization/ticket-list.client'; import { sdk } from '../sdk'; -import { TicketListPureProps } from './TicketList.types'; +import { Action, TicketListPureProps } from './TicketList.types'; export const TicketListPure: React.FC = ({ locale, accessToken, routing, meta, ...component }) => { const { Link: LinkComponent } = createNavigation(routing); @@ -90,6 +90,24 @@ export const TicketListPure: React.FC = ({ locale, accessTo }); }; + const variantConfig: Array<{ variant: Action['variant']; className: string }> = [ + { variant: 'default', className: 'no-underline hover:no-underline' }, + { variant: 'secondary', className: 'no-underline hover:no-underline flex-1' }, + { + variant: 'ghost', + className: + 'flex items-center gap-2 !no-underline hover:!no-underline cursor-pointer h-8 w-full justify-start', + }, + ]; + + const actions: Action[] = (data.forms ?? []).map((form, index) => ({ + label: form.label, + icon: form.icon, + url: form.url || '', + variant: variantConfig[index]?.variant ?? 'default', + className: variantConfig[index]?.className ?? '', + })); + // Define columns configuration outside JSX for better readability const columns = data.table.columns.map((column) => { switch (column.id) { @@ -125,7 +143,7 @@ export const TicketListPure: React.FC = ({ locale, accessTo }; } }) as DataListColumnConfig[]; - const actions = data.table.actions + const tableActions = data.table.actions ? { ...data.table.actions, render: (ticket: Model.Ticket) => { @@ -152,29 +170,21 @@ export const TicketListPure: React.FC = ({ locale, accessTo {data.forms && ( ( - - ))} - dropdownActions={data.forms.slice(2).map((form) => ( - - {form.icon && } - {form.label} - - ))} + actions={actions + .filter((action) => action.label) + .map((action, index) => ( + + ))} showMoreLabel={data.labels.showMore} /> )} @@ -221,7 +231,7 @@ export const TicketListPure: React.FC = ({ locale, accessTo viewMode={viewMode} data={data.tickets.data} columns={columns} - actions={actions} + actions={tableActions} cardHeaderSlots={data.cardHeaderSlots} enableRowSelection={component.enableRowSelection} selectedRows={selectedRows} diff --git a/packages/blocks/ticket-list/src/frontend/TicketList.types.ts b/packages/blocks/ticket-list/src/frontend/TicketList.types.ts index 5a2e9488a..434d7b938 100644 --- a/packages/blocks/ticket-list/src/frontend/TicketList.types.ts +++ b/packages/blocks/ticket-list/src/frontend/TicketList.types.ts @@ -1,5 +1,8 @@ +import { VariantProps } from 'class-variance-authority'; import { defineRouting } from 'next-intl/routing'; +import { baseVariant } from '@o2s/ui/lib/utils'; + import type { Model } from '../api-harmonization/ticket-list.client'; export interface TicketListProps { @@ -17,3 +20,9 @@ export type TicketListPureProps = TicketListProps & Model.TicketListBlock; export type TicketListRendererProps = Omit & { slug: string[]; }; + +export type Action = { + url: string; + variant: VariantProps['variant']; + className?: string; +} & ({ label: string; icon?: string } | { label?: string; icon: string }); diff --git a/packages/ui/src/components/ActionList/ActionList.stories.tsx b/packages/ui/src/components/ActionList/ActionList.stories.tsx index 897a09ea8..c5cd51367 100644 --- a/packages/ui/src/components/ActionList/ActionList.stories.tsx +++ b/packages/ui/src/components/ActionList/ActionList.stories.tsx @@ -1,7 +1,6 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'; import { Button } from '@o2s/ui/elements/button'; -import { Link } from '@o2s/ui/elements/link'; import { ActionList } from './ActionList'; @@ -19,7 +18,7 @@ export const Default: Story = { args: { showMoreLabel: 'Show more actions', triggerVariant: 'outline', - visibleActions: [ + actions: [ , @@ -33,43 +32,24 @@ export const Default: Story = { Tertiary Action 2 , ], - dropdownActions: [ - - Primary Action - , - - Secondary Action - , - - Tertiary Action 1 - , - - Tertiary Action 2 - , - ], }, }; export const SingleAction: Story = { args: { showMoreLabel: 'Show more actions', - visibleActions: [ + actions: [ , ], - dropdownActions: [ - - Single Action - , - ], }, }; export const TwoActions: Story = { args: { showMoreLabel: 'Show more actions', - visibleActions: [ + actions: [ , @@ -77,14 +57,6 @@ export const TwoActions: Story = { Secondary Action , ], - dropdownActions: [ - - Primary Action - , - - Secondary Action - , - ], }, }; @@ -92,8 +64,8 @@ export const WithDifferentVariant: Story = { args: { showMoreLabel: 'Show more actions', triggerVariant: 'destructive', - visibleActions: [ - , , ], - dropdownActions: [ - - Primary Action - , - - Secondary Action - , - - Tertiary Action 1 - , - - Tertiary Action 2 - , - ], }, }; diff --git a/packages/ui/src/components/ActionList/ActionList.tsx b/packages/ui/src/components/ActionList/ActionList.tsx index c0772c2b2..ac1cd3001 100644 --- a/packages/ui/src/components/ActionList/ActionList.tsx +++ b/packages/ui/src/components/ActionList/ActionList.tsx @@ -2,51 +2,34 @@ import React from 'react'; import { cn } from '@o2s/ui/lib/utils'; -import { DynamicIcon } from '@o2s/ui/components/DynamicIcon'; +import { MoreActionsMenu } from '@o2s/ui/components/MoreActionsMenu'; -import { Button } from '@o2s/ui/elements/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@o2s/ui/elements/dropdown-menu'; - -import { ActionListProps } from './ActionList.types'; +import type { ActionListProps } from './ActionList.types'; export const ActionList: React.FC> = ({ className, showMoreLabel, - visibleActions, - dropdownActions, + actions, triggerVariant = 'outline', }) => { - if (!visibleActions.length && !dropdownActions.length) { - return null; - } + if (!actions?.length) return null; + + const dropdownActions = actions.slice(2); return (
- {visibleActions[0]} + {actions?.[0]}
- {visibleActions[1]} + {actions?.[1]} {dropdownActions.length > 0 && ( - - - - - - {dropdownActions.map((action) => ( - - {action} - - ))} - - + )}
diff --git a/packages/ui/src/components/ActionList/ActionList.types.ts b/packages/ui/src/components/ActionList/ActionList.types.ts index 4d617e84d..729472487 100644 --- a/packages/ui/src/components/ActionList/ActionList.types.ts +++ b/packages/ui/src/components/ActionList/ActionList.types.ts @@ -3,8 +3,7 @@ import { VariantProps } from 'class-variance-authority'; import { baseVariant } from '@o2s/ui/lib/utils'; export type ActionListProps = { - visibleActions: React.ReactNode[]; - dropdownActions: React.ReactNode[]; + actions?: React.ReactNode[]; showMoreLabel: string; className?: string; triggerVariant?: VariantProps['variant']; diff --git a/packages/ui/src/components/Breadcrumbs/Breadcrumbs.tsx b/packages/ui/src/components/Breadcrumbs/Breadcrumbs.tsx index a7e6a81a9..c6457e0ee 100644 --- a/packages/ui/src/components/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/ui/src/components/Breadcrumbs/Breadcrumbs.tsx @@ -14,7 +14,7 @@ import { Link } from '@o2s/ui/elements/link'; import { BreadcrumbsProps } from './Breadcrumbs.types'; export const Breadcrumbs: React.FC = ({ breadcrumbs, LinkComponent }) => { - if (!breadcrumbs?.length) return null; + if (!breadcrumbs?.length || breadcrumbs.length === 1) return null; return ( diff --git a/packages/ui/src/components/Cards/InformativeCard/InformativeCard.tsx b/packages/ui/src/components/Cards/InformativeCard/InformativeCard.tsx index 39ea23dd6..53c05ee7a 100644 --- a/packages/ui/src/components/Cards/InformativeCard/InformativeCard.tsx +++ b/packages/ui/src/components/Cards/InformativeCard/InformativeCard.tsx @@ -62,7 +62,7 @@ export const InformativeCard: React.FC> = (props) diff --git a/packages/ui/src/components/Carousel/Carousel.tsx b/packages/ui/src/components/Carousel/Carousel.tsx index 835d01d68..803149ce5 100644 --- a/packages/ui/src/components/Carousel/Carousel.tsx +++ b/packages/ui/src/components/Carousel/Carousel.tsx @@ -1,6 +1,8 @@ 'use client'; -import React from 'react'; +import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react'; +import React, { useRef, useState } from 'react'; +import { Swiper as SwiperType } from 'swiper'; import 'swiper/css'; import 'swiper/css/navigation'; import 'swiper/css/pagination'; @@ -9,6 +11,8 @@ import { Swiper, SwiperSlide } from 'swiper/react'; import { cn } from '@o2s/ui/lib/utils'; +import { Button } from '@o2s/ui/elements/button'; + import { CarouselProps } from './Carousel.types'; export const Carousel: React.FC> = ({ @@ -19,8 +23,18 @@ export const Carousel: React.FC> = ({ modules = [], startingSlideIndex = 0, noSwipingSelector, + labels = { + previous: 'Previous slide', + next: 'Next slide', + }, ...swiperProps }) => { + const swiperRef = useRef(null); + + const [index, setIndex] = useState(startingSlideIndex); + const [isEnd, setIsEnd] = useState(false); + const [loop, setLoop] = useState(swiperProps.loop ?? false); + const allModules = [ A11y, Keyboard, @@ -30,19 +44,71 @@ export const Carousel: React.FC> = ({ ]; return ( - - {slides.map((slide, index) => ( - {slide} - ))} - +
+ { + swiperRef.current = swiper; + }} + onInit={(swiper) => { + const loopMode = swiperProps.loop ?? swiper.params.loop ?? false; + setLoop(loopMode); + setIndex(swiper.realIndex); + if (!loopMode) { + setIsEnd(swiper.isEnd); + } + }} + onSlideChange={(swiper) => { + setIndex(swiper.realIndex); + const loopMode = swiperProps.loop ?? swiper.params.loop ?? false; + if (!loopMode) { + setIsEnd(swiper.isEnd); + } + }} + keyboard={{ enabled: true, onlyInViewport: true }} + navigation={false} + pagination={showPagination ? { clickable: true } : false} + initialSlide={startingSlideIndex} + noSwipingSelector={noSwipingSelector} + > + {slides.map((slide, index) => ( + + {slide} + + ))} + + + {showNavigation && ( +
+ + + +
+ )} +
); }; diff --git a/packages/ui/src/components/Carousel/Carousel.types.ts b/packages/ui/src/components/Carousel/Carousel.types.ts index b0a73e32a..73541fa35 100644 --- a/packages/ui/src/components/Carousel/Carousel.types.ts +++ b/packages/ui/src/components/Carousel/Carousel.types.ts @@ -8,4 +8,8 @@ export interface CarouselProps extends Omit { showPagination?: boolean; startingSlideIndex?: number; noSwipingSelector?: string; + labels?: { + previous?: string; + next?: string; + }; } diff --git a/packages/ui/src/components/Chart/ChartTooltip/ChartTooltip.tsx b/packages/ui/src/components/Chart/ChartTooltip/ChartTooltip.tsx index 9f7e692c7..4e8827b56 100644 --- a/packages/ui/src/components/Chart/ChartTooltip/ChartTooltip.tsx +++ b/packages/ui/src/components/Chart/ChartTooltip/ChartTooltip.tsx @@ -16,33 +16,28 @@ export const ChartTooltip: React.FC = ({ type = 'number', act
{payload - .map( - ( - item: { name?: string; value?: number | string; color?: string; unit?: string }, - index: number, - ) => ( -
-
- - - - {`${item?.name} :`} -
- - {type === 'price' ? ( - - ) : ( - item.value - )} - + ?.map((item, index) => ( +
+
+ + + + {`${item?.name} :`}
- ), - ) + + {type === 'price' ? ( + + ) : ( + item.value + )} + +
+ )) .reverse()}
diff --git a/packages/ui/src/components/Chart/ChartTooltip/ChartTooltip.types.ts b/packages/ui/src/components/Chart/ChartTooltip/ChartTooltip.types.ts index fc48198bf..6b3247648 100644 --- a/packages/ui/src/components/Chart/ChartTooltip/ChartTooltip.types.ts +++ b/packages/ui/src/components/Chart/ChartTooltip/ChartTooltip.types.ts @@ -9,7 +9,5 @@ export interface ChartTooltipProps extends TooltipProps { value?: number | string; color?: string; unit?: string; - dataKey?: string; - payload?: unknown; }>; } diff --git a/packages/ui/src/components/MoreActionsMenu/MoreActionsMenu.stories.tsx b/packages/ui/src/components/MoreActionsMenu/MoreActionsMenu.stories.tsx new file mode 100644 index 000000000..a6b9b3bd1 --- /dev/null +++ b/packages/ui/src/components/MoreActionsMenu/MoreActionsMenu.stories.tsx @@ -0,0 +1,80 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite'; + +import { Link } from '@o2s/ui/elements/link'; + +import { MoreActionsMenu } from './MoreActionsMenu'; + +const meta = { + title: 'Components/MoreActionsMenu', + component: MoreActionsMenu, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + showMoreLabel: 'Show more actions', + triggerVariant: 'ghost', + triggerIcon: 'MoreVertical', + actions: [ + + Primary Action + , + + Secondary Action + , + ], + }, +}; + +export const SingleAction: Story = { + args: { + showMoreLabel: 'Show more actions', + triggerVariant: 'outline', + actions: [ + + Single Action + , + ], + }, +}; + +export const TwoActions: Story = { + args: { + showMoreLabel: 'Show more actions', + triggerVariant: 'destructive', + actions: [ + + Primary Action + , + + Secondary Action + , + ], + }, +}; + +export const WithDifferentVariant: Story = { + args: { + showMoreLabel: 'Show more actions', + triggerVariant: 'ghost', + triggerIcon: 'MoreVertical', + actions: [ + + Primary Action + , + + Secondary Action + , + + Tertiary Action + , + + Ghost Action + , + ], + }, +}; diff --git a/packages/ui/src/components/MoreActionsMenu/MoreActionsMenu.tsx b/packages/ui/src/components/MoreActionsMenu/MoreActionsMenu.tsx new file mode 100644 index 000000000..eb36270ce --- /dev/null +++ b/packages/ui/src/components/MoreActionsMenu/MoreActionsMenu.tsx @@ -0,0 +1,42 @@ +import React from 'react'; + +import { DynamicIcon } from '@o2s/ui/components/DynamicIcon'; + +import { Button } from '@o2s/ui/elements/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@o2s/ui/elements/dropdown-menu'; + +import type { MoreActionsMenuProps } from './MoreActionsMenu.types'; + +export const MoreActionsMenu: React.FC> = ({ + className, + showMoreLabel, + actions, + triggerVariant = 'ghost', + triggerIcon = 'MoreVertical', + open, + onOpenChange, +}) => { + if (actions.length === 0) return null; + + return ( + + + + + + {actions.map((action, index) => ( + + {action} + + ))} + + + ); +}; diff --git a/packages/ui/src/components/MoreActionsMenu/MoreActionsMenu.types.ts b/packages/ui/src/components/MoreActionsMenu/MoreActionsMenu.types.ts new file mode 100644 index 000000000..70ace5eab --- /dev/null +++ b/packages/ui/src/components/MoreActionsMenu/MoreActionsMenu.types.ts @@ -0,0 +1,13 @@ +import { VariantProps } from 'class-variance-authority'; + +import { baseVariant } from '@o2s/ui/lib/utils'; + +export type MoreActionsMenuProps = { + actions: React.ReactNode[]; + showMoreLabel: string; + className?: string; + triggerVariant?: VariantProps['variant']; + triggerIcon?: string; + open?: boolean; + onOpenChange?: (open: boolean) => void; +}; diff --git a/packages/ui/src/components/MoreActionsMenu/index.ts b/packages/ui/src/components/MoreActionsMenu/index.ts new file mode 100644 index 000000000..bbe48c63c --- /dev/null +++ b/packages/ui/src/components/MoreActionsMenu/index.ts @@ -0,0 +1,2 @@ +export { MoreActionsMenu } from './MoreActionsMenu'; +export type { MoreActionsMenuProps } from './MoreActionsMenu.types';