فهرست منبع

fix(dashboard): Multiple style & functionality fixes

Michael Bromley 5 ماه پیش
والد
کامیت
cca25e24d9

+ 399 - 33
packages/dashboard/src/app/routes/_authenticated/_orders/components/order-history/order-history.tsx

@@ -1,15 +1,46 @@
 import { HistoryEntry, HistoryEntryItem } from '@/vdb/components/shared/history-timeline/history-entry.js';
 import { HistoryNoteInput } from '@/vdb/components/shared/history-timeline/history-note-input.js';
-import { HistoryTimeline } from '@/vdb/components/shared/history-timeline/history-timeline.js';
+import {
+    HistoryTimeline,
+    useHistoryTimeline,
+} from '@/vdb/components/shared/history-timeline/history-timeline.js';
 import { Badge } from '@/vdb/components/ui/badge.js';
+import { Button } from '@/vdb/components/ui/button.js';
+import {
+    DropdownMenu,
+    DropdownMenuContent,
+    DropdownMenuItem,
+    DropdownMenuTrigger,
+} from '@/vdb/components/ui/dropdown-menu.js';
+import { Separator } from '@/vdb/components/ui/separator.js';
 import { Trans } from '@/vdb/lib/trans.js';
-import { ArrowRightToLine, CheckIcon, CreditCardIcon, SquarePen } from 'lucide-react';
+import {
+    ArrowRightToLine,
+    Ban,
+    CheckIcon,
+    ChevronDown,
+    ChevronUp,
+    CreditCardIcon,
+    Edit3,
+    MoreVerticalIcon,
+    PencilIcon,
+    RefreshCcw,
+    SquarePen,
+    TrashIcon,
+    Truck,
+    UserX,
+} from 'lucide-react';
+import { useState } from 'react';
 
 interface OrderHistoryProps {
     order: {
         id: string;
         createdAt: string;
         currencyCode: string;
+        customer?: {
+            firstName: string;
+            lastName: string;
+        } | null;
     };
     historyEntries: Array<HistoryEntryItem>;
     onAddNote: (note: string, isPrivate: boolean) => void;
@@ -18,19 +49,120 @@ interface OrderHistoryProps {
 }
 
 export function OrderHistory({
+    order,
     historyEntries,
     onAddNote,
     onUpdateNote,
     onDeleteNote,
 }: Readonly<OrderHistoryProps>) {
+    const [expandedGroups, setExpandedGroups] = useState<Set<number>>(new Set());
+    const { editNote, deleteNote } = useHistoryTimeline();
+
+    const isPrimaryEvent = (entry: HistoryEntryItem) => {
+        // Based on Angular component's isFeatured method
+        switch (entry.type) {
+            case 'ORDER_STATE_TRANSITION':
+                return (
+                    entry.data.to === 'Delivered' ||
+                    entry.data.to === 'Cancelled' ||
+                    entry.data.to === 'Settled' ||
+                    entry.data.from === 'Created'
+                );
+            case 'ORDER_REFUND_TRANSITION':
+                return entry.data.to === 'Settled';
+            case 'ORDER_PAYMENT_TRANSITION':
+                return entry.data.to === 'Settled' || entry.data.to === 'Cancelled';
+            case 'ORDER_FULFILLMENT_TRANSITION':
+                return entry.data.to === 'Delivered' || entry.data.to === 'Shipped';
+            case 'ORDER_NOTE':
+            case 'ORDER_MODIFIED':
+            case 'ORDER_CUSTOMER_UPDATED':
+            case 'ORDER_CANCELLATION':
+                return true;
+            default:
+                return false; // All other events are secondary
+        }
+    };
+
+    // Group consecutive secondary events
+    const groupedEntries: Array<
+        | { type: 'primary'; entry: HistoryEntryItem; index: number }
+        | {
+              type: 'secondary-group';
+              entries: Array<{ entry: HistoryEntryItem; index: number }>;
+              startIndex: number;
+          }
+    > = [];
+    let currentGroup: Array<{ entry: HistoryEntryItem; index: number }> = [];
+
+    for (let i = 0; i < historyEntries.length; i++) {
+        const entry = historyEntries[i];
+        const isSecondary = !isPrimaryEvent(entry);
+
+        if (isSecondary) {
+            currentGroup.push({ entry, index: i });
+        } else {
+            // If we have accumulated secondary events, add them as a group
+            if (currentGroup.length > 0) {
+                groupedEntries.push({
+                    type: 'secondary-group',
+                    entries: currentGroup,
+                    startIndex: currentGroup[0].index,
+                });
+                currentGroup = [];
+            }
+            // Add the primary event
+            groupedEntries.push({ type: 'primary', entry, index: i });
+        }
+    }
+
+    // Don't forget the last group if it exists
+    if (currentGroup.length > 0) {
+        groupedEntries.push({
+            type: 'secondary-group',
+            entries: currentGroup,
+            startIndex: currentGroup[0].index,
+        });
+    }
+
+    const toggleGroup = (groupIndex: number) => {
+        const newExpanded = new Set(expandedGroups);
+        if (newExpanded.has(groupIndex)) {
+            newExpanded.delete(groupIndex);
+        } else {
+            newExpanded.add(groupIndex);
+        }
+        setExpandedGroups(newExpanded);
+    };
     const getTimelineIcon = (entry: OrderHistoryProps['historyEntries'][0]) => {
         switch (entry.type) {
             case 'ORDER_PAYMENT_TRANSITION':
                 return <CreditCardIcon className="h-4 w-4" />;
+            case 'ORDER_REFUND_TRANSITION':
+                return <CreditCardIcon className="h-4 w-4" />;
             case 'ORDER_NOTE':
                 return <SquarePen className="h-4 w-4" />;
             case 'ORDER_STATE_TRANSITION':
+                if (entry.data.to === 'Delivered') {
+                    return <CheckIcon className="h-4 w-4" />;
+                }
+                if (entry.data.to === 'Cancelled') {
+                    return <Ban className="h-4 w-4" />;
+                }
                 return <ArrowRightToLine className="h-4 w-4" />;
+            case 'ORDER_FULFILLMENT_TRANSITION':
+                if (entry.data.to === 'Shipped' || entry.data.to === 'Delivered') {
+                    return <Truck className="h-4 w-4" />;
+                }
+                return <ArrowRightToLine className="h-4 w-4" />;
+            case 'ORDER_FULFILLMENT':
+                return <Truck className="h-4 w-4" />;
+            case 'ORDER_MODIFIED':
+                return <Edit3 className="h-4 w-4" />;
+            case 'ORDER_CUSTOMER_UPDATED':
+                return <UserX className="h-4 w-4" />;
+            case 'ORDER_CANCELLATION':
+                return <Ban className="h-4 w-4" />;
             default:
                 return <CheckIcon className="h-4 w-4" />;
         }
@@ -39,12 +171,26 @@ export function OrderHistory({
     const getTitle = (entry: OrderHistoryProps['historyEntries'][0]) => {
         switch (entry.type) {
             case 'ORDER_PAYMENT_TRANSITION':
-                return <Trans>Payment settled</Trans>;
+                if (entry.data.to === 'Settled') {
+                    return <Trans>Payment settled</Trans>;
+                }
+                if (entry.data.to === 'Authorized') {
+                    return <Trans>Payment authorized</Trans>;
+                }
+                if (entry.data.to === 'Declined' || entry.data.to === 'Cancelled') {
+                    return <Trans>Payment failed</Trans>;
+                }
+                return <Trans>Payment transitioned</Trans>;
+            case 'ORDER_REFUND_TRANSITION':
+                if (entry.data.to === 'Settled') {
+                    return <Trans>Refund settled</Trans>;
+                }
+                return <Trans>Refund transitioned</Trans>;
             case 'ORDER_NOTE':
                 return <Trans>Note added</Trans>;
             case 'ORDER_STATE_TRANSITION': {
                 if (entry.data.from === 'Created') {
-                    return <Trans>Order created</Trans>;
+                    return <Trans>Order placed</Trans>;
                 }
                 if (entry.data.to === 'Delivered') {
                     return <Trans>Order fulfilled</Trans>;
@@ -52,8 +198,27 @@ export function OrderHistory({
                 if (entry.data.to === 'Cancelled') {
                     return <Trans>Order cancelled</Trans>;
                 }
+                if (entry.data.to === 'Shipped') {
+                    return <Trans>Order shipped</Trans>;
+                }
                 return <Trans>Order transitioned</Trans>;
             }
+            case 'ORDER_FULFILLMENT_TRANSITION':
+                if (entry.data.to === 'Shipped') {
+                    return <Trans>Order shipped</Trans>;
+                }
+                if (entry.data.to === 'Delivered') {
+                    return <Trans>Order delivered</Trans>;
+                }
+                return <Trans>Fulfillment transitioned</Trans>;
+            case 'ORDER_FULFILLMENT':
+                return <Trans>Fulfillment created</Trans>;
+            case 'ORDER_MODIFIED':
+                return <Trans>Order modified</Trans>;
+            case 'ORDER_CUSTOMER_UPDATED':
+                return <Trans>Customer updated</Trans>;
+            case 'ORDER_CANCELLATION':
+                return <Trans>Order cancelled</Trans>;
             default:
                 return <Trans>{entry.type.replace(/_/g, ' ').toLowerCase()}</Trans>;
         }
@@ -65,36 +230,237 @@ export function OrderHistory({
                 <HistoryNoteInput onAddNote={onAddNote} />
             </div>
             <HistoryTimeline onEditNote={onUpdateNote} onDeleteNote={onDeleteNote}>
-                {historyEntries.map(entry => (
-                    <HistoryEntry
-                        key={entry.id}
-                        entry={entry}
-                        isNoteEntry={entry.type === 'ORDER_NOTE'}
-                        timelineIcon={getTimelineIcon(entry)}
-                        title={getTitle(entry)}
-                    >
-                        {entry.type === 'ORDER_NOTE' && (
-                            <div className="flex items-center space-x-2">
-                                <Badge variant={entry.isPublic ? 'outline' : 'secondary'} className="text-xs">
-                                    {entry.isPublic ? 'Public' : 'Private'}
-                                </Badge>
-                                <span>{entry.data.note}</span>
+                {groupedEntries.map((group, groupIndex) => {
+                    if (group.type === 'primary') {
+                        const entry = group.entry;
+                        return (
+                            <HistoryEntry
+                                key={entry.id}
+                                entry={entry}
+                                isNoteEntry={entry.type === 'ORDER_NOTE'}
+                                timelineIcon={getTimelineIcon(entry)}
+                                title={getTitle(entry)}
+                                isPrimary={true}
+                                customer={order.customer}
+                            >
+                                {entry.type === 'ORDER_NOTE' && (
+                                    <div className="space-y-2">
+                                        <p className="text-sm text-foreground">{entry.data.note}</p>
+                                        <div className="flex items-center gap-2">
+                                            <Badge
+                                                variant={entry.isPublic ? 'outline' : 'secondary'}
+                                                className="text-xs"
+                                            >
+                                                {entry.isPublic ? 'Public' : 'Private'}
+                                            </Badge>
+                                            <DropdownMenu>
+                                                <DropdownMenuTrigger asChild>
+                                                    <Button variant="ghost" size="sm" className="h-6 w-6 p-0">
+                                                        <MoreVerticalIcon className="h-3 w-3" />
+                                                    </Button>
+                                                </DropdownMenuTrigger>
+                                                <DropdownMenuContent align="end">
+                                                    <DropdownMenuItem
+                                                        onClick={() =>
+                                                            editNote(
+                                                                entry.id,
+                                                                entry.data.note,
+                                                                !entry.isPublic,
+                                                            )
+                                                        }
+                                                        className="cursor-pointer"
+                                                    >
+                                                        <PencilIcon className="mr-2 h-4 w-4" />
+                                                        <Trans>Edit</Trans>
+                                                    </DropdownMenuItem>
+                                                    <Separator className="my-1" />
+                                                    <DropdownMenuItem
+                                                        onClick={() => deleteNote(entry.id)}
+                                                        className="cursor-pointer text-red-600 focus:text-red-600"
+                                                    >
+                                                        <TrashIcon className="mr-2 h-4 w-4" />
+                                                        <span>Delete</span>
+                                                    </DropdownMenuItem>
+                                                </DropdownMenuContent>
+                                            </DropdownMenu>
+                                        </div>
+                                    </div>
+                                )}
+                                {entry.type === 'ORDER_STATE_TRANSITION' && entry.data.from !== 'Created' && (
+                                    <p className="text-xs text-muted-foreground">
+                                        <Trans>
+                                            From {entry.data.from} to {entry.data.to}
+                                        </Trans>
+                                    </p>
+                                )}
+                                {entry.type === 'ORDER_PAYMENT_TRANSITION' && (
+                                    <p className="text-xs text-muted-foreground">
+                                        <Trans>
+                                            Payment #{entry.data.paymentId} transitioned to {entry.data.to}
+                                        </Trans>
+                                    </p>
+                                )}
+                                {entry.type === 'ORDER_REFUND_TRANSITION' && (
+                                    <p className="text-xs text-muted-foreground">
+                                        <Trans>
+                                            Refund #{entry.data.refundId} transitioned to {entry.data.to}
+                                        </Trans>
+                                    </p>
+                                )}
+                                {entry.type === 'ORDER_FULFILLMENT_TRANSITION' && entry.data.from !== 'Created' && (
+                                    <p className="text-xs text-muted-foreground">
+                                        <Trans>
+                                            Fulfillment #{entry.data.fulfillmentId} from {entry.data.from} to {entry.data.to}
+                                        </Trans>
+                                    </p>
+                                )}
+                                {entry.type === 'ORDER_FULFILLMENT' && (
+                                    <p className="text-xs text-muted-foreground">
+                                        <Trans>
+                                            Fulfillment #{entry.data.fulfillmentId} created
+                                        </Trans>
+                                    </p>
+                                )}
+                                {entry.type === 'ORDER_MODIFIED' && (
+                                    <p className="text-xs text-muted-foreground">
+                                        <Trans>
+                                            Order modification #{entry.data.modificationId}
+                                        </Trans>
+                                    </p>
+                                )}
+                                {entry.type === 'ORDER_CUSTOMER_UPDATED' && (
+                                    <p className="text-xs text-muted-foreground">
+                                        <Trans>
+                                            Customer information updated
+                                        </Trans>
+                                    </p>
+                                )}
+                                {entry.type === 'ORDER_CANCELLATION' && (
+                                    <p className="text-xs text-muted-foreground">
+                                        <Trans>
+                                            Order cancelled
+                                        </Trans>
+                                    </p>
+                                )}
+                            </HistoryEntry>
+                        );
+                    } else {
+                        // Secondary group
+                        const shouldCollapse = group.entries.length > 2;
+                        const isExpanded = expandedGroups.has(groupIndex);
+                        const visibleEntries =
+                            shouldCollapse && !isExpanded ? group.entries.slice(0, 2) : group.entries;
+
+                        return (
+                            <div key={`group-${groupIndex}`}>
+                                {visibleEntries.map(({ entry }) => (
+                                    <HistoryEntry
+                                        key={entry.id}
+                                        entry={entry}
+                                        isNoteEntry={entry.type === 'ORDER_NOTE'}
+                                        timelineIcon={getTimelineIcon(entry)}
+                                        title={getTitle(entry)}
+                                        isPrimary={false}
+                                        customer={order.customer}
+                                    >
+                                        {entry.type === 'ORDER_NOTE' && (
+                                            <div className="space-y-1">
+                                                <p className="text-xs text-foreground">{entry.data.note}</p>
+                                                <Badge
+                                                    variant={entry.isPublic ? 'outline' : 'secondary'}
+                                                    className="text-xs"
+                                                >
+                                                    {entry.isPublic ? 'Public' : 'Private'}
+                                                </Badge>
+                                            </div>
+                                        )}
+                                        {entry.type === 'ORDER_STATE_TRANSITION' &&
+                                            entry.data.from !== 'Created' && (
+                                                <p className="text-xs text-muted-foreground">
+                                                    <Trans>
+                                                        From {entry.data.from} to {entry.data.to}
+                                                    </Trans>
+                                                </p>
+                                            )}
+                                        {entry.type === 'ORDER_PAYMENT_TRANSITION' && (
+                                            <p className="text-xs text-muted-foreground">
+                                                <Trans>
+                                                    Payment #{entry.data.paymentId} transitioned to{' '}
+                                                    {entry.data.to}
+                                                </Trans>
+                                            </p>
+                                        )}
+                                        {entry.type === 'ORDER_REFUND_TRANSITION' && (
+                                            <p className="text-xs text-muted-foreground">
+                                                <Trans>
+                                                    Refund #{entry.data.refundId} transitioned to {entry.data.to}
+                                                </Trans>
+                                            </p>
+                                        )}
+                                        {entry.type === 'ORDER_FULFILLMENT_TRANSITION' && entry.data.from !== 'Created' && (
+                                            <p className="text-xs text-muted-foreground">
+                                                <Trans>
+                                                    Fulfillment #{entry.data.fulfillmentId} from {entry.data.from} to {entry.data.to}
+                                                </Trans>
+                                            </p>
+                                        )}
+                                        {entry.type === 'ORDER_FULFILLMENT' && (
+                                            <p className="text-xs text-muted-foreground">
+                                                <Trans>
+                                                    Fulfillment #{entry.data.fulfillmentId} created
+                                                </Trans>
+                                            </p>
+                                        )}
+                                        {entry.type === 'ORDER_MODIFIED' && (
+                                            <p className="text-xs text-muted-foreground">
+                                                <Trans>
+                                                    Order modification #{entry.data.modificationId}
+                                                </Trans>
+                                            </p>
+                                        )}
+                                        {entry.type === 'ORDER_CUSTOMER_UPDATED' && (
+                                            <p className="text-xs text-muted-foreground">
+                                                <Trans>
+                                                    Customer information updated
+                                                </Trans>
+                                            </p>
+                                        )}
+                                        {entry.type === 'ORDER_CANCELLATION' && (
+                                            <p className="text-xs text-muted-foreground">
+                                                <Trans>
+                                                    Order cancelled
+                                                </Trans>
+                                            </p>
+                                        )}
+                                    </HistoryEntry>
+                                ))}
+
+                                {shouldCollapse && (
+                                    <div className="flex justify-center py-2">
+                                        <Button
+                                            variant="ghost"
+                                            size="sm"
+                                            onClick={() => toggleGroup(groupIndex)}
+                                            className="text-muted-foreground hover:text-foreground h-6 text-xs"
+                                        >
+                                            {isExpanded ? (
+                                                <>
+                                                    <ChevronUp className="w-3 h-3 mr-1" />
+                                                    <Trans>Show less</Trans>
+                                                </>
+                                            ) : (
+                                                <>
+                                                    <ChevronDown className="w-3 h-3 mr-1" />
+                                                    <Trans>Show all ({group.entries.length})</Trans>
+                                                </>
+                                            )}
+                                        </Button>
+                                    </div>
+                                )}
                             </div>
-                        )}
-                        <div className="text-sm text-muted-foreground">
-                            {entry.type === 'ORDER_STATE_TRANSITION' && entry.data.from !== 'Created' && (
-                                <Trans>
-                                    From {entry.data.from} to {entry.data.to}
-                                </Trans>
-                            )}
-                            {entry.type === 'ORDER_PAYMENT_TRANSITION' && (
-                                <Trans>
-                                    Payment #{entry.data.paymentId} transitioned to {entry.data.to}
-                                </Trans>
-                            )}
-                        </div>
-                    </HistoryEntry>
-                ))}
+                        );
+                    }
+                })}
             </HistoryTimeline>
         </div>
     );

+ 14 - 8
packages/dashboard/src/lib/components/data-input/rich-text-input.tsx

@@ -1,9 +1,8 @@
-import { BubbleMenu, Editor, EditorContent, useCurrentEditor, useEditor } from '@tiptap/react';
-import StarterKit from '@tiptap/starter-kit';
-import ListItem from '@tiptap/extension-list-item';
 import TextStyle from '@tiptap/extension-text-style';
+import { BubbleMenu, Editor, EditorContent, useEditor } from '@tiptap/react';
+import StarterKit from '@tiptap/starter-kit';
 import { BoldIcon, ItalicIcon, StrikethroughIcon } from 'lucide-react';
-import { useEffect, useLayoutEffect } from 'react';
+import { useLayoutEffect, useRef } from 'react';
 import { Button } from '../ui/button.js';
 
 // define your extension array
@@ -27,6 +26,8 @@ export interface RichTextInputProps {
 }
 
 export function RichTextInput({ value, onChange }: Readonly<RichTextInputProps>) {
+    const isInternalUpdate = useRef(false);
+
     const editor = useEditor({
         parseOptions: {
             preserveWhitespace: 'full',
@@ -34,6 +35,7 @@ export function RichTextInput({ value, onChange }: Readonly<RichTextInputProps>)
         extensions: extensions,
         content: value,
         onUpdate: ({ editor }) => {
+            isInternalUpdate.current = true;
             onChange(editor.getHTML());
         },
         editorProps: {
@@ -44,11 +46,15 @@ export function RichTextInput({ value, onChange }: Readonly<RichTextInputProps>)
     });
 
     useLayoutEffect(() => {
-        if (editor) {
-            const { from, to } = editor.state.selection;
-            editor.commands.setContent(value, false);
-            editor.commands.setTextSelection({ from, to });
+        if (editor && !isInternalUpdate.current) {
+            const currentContent = editor.getHTML();
+            if (currentContent !== value) {
+                const { from, to } = editor.state.selection;
+                editor.commands.setContent(value, false);
+                editor.commands.setTextSelection({ from, to });
+            }
         }
+        isInternalUpdate.current = false;
     }, [value, editor]);
 
     if (!editor) {

+ 17 - 4
packages/dashboard/src/lib/components/data-table/data-table-bulk-actions.tsx

@@ -9,6 +9,7 @@ import {
 } from '@/vdb/components/ui/dropdown-menu.js';
 import { getBulkActions } from '@/vdb/framework/data-table/data-table-extensions.js';
 import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
+import { useFloatingBulkActions } from '@/vdb/hooks/use-floating-bulk-actions.js';
 import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
 import { usePage } from '@/vdb/hooks/use-page.js';
 import { Trans } from '@/vdb/lib/trans.js';
@@ -26,7 +27,8 @@ export function DataTableBulkActions<TData>({
     bulkActions,
 }: Readonly<DataTableBulkActionsProps<TData>>) {
     const { pageId } = usePage();
-    const { blockId } = usePageBlock();
+    const pageBlock = usePageBlock();
+    const blockId = pageBlock?.blockId;
 
     // Cache to store selected items across page changes
     const selectedItemsCache = useRef<Map<string, TData>>(new Map());
@@ -52,7 +54,13 @@ export function DataTableBulkActions<TData>({
         })
         .filter((item): item is TData => item !== undefined);
 
-    if (selection.length === 0) {
+    const { position, shouldShow } = useFloatingBulkActions({
+        selectionCount: selection.length,
+        containerSelector: '[data-table-root], .data-table-container, table',
+        bottomOffset: 40,
+    });
+
+    if (!shouldShow) {
         return null;
     }
     const extendedBulkActions = pageId ? getBulkActions(pageId, blockId) : [];
@@ -61,8 +69,13 @@ export function DataTableBulkActions<TData>({
 
     return (
         <div
-            className="flex items-center gap-4 px-8 py-2 animate-in fade-in duration-200 absolute bottom-10 left-1/2 transform -translate-x-1/2 bg-white shadow-2xl rounded-md border"
-            style={{ height: 'auto', maxHeight: '60px' }}
+            className="flex items-center gap-4 px-8 py-2 animate-in fade-in duration-200 fixed transform -translate-x-1/2 bg-white shadow-2xl rounded-md border z-50"
+            style={{ 
+                height: 'auto', 
+                maxHeight: '60px',
+                bottom: position.bottom,
+                left: position.left
+            }}
         >
             <span className="text-sm text-muted-foreground">
                 <Trans>{selection.length} selected</Trans>

+ 2 - 3
packages/dashboard/src/lib/components/layout/nav-main.tsx

@@ -150,7 +150,7 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
     const renderBottomSection = (item: NavMenuSection | NavMenuItem) => {
         if ('url' in item) {
             return (
-                <NavItemWrapper key={item.title} locationId={item.id} order={item.order}>
+                <NavItemWrapper key={item.title} locationId={item.id} order={item.order} offset={true}>
                     <SidebarMenuItem>
                         <SidebarMenuButton
                             tooltip={item.title}
@@ -167,7 +167,7 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
             );
         }
         return (
-            <NavItemWrapper key={item.title} locationId={item.id} order={item.order}>
+            <NavItemWrapper key={item.title} locationId={item.id} order={item.order} offset={true}>
                 <Collapsible
                     asChild
                     open={openBottomSectionId === item.id}
@@ -218,7 +218,6 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
         <>
             {/* Top sections */}
             <SidebarGroup>
-                <SidebarGroupLabel>Platform</SidebarGroupLabel>
                 <SidebarMenu>{topSections.map(renderTopSection)}</SidebarMenu>
             </SidebarGroup>
 

+ 19 - 4
packages/dashboard/src/lib/components/shared/asset/asset-bulk-actions.tsx

@@ -8,6 +8,7 @@ import {
     DropdownMenuTrigger,
 } from '@/vdb/components/ui/dropdown-menu.js';
 import { getBulkActions } from '@/vdb/framework/data-table/data-table-extensions.js';
+import { useFloatingBulkActions } from '@/vdb/hooks/use-floating-bulk-actions.js';
 import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
 import { usePage } from '@/vdb/hooks/use-page.js';
 import { Trans } from '@/vdb/lib/trans.js';
@@ -34,9 +35,15 @@ interface AssetBulkActionsProps {
 
 export function AssetBulkActions({ selection, bulkActions, refetch }: Readonly<AssetBulkActionsProps>) {
     const { pageId } = usePage();
-    const { blockId } = usePageBlock();
+    const pageBlock = usePageBlock();
+    const blockId = pageBlock?.blockId;
+    
+    const { position, shouldShow } = useFloatingBulkActions({
+        selectionCount: selection.length,
+        containerSelector: '[data-asset-gallery]'
+    });
 
-    if (selection.length === 0) {
+    if (!shouldShow) {
         return null;
     }
 
@@ -62,13 +69,21 @@ export function AssetBulkActions({ selection, bulkActions, refetch }: Readonly<A
     allBulkActions.sort((a, b) => (a.order ?? 10_000) - (b.order ?? 10_000));
 
     return (
-        <div className="flex items-center gap-2 px-2 py-1 mb-2 bg-muted/50 rounded-md border">
+        <div
+            className="flex items-center gap-4 px-8 py-2 animate-in fade-in duration-200 fixed transform -translate-x-1/2 bg-white shadow-2xl rounded-md border z-50"
+            style={{ 
+                height: 'auto', 
+                maxHeight: '60px',
+                bottom: position.bottom,
+                left: position.left
+            }}
+        >
             <span className="text-sm text-muted-foreground">
                 <Trans>{selection.length} selected</Trans>
             </span>
             <DropdownMenu>
                 <DropdownMenuTrigger asChild>
-                    <Button variant="outline" size="sm" className="h-8">
+                    <Button variant="outline" size="sm" className="h-8 shadow-none">
                         <Trans>With selected...</Trans>
                         <ChevronDown className="ml-2 h-4 w-4" />
                     </Button>

+ 2 - 2
packages/dashboard/src/lib/components/shared/asset/asset-gallery.tsx

@@ -232,7 +232,7 @@ export function AssetGallery({
     };
 
     return (
-        <div className={`flex flex-col w-full ${fixedHeight ? 'h-[600px]' : ''} ${className}`}>
+        <div className={`relative flex flex-col w-full ${fixedHeight ? 'h-[600px]' : ''} ${className}`}>
             {showHeader && (
                 <div className="flex flex-col md:flex-row gap-2 mb-4 flex-shrink-0">
                     <div className="relative flex-grow flex items-center gap-2">
@@ -291,7 +291,7 @@ export function AssetGallery({
                     </div>
                 )}
 
-                <div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3 p-1">
+                <div data-asset-gallery className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3 p-1">
                     {isLoading ? (
                         <div className="col-span-full flex justify-center py-12">
                             <Loader2 className="h-8 w-8 animate-spin text-primary" />

+ 37 - 0
packages/dashboard/src/lib/components/shared/history-timeline/history-entry-date.tsx

@@ -0,0 +1,37 @@
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/vdb/components/ui/tooltip.js';
+import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
+
+interface HistoryEntryDateProps {
+    date: string;
+    className?: string;
+}
+
+export function HistoryEntryDate({ date, className }: Readonly<HistoryEntryDateProps>) {
+    const { formatRelativeDate, formatDate } = useLocalFormat();
+
+    const formatFullDateTime = (dateString: string) => {
+        return formatDate(dateString, {
+            year: 'numeric',
+            month: 'long',
+            day: 'numeric',
+            hour: 'numeric',
+            minute: 'numeric',
+            second: 'numeric',
+        });
+    };
+
+    return (
+        <TooltipProvider>
+            <Tooltip>
+                <TooltipTrigger asChild>
+                    <div className={className}>
+                        {formatRelativeDate(date)}
+                    </div>
+                </TooltipTrigger>
+                <TooltipContent>
+                    <p>{formatFullDateTime(date)}</p>
+                </TooltipContent>
+            </Tooltip>
+        </TooltipProvider>
+    );
+}

+ 80 - 75
packages/dashboard/src/lib/components/shared/history-timeline/history-entry.tsx

@@ -1,16 +1,5 @@
-import { Button } from '@/vdb/components/ui/button.js';
-import {
-    DropdownMenu,
-    DropdownMenuContent,
-    DropdownMenuItem,
-    DropdownMenuTrigger,
-} from '@/vdb/components/ui/dropdown-menu.js';
-import { Separator } from '@/vdb/components/ui/separator.js';
-import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
-import { Trans } from '@/vdb/lib/trans.js';
-import { MoreVerticalIcon, PencilIcon, TrashIcon } from 'lucide-react';
-import { useCallback } from 'react';
-import { useHistoryTimeline } from './history-timeline.js';
+import { cn } from '@/vdb/lib/utils.js';
+import { HistoryEntryDate } from './history-entry-date.js';
 
 export interface HistoryEntryItem {
     id: string;
@@ -25,12 +14,19 @@ export interface HistoryEntryItem {
     data: any;
 }
 
+interface OrderCustomer {
+    firstName: string;
+    lastName: string;
+}
+
 interface HistoryEntryProps {
     entry: HistoryEntryItem;
     isNoteEntry: boolean;
     timelineIcon: React.ReactNode;
     title: string | React.ReactNode;
     children: React.ReactNode;
+    isPrimary?: boolean;
+    customer?: OrderCustomer | null;
 }
 
 export function HistoryEntry({
@@ -39,80 +35,89 @@ export function HistoryEntry({
     timelineIcon,
     title,
     children,
+    isPrimary = true,
+    customer,
 }: Readonly<HistoryEntryProps>) {
-    const { formatDate } = useLocalFormat();
-    const { editNote, deleteNote } = useHistoryTimeline();
+    const getIconColor = (type: string) => {
+        // Check for success states (payment settled, order delivered)
+        if (type === 'ORDER_PAYMENT_TRANSITION' && entry.data.to === 'Settled') {
+            return 'bg-success text-success-foreground';
+        }
+        if (type === 'ORDER_STATE_TRANSITION' && entry.data.to === 'Delivered') {
+            return 'bg-success text-success-foreground';
+        }
+        if (type === 'ORDER_FULFILLMENT_TRANSITION' && entry.data.to === 'Delivered') {
+            return 'bg-success text-success-foreground';
+        }
+        
+        // Check for destructive states (cancellations)
+        if (type === 'ORDER_CANCELLATION') {
+            return 'bg-destructive text-destructive-foreground';
+        }
+        if (type === 'ORDER_STATE_TRANSITION' && entry.data.to === 'Cancelled') {
+            return 'bg-destructive text-destructive-foreground';
+        }
+        if (type === 'ORDER_PAYMENT_TRANSITION' && (entry.data.to === 'Declined' || entry.data.to === 'Cancelled')) {
+            return 'bg-destructive text-destructive-foreground';
+        }
+        
+        // All other entries use neutral colors
+        return 'bg-muted text-muted-foreground';
+    };
 
-    const formatDateTime = useCallback(
-        (date: string) => {
-            return formatDate(date, {
-                year: 'numeric',
-                month: 'long',
-                day: 'numeric',
-                hour: 'numeric',
-                minute: 'numeric',
-                second: 'numeric',
-            });
-        },
-        [formatDate],
-    );
+    const getActorName = () => {
+        if (entry.administrator) {
+            return `${entry.administrator.firstName} ${entry.administrator.lastName}`;
+        } else if (customer) {
+            return `${customer.firstName} ${customer.lastName}`;
+        }
+        return '';
+    };
 
     return (
-        <div key={entry.id} className="relative mb-4 pl-11">
-            <div className="absolute left-0 w-10 flex items-center justify-center">
-                <div className={`rounded-full flex items-center justify-center h-6 w-6`}>
+        <div key={entry.id} className="relative group">
+            <div
+                className={`flex gap-3 p-3 rounded-lg hover:bg-muted/30 transition-colors ${!isPrimary ? 'opacity-75' : ''}`}
+            >
+                <div className={cn(`relative z-10 flex-shrink-0 ${isNoteEntry ? 'ml-2' : ''}`, isPrimary ? '-ml-1' : '')}>
                     <div
-                        className={`rounded-full bg-muted text-muted-foreground flex items-center justify-center h-6 w-6`}
+                        className={`rounded-full flex items-center justify-center ${isPrimary ? 'h-8 w-8' : 'h-6 w-6'} ${getIconColor(entry.type)} border-2 border-background ${isPrimary ? 'shadow-sm' : 'shadow-none'}`}
                     >
-                        {timelineIcon}
+                        <div className={isPrimary ? 'text-current' : 'text-current scale-75'}>
+                            {timelineIcon}
+                        </div>
                     </div>
                 </div>
-            </div>
 
-            <div className="px-4 rounded-md">
-                <div className="mt-2 text-sm text-muted-foreground flex items-center">
-                    <span>{formatDateTime(entry.createdAt)}</span>
-                    {entry.administrator && (
-                        <span className="ml-2">
-                            {entry.administrator.firstName} {entry.administrator.lastName}
-                        </span>
-                    )}
-                </div>
-                <div className="flex items-start justify-between">
-                    <div>
-                        <div className="font-medium text-sm">{title}</div>
-                        {children}
-                    </div>
+                <div className="flex-1 min-w-0">
+                    <div className="flex items-start justify-between">
+                        <div className="flex-1 min-w-0">
+                            <h4
+                                className={`text-sm ${isPrimary ? 'font-medium text-foreground' : 'font-normal text-muted-foreground'}`}
+                            >
+                                {title}
+                            </h4>
+                            <div className="mt-1">{children}</div>
+                        </div>
 
-                    {isNoteEntry && (
-                        <DropdownMenu>
-                            <DropdownMenuTrigger asChild>
-                                <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
-                                    <MoreVerticalIcon className="h-4 w-4" />
-                                </Button>
-                            </DropdownMenuTrigger>
-                            <DropdownMenuContent align="end">
-                                <DropdownMenuItem
-                                    onClick={() => editNote(entry.id, entry.data.note, !entry.isPublic)}
-                                    className="cursor-pointer"
-                                >
-                                    <PencilIcon className="mr-2 h-4 w-4" />
-                                    <Trans>Edit</Trans>
-                                </DropdownMenuItem>
-                                <Separator className="my-1" />
-                                <DropdownMenuItem
-                                    onClick={() => deleteNote(entry.id)}
-                                    className="cursor-pointer text-red-600 focus:text-red-600"
-                                >
-                                    <TrashIcon className="mr-2 h-4 w-4" />
-                                    <span>Delete</span>
-                                </DropdownMenuItem>
-                            </DropdownMenuContent>
-                        </DropdownMenu>
-                    )}
+                        <div className="flex items-center gap-2 ml-4 flex-shrink-0">
+                            <div className="text-right">
+                                <HistoryEntryDate
+                                    date={entry.createdAt}
+                                    className={`text-xs cursor-help ${isPrimary ? 'text-muted-foreground' : 'text-muted-foreground/70'}`}
+                                />
+                                {getActorName() && (
+                                    <div
+                                        className={`text-xs ${isPrimary ? 'text-muted-foreground' : 'text-muted-foreground/70'}`}
+                                    >
+                                        {getActorName()}
+                                    </div>
+                                )}
+                            </div>
+                        </div>
+                    </div>
                 </div>
             </div>
-            <div className="border-b border-muted my-4 mx-4"></div>
         </div>
     );
 }

+ 4 - 4
packages/dashboard/src/lib/components/shared/history-timeline/history-note-input.tsx

@@ -19,17 +19,17 @@ export function HistoryNoteInput({ onAddNote }: Readonly<HistoryNoteInputProps>)
     };
 
     return (
-        <div className="border rounded-md p-4">
-            <div className="flex flex-col space-y-4">
+        <div className="bg-muted/20 rounded-lg p-3 mb-4">
+            <div className="flex flex-col space-y-2">
                 <Textarea
                     placeholder="Add a note..."
                     value={note}
                     onChange={e => setNote(e.target.value)}
-                    className="min-h-[80px] resize-none"
+                    className="min-h-[50px] resize-none border-0 bg-background/50 focus:bg-background transition-colors text-sm"
                 />
                 <div className="flex items-center justify-between">
                     <HistoryNoteCheckbox value={noteIsPrivate} onChange={setNoteIsPrivate} />
-                    <Button onClick={handleAddNote} disabled={!note.trim()} size="sm">
+                    <Button onClick={handleAddNote} disabled={!note.trim()} size="sm" className="h-7 px-3 text-xs">
                         Add note
                     </Button>
                 </div>

+ 5 - 3
packages/dashboard/src/lib/components/shared/history-timeline/history-timeline.tsx

@@ -43,10 +43,12 @@ export function HistoryTimeline({ children, onEditNote, onDeleteNote }: Readonly
 
     return (
         <HistoryTimelineContext.Provider value={{ editNote, deleteNote }}>
-            <ScrollArea className=" pr-4">
+            <ScrollArea className="pr-2">
                 <div className="relative">
-                    <div className="absolute left-5 top-0 bottom-[44px] w-0.5 bg-gray-200" />
-                    {children}
+                    <div className="absolute left-6 top-6 bottom-0 w-px bg-gradient-to-b from-border via-border/50 to-transparent" />
+                    <div className="space-y-0.5">
+                        {children}
+                    </div>
                 </div>
             </ScrollArea>
             <HistoryNoteEditor

+ 0 - 0
packages/dashboard/src/lib/components/shared/rich-text-editor.tsx


+ 2 - 2
packages/dashboard/src/lib/framework/defaults.ts

@@ -125,7 +125,7 @@ export function registerDefaults() {
                 icon: Terminal,
                 defaultOpen: false,
                 placement: 'bottom',
-                order: 100,
+                order: 200,
                 items: [
                     {
                         id: 'job-queue',
@@ -153,7 +153,7 @@ export function registerDefaults() {
                 icon: Settings2,
                 defaultOpen: false,
                 placement: 'bottom',
-                order: 200,
+                order: 100,
                 items: [
                     {
                         id: 'sellers',

+ 82 - 0
packages/dashboard/src/lib/hooks/use-floating-bulk-actions.ts

@@ -0,0 +1,82 @@
+import { useEffect, useState } from 'react';
+
+interface FloatingPosition {
+    bottom: string;
+    left: string;
+}
+
+interface UseFloatingBulkActionsOptions {
+    selectionCount: number;
+    containerSelector: string;
+    bufferDistance?: number;
+    bottomOffset?: number;
+}
+
+/**
+ * Common logic used to power floating the bulk action component used in
+ * data tables and in the asset gallery.
+ */
+export function useFloatingBulkActions({
+    selectionCount,
+    containerSelector,
+    bufferDistance = 80,
+    bottomOffset = 10,
+}: Readonly<UseFloatingBulkActionsOptions>) {
+    const [position, setPosition] = useState<FloatingPosition>({ bottom: '2.5rem', left: '50%' });
+    const [isPositioned, setIsPositioned] = useState(false);
+
+    useEffect(() => {
+        if (selectionCount === 0) {
+            setIsPositioned(false);
+            return;
+        }
+
+        const updatePosition = () => {
+            const container = document.querySelector(containerSelector)?.closest('div') as HTMLElement;
+            if (!container) return;
+
+            const containerRect = container.getBoundingClientRect();
+            const viewportHeight = window.innerHeight;
+
+            // Check if container bottom is visible in viewport
+            const containerBottom = containerRect.bottom;
+            const isContainerFullyVisible = containerBottom <= viewportHeight - bufferDistance;
+
+            // Calculate horizontal center
+            const containerLeft = containerRect.left;
+            const containerWidth = containerRect.width;
+            const centerX = containerLeft + containerWidth / 2;
+
+            if (isContainerFullyVisible) {
+                // Position relative to container bottom
+                setPosition({
+                    bottom: `${viewportHeight - containerBottom + bottomOffset}px`,
+                    left: `${centerX}px`,
+                });
+            } else {
+                // Position relative to viewport bottom, centered in container
+                setPosition({
+                    bottom: '2.5rem',
+                    left: `${centerX}px`,
+                });
+            }
+
+            setIsPositioned(true);
+        };
+
+        updatePosition();
+        window.addEventListener('scroll', updatePosition);
+        window.addEventListener('resize', updatePosition);
+
+        return () => {
+            window.removeEventListener('scroll', updatePosition);
+            window.removeEventListener('resize', updatePosition);
+        };
+    }, [selectionCount, containerSelector, bufferDistance]);
+
+    return {
+        position,
+        isPositioned,
+        shouldShow: selectionCount > 0 && isPositioned,
+    };
+}

+ 20 - 5
packages/dashboard/src/lib/hooks/use-local-format.ts

@@ -77,15 +77,30 @@ export function useLocalFormat() {
             if (diffSeconds < 60) {
                 return new Intl.RelativeTimeFormat(locale, options).format(diffSeconds * -1, 'seconds');
             } else if (diffSeconds < 3600) {
-                return new Intl.RelativeTimeFormat(locale, options).format(diffSeconds * -1, 'minutes');
+                return new Intl.RelativeTimeFormat(locale, options).format(
+                    Math.floor((diffSeconds / 60) * -1),
+                    'minutes',
+                );
             } else if (diffSeconds < 86400) {
-                return new Intl.RelativeTimeFormat(locale, options).format(diffSeconds * -1, 'hours');
+                return new Intl.RelativeTimeFormat(locale, options).format(
+                    Math.floor((diffSeconds / 3600) * -1),
+                    'hours',
+                );
             } else if (diffSeconds < 2592000) {
-                return new Intl.RelativeTimeFormat(locale, options).format(diffSeconds * -1, 'days');
+                return new Intl.RelativeTimeFormat(locale, options).format(
+                    Math.floor((diffSeconds / 86400) * -1),
+                    'days',
+                );
             } else if (diffSeconds < 31536000) {
-                return new Intl.RelativeTimeFormat(locale, options).format(diffSeconds * -1, 'months');
+                return new Intl.RelativeTimeFormat(locale, options).format(
+                    Math.floor((diffSeconds / 2592000) * -1),
+                    'months',
+                );
             } else {
-                return new Intl.RelativeTimeFormat(locale, options).format(diffSeconds * -1, 'years');
+                return new Intl.RelativeTimeFormat(locale, options).format(
+                    Math.floor((diffSeconds / 31536000) * -1),
+                    'years',
+                );
             }
         },
         [locale],

+ 0 - 1
packages/dashboard/src/lib/index.ts

@@ -92,7 +92,6 @@ export * from './components/shared/paginated-list-data-table.js';
 export * from './components/shared/permission-guard.js';
 export * from './components/shared/product-variant-selector.js';
 export * from './components/shared/remove-from-channel-bulk-action.js';
-export * from './components/shared/rich-text-editor.js';
 export * from './components/shared/role-code-label.js';
 export * from './components/shared/role-selector.js';
 export * from './components/shared/seller-selector.js';